x86虚拟内存和qemu内存虚拟化

2021-02-24 11:27:16 浏览数 (1)

内存虚拟化是一个很大的话题,最近安全部门发现了一个qemu内存虚拟化的安全漏洞,反馈给云平台让解决,感觉很棘手,引起了我对内存虚拟化的思考,想到什么问题就把思考记录下来。

x86虚拟内存

问题是由学习qemu MemoryRegion想到的,文档memory.rst中有一句话“memory banks used when the guest address space is smaller than the amount of RAM addressed”,说是alias类型MemoryRegion适用于这种场景,大概意思就是qemu给guest提供的物理内存超过了guest的address space,这时就得用alias类型的MemoryRegion了,那这儿的memory banks是什么意思,物理内存条有rank/bank,这儿的bank应当理解成岸,类似于一个岸把湖分成两半,和真正的内存条中的rank/bank没关系。qemu中有below_4g_mem_size和above_4g_mem_size两个MemoryRegion Alias,我觉得这个命名不好,如果加上userspce和kernelspace就好理解了,进程的虚拟地址空间分为用户空间和内核空间,两者执行权限不一样。所有进程内核空间都一样,只是用户空间不一样,这样所有进程就可以共享内核,所以需要在4G空间有一条线分成两部分。每个进程有自己的页目录,其中page table中关于内核部分指向相同,借用网上的这张图说明一下,假设CPU是32位,内核空间1G,用户态空间3G。

再想想虚拟地址空间是如果生成的,gcc编译源代码生成elf格式, linux内核load可执行程序elf格式文件生成虚拟地址空间,虚拟地址空间由段和页构成,段有code,data,heap和stack等, stack是固定大小,linux中的段都指向0,主要是page发挥作用。执行时大概是这样IP指令寄存器告诉MMU要加载的指令,如果page fault, 增加page然后建立映射关系, load指令到内存,其它load指令告诉MMU,要把数据放到内存中,不知道还区分数据总线和地址总线不,课本中学过,程序执行用的都是虚拟地址,反汇编可以看到虚拟地址。

虚拟地址空间设计成这样是因为内核不能发生pagefault,如果内核处理pagefault时发生pagefault没法玩了,所以说内核常驻物理内存中。用户态malloc一块内核,用虚拟地址访问发生pagefault,内核找一个page然后对应起来,那内核分配一个page的内存,内核先得到的是这个page的物理地址,然后把物理地址转换成内核虚拟地址,总之内核管理物理内存,并且和物理内存一一对应,为什么要一一对应没想明白,感觉这样实现是简单,内核经常需要在虚拟和物理地址之间转来转去的,一一对应用virt_to_phys和phys_to_virt就能实现虚拟和物理地址互相转换,简单性能高。也许是因为MMU不是一开始就开启的,内核在CPU处于实模式时创建early_level4_pgt和init_level4_pgt,切换到保护模式才开启MMU了,内核虚拟空间和物理内核一一对应是实模式要求这样的,如果不这样实模式时就没法操作了,要理解虚拟内存肯定得看懂实模式时代码干的活,否则还是有点虚。内核虚拟空间是1G,实际上内核只占用了896M虚拟空间,一一映射那就和物理地址0开始的896M,896M以上的物理地址就叫做high mem,kernel要访问就建立映射到它剩下的128M虚拟空间中,详见函数kmap_high。

为什么是896M?

https://stackoverflow.com/questions/4528568/how-does-the-linux-kernel-manage-less-than-1gb-physical-memory

Final kernel Page Table when RAM size is less than 896 MB - Linux Kernel Reference

前面说的都是内存,外设内存要是怎么访问的?个人理解外设内存分为配置,BAR和其它内存,配置内存是PCI规范指定的,配置内存中指定BAR空间开始地址和长度,BAR空间中指定其它内存如常说的显卡显存大小。设备写固件时在配置内存中指定BAR开始物理地址和长度,开机时bios遍历PCI总线发现PCI设备和内存,bios拼凑出物理地址空间,拼凑完有可能改变一个设备BAR的开始物理地址,把改变后的值重新写入配置内存中,配置内存个人理解是linux pci系统统一映射到内存中的,BAR是加载设备驱动时映射的,pci bar mmio理解为从pci configure space中得到bar的phy_addr,然后ioremap建立page entry,访问这个phy_addr,pci bus把请求路由给设备而不是内存。外设的其它内存CPU不能直接访问,只有外设自己可以访问,CPU要访问得委托外设DMA把数据写到内存,这部分内存地址CPU不处理,只要驱动和外设配合来就行了,外设可以访问内存,可以访问自己的这部分内存。

x86中cr3指定页目录,同一个进程系统调用从用户态切换到内核只切换stack和cpu context,不切换cr3,只有不同进程切换时才切换cr3。

32位CPU可以访问的物理内存最大是4G,但有了PAE就不一样了,一个CPU的虚拟地址空间还是4G,只是这4G不再局限于映射到物理低4G上了,可以访问的最大物理空间总和一定不会超过4G,总的几级页结构换算出来的值一定是4G,但page table entry的结果是64位,用其中46位,加上4k中的偏移即可以得到物理地址。

qemu内存虚拟化

host的内存物理内存是bios拼凑出来的,guest的物理内存是qemu用MemoryRegion拼凑出来的,guest物理内存也包含内存条内存和设备内存,只是guest内存条内存和设备内存都是由host的的内存虚拟出来的,guest访问内存条内存和设备内存触发kvm执行的动作是不一样的。

guest和host是独立的系统,两者都有自己的虚拟地址和物理地址,唯一的关系就是把guest的物理地址映射到host的虚拟地址,也就是qemu进程的虚拟地址。拿用的最多的EPT来说,guest有自己的页目录,kvm又维护了一个guest物理地址到host物理地址映射的页目录,cpu进入guest模式时一个虚拟地址要依次查找这两个页目录,guest查找自己的页目录,如果找不到就发生pagefault,处理pagefault,找到一个物理页,给页目录增加一项,访问物理页面发生EPT violation exit,kvm增加guest物理地址到host物理地址映射的页目录表项,重新enter guest继续执行。如果guest在自己页目录中找到,继续查找kvm维护的页目录,如果找不到发生EPT violation exit,kvm调用handle_ept_violation增加guest物理地址到host物理地址映射的页目录表项,然后重新enter guest继续执行,kvm的过程对于guest来说是透明的。如果guest内核回收一个page,删除一个自己的页目录表项,此时guest exit,因为kvm对guest页目录占用的内存做过特殊标记,kvm调用handle_invlpg删除自己的表项。

guest启动时是实模式,还没有页目录,没有MMU功能,早期guest实模式时由qemu来模拟,后来Intel CPU中加入了Unrestricted Guest,EPT开始支持实模式。

https://lists.gnu.org/archive/html/qemu-devel/2019-09/msg06225.html

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=3a624e29c7587b79abab60e279f9d1a62a3d4716

guest访问自己的设备内存,qemu和kvm对这些内存做了特殊标志,guest访问就触发EPT misconfig,然后kvm调用handle_ept_misconfig处理,根据地址范围找到属于的设备,然后调用设备模拟的代码,如果kvm搞不定退回qemu继续处理,kvm和qemu要做的事情就是把guest的物理地址转换成host的虚拟地址,然后读写转换后的虚拟地址,这样就等价于guest读写自己的外设内存了,处理完后,enter guest让guest继续运行。

如果物理CPU支持pae特性,比较新一点的linux guest和kvm会检测自动把pae利用起来。

总结

个人产生一些想法,努力去找答案,然后记录下来,刚开始都是一些问题,由一个问题想到更多问题,有些问题之间扯的很远,然后找答案,记录下来的都是问题和答案,努力加工整理成文章,感觉还是不成体系,理解不深而且未必正确。

0 人点赞