侧边栏壁纸
博主头像
蔚然小站博主等级

未来会有的,不要辜负了梦想

  • 累计撰写 35 篇文章
  • 累计创建 14 个标签
  • 累计收到 9 条评论

目 录CONTENT

文章目录

VMM为Guest准备物理内存

皮蛋熊
2023-08-26 / 0 评论 / 0 点赞 / 115 阅读 / 8841 字
温馨提示:
本文最后更新于 2023-08-26,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

计算机启动过程中,会先将主板的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这个功能。

  1. 将自己实现的模拟BIOS中的查询内存系统的中断处理函数,直接置于内存0x000F0000~0x000FFFFF中。
  2. 建立IVT时,设置第0x15个表项中的中断函数地址指向模拟的BIOS中的内存查询处理函数地址。
  3. 真实物理系统,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内存相关图:

Pasted image 20220717225504

内核是如何获取内存的

  1. 根据上面的结构体,我们知道e820_entry结构体总共占用20Bytes空间,
  2. 内核构建eax(0x0000e820举例),es:di(E820MAP宏(0X2D0),内核零页中,内核在实模式下通过BIOS获取的信息存储在这里,内核进入保护模式后,会读取到这里的信息),ecx(20,e820_entry结构体大小),edx(0x534d4150魔术字),然后调用中断服务函数(int 15)。刚开始ebx=0,每调用一次0x15中断会加1(由中断来加),如果内存遍历完了ebx会被中断服务函数置零。
  3. 中断函数会把相关记录复制到上面的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 */

建立内存段信息

Pasted image 20220718114519

典型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中:

  1. kvm需要在这个地址准备号0x15号中断处理函数
  2. 在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 */
};

Pasted image 20220719134341

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模块收到用户空间传递下来的内存信息后将创建具体的内存条实例:

Pasted image 20220719142559

05:定义了一个新的内存条 07:将Byte为单位转为Page为单位 14~19:为内存条准备页面

当Guest发生缺页异常陷入KGM时,KVM的缺页异常处理函数将根据缺页异常地址GPA,在内存条页面数组phys_mem中分配空间的物理页面。

Pasted image 20220719143254

这种就是预留所有内存的方式。也有非全部预留的方式,与内存条相关的数据结构会多一个userspace_addr字段。

Pasted image 20220719143803

当Guest发生缺页异常陷入KVM时,KVM根据缺页地址,计算出该地址属于的内存条,以内存条在Host用户空间的起始地址userspace_addr为基址,加上缺页地址在内存段内的偏移,即可将GPA空间的地址转换为HVA空间的地址,此时内核可以使用函数get_user_page按需动态为虚拟地址分配物理页面了。

Pasted image 20220719144219

参考文献

  1. 《深度探索Linux系统虚拟化:原理与实现》 - 王柏生 & 谢广军
0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区