首先开宗明义:
在QEMU中,当processor进入VM环境,对基于EPT机制的内存虚拟化是完全支持的,当VM中的GuestOS在执行内存访问操作时,会通过EPT-page-structure将linear-address先转换成guest-physical-address,然后再将guest-physical-address转换成实际的物理地址physical-address;得到了最终的实际物理地址。
按Intel手册的规定,根据ept-page-table中表项的rwx属性,就能决定该物理地址是否能被access了,但是这个实际的物理地址是否能够被访问,最终还要受到guest-CR3(VM中的CR3)中存储的guest-physical-address实地址映射到相同的实地址(physical-address)中存储的目录表及其页表最终控制。
也就是说:Guest-CR3中目录表实地址映射的内存页也维护了一个目录表结构,这个目录表结构是用来管理EPT表中存储的所有实地址,这里将它定义为Guest-real-map-cr3;也就是说EPT表中每个不为空的叶子节点(ept-page-table的表项)中存储的实际物理地址,必须存在于Guest-real-map-cr3维护的目录表结构中,如果不在其中就不可访问。
第四节详细论述了为甚么QEMU要这么做,因为在VM(GuestOS)中,经过EPT-page-structure映射后的得到的实际物理内存页,不需要再经过MMU(host目录表)映射了,而是直接送往地址总线进行访存了,这样做的好处是提高了访存的效率,但对于VMM来说,它对整个物理内存的管控有一定的风险。
下面开启一周的痛苦的校验自省过程了:
这个问题困扰了我整整1周时间,因为当我在VM中想实现task-switch的时候通过手动还原和备份新老进程的context后,当执行到mov-to-cr3想讲目录表切换到新进程的4G地址空间后,程序总是报can not access memory at address xxxx。
通过EPT-page-structure逐个校验了新进程目录表对应的页表都是正确的,前256个目录项存储的值和老进程的前256项都是一样的,也就是新老进程是共享内核1G地址空间的,不应该报错了,processor会根据CR3中存储的guest-physical-address通过EPT-page-structure最终定位到实际的物理地址,然后利用线性地址的高10位定位到具体的页表,依次类推,最终能定位到程序要执行的代码或数据所在的实际物理页,完全没问题,但是就是报can not access memory at address xxxx。
1. guest-CR3实地址映射内存页,要是不初始化为映射1G内核空间的话,EPT映射得到的实际物理内存是无权访问的
必要的说明:进程的guest-CR3=0x1000 (guest-physical-address), 页表存储在guest地址空间的1M~5M之间,这样可以映射4G内存。

如上图1所示,cr3-guest-phy-addr(0x1000)对应的实际物理地址是红色圈住部分0x93f5000.
下面再看看0x93f5000中存储的是什么

如图2所示,该目录表中存储的页表的起始地址是从0x100000,1M开始的。
前1G地址空间是实地址映射的
我们以第四个目录项举例:0x00103007映射的是12M~16M地址空间,
因为这里存储的都是guest-phy-addr,所以要经过EPT转换成最终的实际物理地址:
由于guest-phy-addr是48位的,而我的是IA-32 32位的arch,所以其guest-phy-addr分割如下:
pml4 index pdpt index pd-index pt-index
000000000 000000000 000000000 100000011 000000000000
EPT 表的目录项是64位,所以addr=base+index*8

由图1可知pml4_base = 0x93fc000, pdpt_base=0x93fb000
上面说了pml4_index=pdpt_index=0x00,所以都是取得第一项,所以pd_base=0x93fa000
Pd_index=0x00,所以pt_base=0x93f6000
pt_index=100000011b,所以page_addr=pt_base+pt_index*8=0x93f6818=0x93f1000.
由图3可知,0x93f1000存储的就是实地址映射的page 12M~16M.
这时我们再看一下0x1000处存储的内容是什么:

如图4所示0x1000处存储的全为0
下面我们执行以下vmlaunch操作发现如下错误:

如图5所示:VM环境中CR3的值已经是0x1000了,EPT的映射也是好的,但是总是报
“can not access memory at address 0xc00000”.
按照Intel的规则和协议,通过EPT就可以映射到0xC00000这个实际物理地址了,图3里最后打印的就是对0xC0000的映射。
当我们把0x1000初始化为实地址映射1G地址空间后看看会发生什么,本系统的host 目录表存储在0x00处,页表是存储在实地址1M~5M之间的。
所以这了将0x1000处的目录表也初始化成页表在1M~5M空间,共享host OS的页表。

如图6所示: 0x1000被初始化为实地址映射内核1G地址空间了。
这时就能够顺利进入VM并执行GuestOS内核代码了。


如上图7和图8所示:成功进入GuestOS的bootloader和main函数了。
2. 将目录表(0x1000)管理的4M页表分配在16M~20M地址空间。
这样做的目的是:
因为GuestOS的目录表(guest-cr3=0x1000,phy-addr=0x93f5000)是存放在guest-phy-space的1M~5M地址空间的,如果将guest-cr3=0x1000实地址映射的目录表(phy-addr=0x1000)
管理的页表也映射到实地址1M~5M地址空间的话(这和guest-phy-space的1~5M不一样),总感觉qemu会不会通过透传机制将它们映射在一起。
所以这里将它们的页表分配在不同地址空间,进行校验。

如图9所示:0x1000实地址处的目录表管理的页表,都存放到16M~20M实地址空间了。

图10清楚的表明,CR3已经切换到了0x1000, 其对应的实地址所管理的页表是存放在0x1000000=16M开始处,CR3经过EPT转换后的实地址是0x93f5000,其管理的页表存放在0x100000 =1M开始处,由此可见CR3其对应的实地址和经EPT转换后的实地址是不存在1:1映射关系的。
3. 到这里是不是有点怀疑VM中到底有没有使用EPT进行内存虚拟化啊,因为guest-CR3对应的相同实地址处存储的可是一个完整的目录表啊,VM会不会直接就用它进行内存管理了。
这的确是个值得怀疑的地方。
当我把EPT-Violation handler中的打印开启后,发现当它访问GuestOS的第一条指令(startup_32)就会触发EPT-Violation,这也就是说GuestOS中的每一个访存指令肯定是通过EPT-page-structure转换后才能得到实际的物理内存进行访问的。


图11可以看出当执行第一条GuestOS指令发生在0xC00000处时,就产生EPT-violation触发vm-exit到VMM中进行EPT缺页管理。
到这里可以得出如下结论:
Guest-CR3中目录表实地址映射的内存页也维护了一个目录表结构,这个目录表结构是用来管理EPT表中存储的所有实地址,这里将它定义为Guest-real-map-cr3;也就是说EPT表中每个不为空的叶子节点(ept-page-table的表项)中存储的实际物理地址,必须存在于Guest-real-map-cr3维护的目录表结构中,如果不在其中就不可访问。
QEMU为什么要这么实现:这里推测应该是为了更好的对VM的占用的内存就行隔离和管理(其实对内存的访问控制进行了两次检查)。
尤其是当一个VM中创建了很多进程且都共享一个EPTP(相同的ept-page-structure),当要释放一个进程的时候在VM环境中,我们可以释放所有guest-phy-page,但是其对应的phy-page,是存储在EPT table中的,VM环境下无法释放,这就要vm-exit到VMM环境下释放该进程占用的ept-table表项及存储的实际物理页,这时要和CR3中管理的page进行check,防止释放了其他进程的表项。
这里还是需要QEMU的VMX模块开发者或看过这块代码实现的大佬确认下。
其实在以上操作之前还经历了一下痛苦校验和resolve的过程:
这里详细介绍为什么通过mov-to-cr3将当前cr3存储的目录表地址,改成新任务的目录表地址为什么不行,总是报can not access memory at address xxxxx.
实现的思路是这样的:
和非VMX环境下的进程切换一样,在fork一个新进程的时候,会为新进程分配一个自有的目录表,不过这个目录表的前256目录项和父进程相应的目录项的值是一样的都指向相同的页表,这样就共享了1G的内核地址空间了,这样做也节省了内存和不必要的内核页表copy,另外的1024-256个目录项用于管理进程的用户态地址空间共3G大小,这个是每个进程私有的,要按需同缺页分配了。
所以在GuestOS中也是想通过这种思路,来实现每个进程都有自己独立的4G地址空间(通过mov-to-cr3),并通过自己维护进程上相文来实现进程的切换,
但是每次运行完该指令后,GDB就报can not access memory at address xxxxx错误.
先后做了如下check:
1. 看看新任务的目录页和老任务的目录页是不是完全一样的,结果显示是一样的,按理cr3中存储的新任务的目录表(guest-phy-addr)通过EPT-page-structure
就可以映射到共享的内核页表了,可老是报can not access memory at address xxxxx错误,但是CR3的值已经是新任务的目录表了,已经切到新任务了。
当你通过ni调试指令继续执行下去的话,就是在新任务中执行mov-to-cr3后续的指令;为了验证任务是否是在VM中运行的,故意在mov-to-cr3指令后加了一条cpuid指令强制vm-exit到VMM中,也确实返回到VMM中了,这说明切换是成功了但有问题。
2. 继续看手册,详细研究了paging cache这块,突然发现ept-page-structure本身也有cache,mov-to-cr3仅仅刷新的是TLB而不是EPT cache,到这里真是如获至宝啊,终于找到问题所在了,所以赶紧刷新EPT-cache,通过invept,invvpid,将与当前VPID相关的EPT-page-structure全刷一遍,但结果给了我又一记闷棍,还是报同样错误,到底是哪出错了呢,继续想,折腾。
3. 为了验证是否与EPT-cache想关,在fork新进程的时候只分配了一个空目录表,没有将其初始化使其共享内核空间,这时竟然还是报相同错误,CR3已经是新任务的 目录表了,执行ni竟然还能继续执行随后的指令,尼玛这肯定是用的ept-cache里的老映射了,无疑了,激动啊,但仔细一想,我不是通过invept和invvpid都flush 过了吗,不可能还用老的map啊,迷惑了。
4. 突然灵光乍现,记得手册上说在VM中可以调用VMFUNC实现eptp-switching,难道不同进程的目录表要对应不同的eptp,通过这种方式实现进程拥有自己独立的4G地址空间,这就是为mov different-value to cr3,做准备的吧,又兴奋起来了,赶紧试一把。
在执行mov-to-cr3执行之前,先用VMfunc指令将当前eptp替换为新任务的eptp,然后tm还是相同的错误,要崩溃了,垃圾intel你这是要闹哪样。
5. 静下心,仔细复盘一下,突然想到了为什么在VM中不能执行task_swich指令,这条指令会强制VM-EXIT的,之前还吐槽为啥就不支持任务切换呢,
到这里我的理解是:
手册上说通过设置vm-execute control是可以执行mov-to-cr3和mov-from-cr3指令的,
但是执行mov-to-cr3我的理解是不改变CR3的值,这条指令仅仅是为了刷新TLB,因为任务切换后是一定要flush TLB的,压根就不支持chenge cr3 to different value.
6. 目前的方式1: 就是通过EPT更改Guest-CR3的实际目录页地址,以达到进程切换到自己的4G地址空间。
不过这种方式有个缺点,因为是共享相同的EPT-page-structure,这样每次进程切换就会flush EPT-cache,效率有一定损失。
7. 还有一种方式: 为每个进程分配自己的ept-page-structure,不改变Guest-CR3的值,通过vmfunc改变eptp从而能实现每个进程都有独立的4G地址空间,
目前的方式2: 就是通过这种方式实现的,但是这种方式的确定就是会占用更多的内存空间,但是每次任务切换由于每个任务都有自己独立的EPT所以EPT-cache中的内容是不需每次都flush的,这样效率更高.
4. 通过验证Guest-CR3中的实地址是否要经过host-CR3的映射来确认VM中Guest-CR3的实地址映射机制。
4.1 首先定义Guest-CR3=0x1000,在VMM(host model)中,将0x1000对应的物理内存初始化为0x00,将0x2000对应的物理内存初始化为页表起始地址为0x1000000处。

如图13所示,在VMM中,在还没有将0x1000重映射到0x2000之前,先初始化0x1000处内存页为0x00,0x2000处的内存页初始化为页表的起始地址为:0x1000000.
4.2 在VMM环境中,将0x1000内存页映射到0x2000内存页
这样在VMM环境中,当访问0x1000内存时,实际访问的0x2000内存页的内容,0x1000内存页实际上是访问不到了。

如图14所示:0x1000已经成功映射到0x2000了,这样在VMM环境下0x1000内存页建立了一个完整的目录表结构了。
4.3 验证VM环境下,当Guest-CR3=0x1000时,Guest-CR3实地址映射的0x1000处内存页是否需要经过VMM的目录表的再次映射。

如图15所示:当进入VM环境执行访存操作的时候,一直报can not access memory at address xxx。
图14中已经打印出0x1000处的物理内存页存储的全是0x00,所以Guest-CR3中存储的0x1000目录表内存页是不需要再经过VMM的目录表映射的,也就是不经过MMU直接送往地址总线进行访存了,因为对应的0x1000物理内存页全是0x00,所以内存不可访问。
如果要是用到了VMM中的目录页进行映射的话,那么实际访问的就是0x2000物理内存页,就不应该再报内存无法访问的错误了。
由此,可以进一步得出结论:
Guest-phy-addr经过EPT-page-structure映射后得到的phy-addr也是不需要经过VMM的目录表转换的,而是可以直接送往地址总线了,这也就是为什么QEMU会加一个额外的目录结构来管理EPT映射后的实际物理内存页,因为这些经过EPT映射后的得到的物理内存是不需要经过MMU(VMM目录表,物理内存页0x00地址处)映射,直接送往地址总线了,这样太危险了,但好处是不需要再经过MMU映射了,访存效率高啊。
不过还是那句话:
有的时候慢就是快!这段经历让我对EPT的机制有了彻底的了解,尤其对TLB cache和EPT-cache机制也有了通透的了解。
总之,终于可以在VM中愉快的进行进程调度了^_^