计算机启动过程中,会先将主板的BIOS ROM映射到内存空间,比如x86架构32位处理器会把BIOS ROM映射到0x000F0000~0x000FFFFF
,然后CPU跳转到0xF000:0xFFF0
处,开始运行BIOS代码。BIOS会检查内存信息,记录下来,并对外提供内存信息查询功能。
BIOS将中断向量表(IVT)的第0x15表项中的地址设置为查询内存信息函数的地址,后序的bootloader或者OS就可以通过发起中断号为0x15的软中断,调用BIOS中的这个函数,获取系统内存信息。
因此,为了给Guest提供获取内存系统信息的功能,VMM需要模拟BIOS的这个功能。
VMM支持guest获取内存系统信息需要模拟BIOS这个功能。
- 将自己实现的模拟BIOS中的查询内存系统的中断处理函数,直接置于内存
0x000F0000~0x000FFFFF
中。 - 建立IVT时,设置第0x15个表项中的中断函数地址指向模拟的BIOS中的内存查询处理函数地址。
- 真实物理系统,BIOS会查询真实的物理内存情况。而VMM而言,则需要根据用户配置的虚拟机的内存信息,在BIOS数据区中制造内存信息表。
中断号为0x15的BIOS中断处理函数,根据传入的参数(寄存器 eax,ah)中的值,将返回不同的内存信息。如
- ah=0x8a,中断处理函数将返回扩展内存大小,即地址在1MB以上的内存的尺寸。
- ah=0x88,最多可以返回64MB内存,超过了也是这么多
- eax=0xE820,0x15号中断处理函数将返回主机的完整内存信息。
由于技术的演进与迭代,内存地址空间被分为了许多不连续的段。对于使用0xE820方式获取内存而言,0x15号中断处理函数使用结构体e820_entry描述每个段,包括内存段的起始地址、内存段的大小以及内存段的类型:
kvmtool.git/include/kvm/e820.h
struct e820_entry {
uint64_t addr; /* start of memory segment */
uint64_t size; /* size of memory segment */
uint32_t type; /* type of memory segment */
} __attribute__((packed));
VMM需要模拟BIOS内存相关图:
内核是如何获取内存的
- 根据上面的结构体,我们知道e820_entry结构体总共占用20Bytes空间,
- 内核构建eax(0x0000e820举例),es:di(E820MAP宏(0X2D0),内核零页中,内核在实模式下通过BIOS获取的信息存储在这里,内核进入保护模式后,会读取到这里的信息),ecx(20,e820_entry结构体大小),edx(0x534d4150魔术字),然后调用中断服务函数(int 15)。刚开始ebx=0,每调用一次0x15中断会加1(由中断来加),如果内存遍历完了ebx会被中断服务函数置零。
- 中断函数会把相关记录复制到上面的di所指定的地方。
linux-2.3.16/arch/i386/boot/setup.S
01 meme820:
02 mov edx, #0x534d4150 ! ascii `SMAP'
03 xor ebx, ebx ! continuation counter
04
05 mov di, #E820MAP ! point into the whitelist
06 …
07 jmpe820:
08 mov eax, #0x0000e820 ! e820, upper word zeroed
09 mov ecx, #20 ! size of the e820rec
10 …
11 int 0x15 ! make the call
12 …
13 good820:
14 …
15 mov ax, di
16 add ax, #20
17 mov di, ax
18
19 again820:
20 cmp ebx, #0 ! check to see if ebx is
21 jne jmpe820 ! set to EOF
linux-2.3.16/ include/asm-i386/e820.h
22 #define E820MAP 0x2d0 /* our map */
建立内存段信息
典型x86架构的32位系统的内存映射kvmtool.git/kvm.c
void kvm__setup_mem(struct kvm *self)
{
struct e820_entry *mem_map;
unsigned char *size;
size = guest_flat_to_host(self, E820_MAP_SIZE);
mem_map = guest_flat_to_host(self, E820_MAP_START);
*size = 4;
mem_map[0] = (struct e820_entry) {
.addr = REAL_MODE_IVT_BEGIN,
.size = BDA_END - REAL_MODE_IVT_BEGIN,
.type = E820_MEM_RESERVED,
};
mem_map[1] = (struct e820_entry) {
.addr = BDA_END,
.size = EBDA_END - BDA_END,
.type = E820_MEM_USABLE,
};
mem_map[2] = (struct e820_entry) {
.addr = EBDA_END,
.size = BZ_KERNEL_START - EBDA_END,
.type = E820_MEM_RESERVED,
};
mem_map[3] = (struct e820_entry) {
.addr = BZ_KERNEL_START,
.size = self->ram_size - BZ_KERNEL_START,
.type = E820_MEM_USABLE,
};
}
#define BZ_KERNEL_START 0x100000UL
kvmtool.git/include/kvm/bios.h
#define E820_MAP_SIZE EBDA_START
#define E820_MAP_START (EBDA_START + 0x01)
kvm将内存段设为4个,存放在E820_MAP_SIZE
处,内核可以从这个位置读取内存段数。
准备中断0x15的处理函数以及设置IVT:
中断处理函数由BIOS实现,存储在BIOS ROM中,映射于内存空间0x000F0000~0x000FFFFF
中:
- kvm需要在这个地址准备号0x15号中断处理函数
- 在IVT中设置到0x15表项指向中断处理函数
kvmtool.git/include/kvm/bios.h
01 #define MB_BIOS_BEGIN 0x000f0000
02 #define MB_BIOS_END 0x000fffff
kvmtool.git/bios.c
03 void setup_bios(struct kvm *kvm)
04 {
05 unsigned long address = MB_BIOS_BEGIN;
06 …
07 address = BIOS_NEXT_IRQ_ADDR(address, bios_int10_size); // 确定0x15号中断处理函数的起始存储地址
08 bios_setup_irq_handler(kvm, address, 0x15, bios_int15,
09 bios_int15_size);
10 …
11 }
12 static void bios_setup_irq_handler(struct kvm *kvm, …)
13 {
14 struct real_intr_desc intr_desc;
15 void *p;
16
17 p = guest_flat_to_host(kvm, address); // 将GPA转为HVA
18 memcpy(p, handler, size);
19 intr_desc = (struct real_intr_desc) {
20 .segment = REAL_SEGMENT(address),
21 .offset = REAL_OFFSET(address),
22 };
23 interrupt_table__set(&kvm->interrupt_table,
24 &intr_desc, irq);
25 }
kvmtool.git/include/kvm/kvm.h
static inline void *guest_flat_to_host(struct kvm *self,
unsigned long offset)
{
return self->ram_start + offset;
}
void interrupt_table__set(struct interrupt_table *self,
struct real_intr_desc *entry, unsigned int num)
{
if (num < REAL_INTR_VECTORS)
self->entries[num] = *entry;
}
ram_start就是host在HVA空间为Guest分配的GPA的基地址。
组织好所有的IVT表项后,setup_bios最后调用函数interrupt_table__copy将表项一次性地复制到IVT中.
kvmtool.git/bios.c
void setup_bios(struct kvm *kvm)
{
…
p = guest_flat_to_host(kvm, 0); // IVT存储在物理内存0处
interrupt_table__copy(&kvm->interrupt_table, p,
REAL_INTR_SIZE);
}
kvmtool.git/interrupt.c
void interrupt_table__copy(struct interrupt_table *self,
void *dst, unsigned int size)
{
…
memcpy(dst, self->entries, sizeof(self->entries));
}
中断处理函数:
kvmtool.git/bios/int15.S
01 GLOBAL(bios_int15)
02 .incbin "bios/int15-real.bin"
03 GLOBAL(bios_int15_end)
kvmtool.git/bios/int15-real.S
04 ENTRY(___int15)
05 …
06 call e820_query_map
07 …
kvmtool.git/bios/e820.c
08 void e820_query_map(struct e820_query *query)
09 {
10 uint8_t map_size;
11 uint32_t ndx;
12
13 ndx = query->ebx;
14
15 map_size = rdfs8(E820_MAP_SIZE); // 读取e820记录数量
16
17 if (ndx < map_size) {
18 unsigned long start;
19 unsigned int i;
20 uint8_t *p;
21
22 start = E820_MAP_START +
23 sizeof(struct e820_entry) * ndx;
24
25 p = (void *) query->edi;
26
27 for (i = 0; i < sizeof(struct e820_entry); i++)
28 *p++ = rdfs8(start + i);
29 }
30
31 query->eax = SMAP;
32 query->ecx = 20;
33 query->ebx = ++ndx;
34
35 if (ndx >= map_size)
36 query->ebx = 0; /* end of map */
37 }
补充:ESI/EDI(源/目标索引寄存器,source/destination index),DS:ESI指向源串,ES:EDI指向目标串。
虚拟内存条:
创建虚拟机时,vmm也需要为虚拟机分配内存条。kvm_memory_region
供用户空间描述申请内存条的信息:
linux.git/include/linux/kvm.h
struct kvm_memory_region {
__u32 slot; // 第几个内存条
__u32 flags; //
__u64 guest_phys_addr; // 物理空间的起始地址
__u64 memory_size; /* bytes */
};
KVM在内核模块中定义的代表内存条实例的数据结构为kvm_memory_slot
。
linux.git/drivers/kvm/kvm.h
struct kvm_memory_slot {
gfn_t base_gfn; // 使用页帧号描述内存条的起始地址 guest_phys_addr
unsigned long npages;
unsigned long flags;
struct page **phys_mem; // 记录属于这个内存条里面的所有页面
unsigned long *dirty_bitmap;
};
KVM模块收到用户空间传递下来的内存信息后将创建具体的内存条实例:
05:定义了一个新的内存条 07:将Byte为单位转为Page为单位 14~19:为内存条准备页面
当Guest发生缺页异常陷入KGM时,KVM的缺页异常处理函数将根据缺页异常地址GPA,在内存条页面数组phys_mem
中分配空间的物理页面。
这种就是预留所有内存的方式。也有非全部预留的方式,与内存条相关的数据结构会多一个userspace_addr
字段。
当Guest发生缺页异常陷入KVM时,KVM根据缺页地址,计算出该地址属于的内存条,以内存条在Host用户空间的起始地址userspace_addr
为基址,加上缺页地址在内存段内的偏移,即可将GPA空间的地址转换为HVA空间的地址,此时内核可以使用函数get_user_page
按需动态为虚拟地址分配物理页面了。
参考文献
- 《深度探索Linux系统虚拟化:原理与实现》 - 王柏生 & 谢广军
评论区