一、 相关信息:
1. VMCS包含一个只读域提供VM exit的信息。 (手册20.9)
Exit reason(32 bits)其中15:0为basic exit reason
2. VM Exit进程包括从Guest向VMM传输指令或者数据,进入到Root模式,在VMCS保存Guest状态
并且重新载入Guest状态
3. (手册25.7) 处理VM EXITs
首先使用VMREAD指令获取exit-reason(在VMCS里) VMRead exit-qualification提供辅助信息
根据exit reason获取其他相关的VMCS内的信息 处理VM-exit
重新进入vm继续执行
二、 DebugView中打印出的一段信息
以下共包含2次VM Exit的处理,分别对应vmlauch 和 CR_ACCESS 00010052 0.60838979 <98>: VmxDispatchEvent(): exitcode = 20 //vmlaunch 00010053 0.60839295 <98>: VmxHandleInterception(): Exitcode 20 00010054 0.60839611 <98>: VmxDispatchEvent(): exitcode = 1c //CR_ACCESS 00010055 0.60839927 <98>: VmxHandleInterception(): Exitcode 1c 00010056 0.60840195 <98>: VmxDispatchCrAccess() 00010057 0.60840583 <98>: VmxDispatchCrAccess(): gp: 0x2 cr: 0x3 exit_qualification: 0x203 00010058 0.60840923 <98>: VmxDispatchCrAccess(): TYPE_MOV_TO_CR cr3:0x3d09d000
三、 NBP对于VM Exit处理过程详解
1. 首先来看VM Exit引发后执行的函数是如何调用到的:
由于VM Exit会引发许多寄存器值刷新,这里主要关系到的是RIP和RSP。在VM Exit触发后,RIP和RSP会被VMCS中Host RIP和Host RSP域的值替换。 在Vmx.c中的VmxSetupVMCS中
VmxWrite (HOST_RIP, (ULONG64) VmxVmexitHandler);
这句的把VmxVmexitHandler的函数指针写到了HOST_RIP(0x00006c16)中,这样在VM Exit被触发并替换RIP的值后,就会自动执行VmxVmexitHandler这个方法。
另外VmxWrite (HOST_RSP, (ULONG64) Cpu);表示VM Exit触发后,RSP的值会指向Cpu结构体。所以在执行Trap过程的堆栈在内存里的位置应该是和Cpu结构体的位置相关的。同时在调用VmxVmexitHandler时的第一个参数PCPU Cpu,也就可以通过rsp的值来获得了。
2. VmxVmexitHandler(Vmx-asm.asm)
前期的准备是为调用HvmEventCallback而做的,由于HvmEventCallback的参数为:PCPU Cpu, PGUEST_REGS GuestRegs。在64位系统中,由调用规范,可以看到,Cpu指针被赋给了rcx,GuestRegs指针被赋给了rdx(这里我的理解是,VmxVmexitHandler首先执行了HVM_SAVE_ALL_NOSEGREGS, 此后rsp指向栈中保存的所有寄存器值的首地址,这样栈内的参数就等于自动与GuestRegs中的变量绑定,然后在trap方法中通过修改GuestRegs中相应变量的值,就相当于修改了栈中对应保存的寄存器的值,然后退出VmxVmexitHandler 之前会调用HVM_RESTORE_ALL_NOSEGREGS来恢复寄存器,此时用来恢复的值已经由于保存在栈中已经被修改,也就达到了对输出结果的修改。
sub rsp,28h,这句根据x64调用规范,为调用函数为被调用函数分配参数在栈中的空间,然后被调用函数会在调用时把参数放到之前保留的空间中。由于必须至少保留4个寄存器参数的空间,因此至少要空出20H的空间(经测试,改为20H也可跑通)。
执行完HvmEventCallback后,把之前分配的空间取消也就是add rsp, 28h。然后恢复寄存器(HVM_RESTORE_ALL_NOSEGREGS),然后通过vmx_resume把控制权交还给VM。
3. HvmEventCallback(Hvm.c)
if (Hvm->Architecture == ARCH_VMX) GuestRegs->rsp = VmxRead (GUEST_RSP);
RSP在这里被VmxRead方法赋值,不过这个方法只是与Intel架构相关,GUEST_RSP是指示存储在VMCS中的物理地址,从这里读到的值就是Guest的rsp的值。 //GUEST_RSP = 0x0000681c,
if (Hvm->ArchIsNestedEvent (Cpu, GuestRegs)) { …… }
这里的if语句在Intel环境下不会进入,由于没有实现嵌套。 因此关键的语句是Hvm->ArchDispatchEvent (Cpu, GuestRegs);
if (Hvm->Architecture == ARCH_VMX)
VmxWrite (GUEST_RSP, GuestRegs->rsp);
这两句与之前的read rsp的值对应。其实由于进入vmm,之前提到的保存寄存器的动作执行时,有些寄存器的值是已经被修改了的,比如rsp(此时已经是指向vmm的栈了),这时保存的值是不能和GuestRegs的rsp对应上的,也因此在做保存寄存器的值时,push的是rbp不是rsp:
注释为rsp,因为在GuestRegs中对应的位置放的应该是rsp值
对rsp的保存和恢复是必须通过VMCS中的GUEST_RSP域的赋值来实现的,这里通过GuestRegs->rsp = VmxRead (GUEST_RSP);来获取rsp的值写到GuestRegs的对应变量中。
4. VmxDispatchEvent (Vmx.c ArchDispatchEvent对应的intel架构函数指针)
向下函数调用VmxHandleInterception (Cpu, GuestRegs, FALSE /* this intercept will not be handled by guest hv */
);
5. VmxHandleInterception (Vmx.c)
Exitcode = VmxRead (VM_EXIT_REASON);
读取VMCS中的EXIT_REASON,Exitcode的值与含义的对应关系可在Vmx.h中找到,均有芯片固定提供。
Status = TrFindRegisteredTrap (Cpu, GuestRegs, Exitcode, &Trap);
在开始绑定的处理函数与Exit_reason的Trap的链表中找对应的Trap附给Trap变量,找到的话返回STATUS_SUCCESS,没找到返回STATUS_NOT_FOUND
执行TrExecuteGeneralTrapHandler。
6. TrExecuteGeneralTrapHandler(Traps.c)
if (Trap->TrapCallback (Cpu, GuestRegs, Trap, WillBeAlsoHandledByGuestHv)) { // trap handler wants us to adjust guest's RIP Hvm->ArchAdjustRip (Cpu, GuestRegs, Trap->General.RipDelta); }
Trap->TrapCallback (Cpu, GuestRegs, Trap, WillBeAlsoHandledByGuestHv)这句相当于调用了对应的处理函数。以CPUID为例,调用的就是VmxDispatchCpuid(VmxTraps.c)。 Hvm->ArchAdjustRip (Cpu, GuestRegs, Trap->General.RipDelta);
内部执行的是VmxWrite (GUEST_RIP, VmxRead (GUEST_RIP) + Delta);
可以理解为由于对某条指令执行了trap,在把控制权返还VM时,需要告诉VM跳过trap的那条指令。
四、 添加Trap
Hvm.c的Status = Hvm->ArchRegisterTraps (Cpu); 会调用到Vmxtraps.c中的VmxRegisterTraps方法
这个方法初始化了目前虚拟机trap的相关内容,对每一种trap调用
TrInitializeGeneralTrap或TrInitializeMsrTrap(intel部分未使用)或TrInitializeIoTrap(均未使用) 来绑定相应的处理函数和属性,再调用TrRegisterTrap把初始完的一个trap添加到对应的链表里。
简单地说就是通过上图的类似方法添加即可。
目前看来,这样的添加只对某些开启的会引发VM Exit的一些指令相关(如何开启时通过在VMCS相应位
置设定值来定义的)。
因篇幅问题不能全部显示,请点此查看更多更全内容