intel:x86架构VT虚拟化(二):核心代码入门介绍
上次介绍了VT的基本原理和核心流程,今天细说VT的关键代码。核心代码的git地址:https://github.com/zzhouhe/VT_Learn ;这是一个miniVT框架,实现了最基本的VT框架功能,非常适合初学入门。
VT的基本流程如下,下面就按照这个流程细说关键代码;
- 检查是否支持VT
(1)CPUID:CPUID指令是intel IA32架构下获得CPU信息的汇编指令,可以得到CPU类型,型号,制造商信息,商标信息,序列号,缓存等一系列CPU相关的东西;执行后返回结果保存在;这个指令3环都能执行,本人物理机执行结果如下:RCX是0x64=0110 0100,VMX=1,说明是支持VT的;
(2)检查CR0和CR4是否开启了段保护和页保护,否则是没法开启VT的;
(3)检查CR4的VMXE是否为1;如果是,说明已经开启VT(现在很多杀软、游戏驱动保护的都用了VT,在-1环做各种保护),这时已经处于别人的监控之中,建议关闭后再开启,收回主动权
(4)检查BIOS主板是否锁定了VT指令,这个一般都没有;
完整代码如下:
BOOLEAN IsVTEnabled() { ULONG uRet_EAX, uRet_ECX, uRet_EDX, uRet_EBX; _CPUID_ECX uCPUID; _CR0 uCr0; uCr4; IA32_FEATURE_CONTROL_MSR msr; //1. CPUID Asm_CPUID(1, &uRet_EAX, &uRet_EBX, &uRet_ECX, &uRet_EDX); *((PULONG)&uCPUID) = uRet_ECX; if (uCPUID.VMX != 1) { Log("ERROR: 这个CPU不支持VT!",0); return FALSE; } // 2. CR0 CR4 *((PULONG)&uCr0) = Asm_GetCr0(); *((PULONG)&uCr4) = Asm_GetCr4(); if (uCr0.PE != 1 //开启段保护模式 || uCr0.PG!=1 //开启页保护模式;这两个必须都是1,VMXON才会成功。并且直到VMXOFF,这两个都必须是1; || uCr0.NE!=1) { Log("ERROR:这个CPU没有开启VT!",0); return FALSE; } if (uCr4.VMXE == 1)//防止嵌套,被别的应用牵着鼻子走;所以初期要求为0,后续自己手工设置成1; { Log("ERROR:这个CPU已经开启了VT!",0); Log("可能是别的驱动已经占用了VT,你必须关闭它后才能开启。",0); return FALSE; } // 3. MSR *((PULONG)&msr) = (ULONG)Asm_ReadMsr(MSR_IA32_FEATURE_CONTROL); if (msr.Lock!=1)//bios主板的那个设置 { Log("ERROR:VT指令未被锁定!",0); return FALSE; } Log("SUCCESS:这个CPU支持VT!",0); return TRUE; }
- 启动虚拟机
这个简单,直接一条VMXON即可;不过要注意:需要事先分配4KB的物理空间供host CPU记录一些信息。这4KB怎么维护就是CPU自己的事了,开发人员不用管;核心代码如下:
Vmx_VmxOn(g_VMXCPU.pVMXONRegion_PA.LowPart, g_VMXCPU.pVMXONRegion_PA.HighPart); *((PULONG)&uEflags) = Asm_GetEflags(); if (uEflags.CF != 0) { Log("ERROR:VMXON指令调用失败!",0); return; } Log("SUCCESS:VMXON指令调用成功!",0);
- vmclear和vmprtload
清空和加载VMCS块,用intel提供现成的vmclear和vmprtload、带上分配的VMCS内存块即可,代码如下:
Vmx_VmClear(g_VMXCPU.pVMCSRegion_PA.LowPart, g_VMXCPU.pVMCSRegion_PA.HighPart); *((PULONG)&uEflags) = Asm_GetEflags(); if (uEflags.CF != 0 || uEflags.ZF != 0) { Log("ERROR:VMCLEAR指令调用失败!",0) return; } Log("SUCCESS:VMCLEAR指令调用成功!",0) Vmx_VmPtrld(g_VMXCPU.pVMCSRegion_PA.LowPart, g_VMXCPU.pVMCSRegion_PA.HighPart);
- 初始化VMCS:最重要的设置
1、先看看运行时的控制execution control field:
(1)pin-based vm执行控制域:主要各种外部中断处理的控制,最重要的是第0位的设置:如果为1,那么外部的中断会触发exit,从guest退回到host;
从MSR寄存器看,默认是0,说明guest自己处理外部中断,不用退回host;
设置的代码:
// 3.虚拟机运行控制域 //guest一旦发生CR3读写,必须exit到host;后续写host代码,必须处理guest读写CR3的事件; //pin-base(针脚硬件)中断,host要不要先处理? 要不要拦截CR4和CR0? Vmx_VmWrite(PIN_BASED_VM_EXEC_CONTROL, VmxAdjustControls(0, MSR_IA32_VMX_PINBASED_CTLS));
(2)cpu-based vm执行控制域:必须设置成1的位
查手册得知:读取和加载CR3会触发vm exit事件,这就要求exit处理的函数必须要考虑CR3的读写了;
还有另外一个比较重要的位:一旦设置为1,guest所有对DR寄存器的设置都会触发vm exit,这时可以用来调试和反调试的;
代码如下:
//读写msr的时候要不要退出到host?要不要拦截IO? Vmx_VmWrite(CPU_BASED_VM_EXEC_CONTROL, VmxAdjustControls(0, MSR_IA32_VMX_PROCBASED_CTLS));
(3)VMEntry和VMExit控制设置:同上,先读出对应MSR寄存器的值,再把高32bit清零,保留低32bit,也就是把intel规定的必须设置为1的位设置成1,其他的位开发人员自行操作;
intel手册的部分说明如下:
// 4.VMEntry运行控制域 //guest推出到host的时候,要不要保存dr7和msr的debug寄存器? Vmx_VmWrite(VM_ENTRY_CONTROLS, VmxAdjustControls(0, MSR_IA32_VMX_ENTRY_CTLS)); // 5.VMExit运行控制域 Vmx_VmWrite(VM_EXIT_CONTROLS, VmxAdjustControls(0, MSR_IA32_VMX_EXIT_CTLS));
2、vmm/host宿主机设置:物理CPU要么执行guestOS的执行,要么执行hotsOS的指令。一旦执行VMXON,cpu便开始执行hostOS的指令;既然是执行OS的指令,自然少不了和指令相关的各类段寄存器/描述符、控制寄存器、GDT、IDT等,这些寄存器、各类表的值该怎么设置了?
- 写过OS底层代码的人都知道,开机上电后cpu处于实模式,bios会从内存的0x007c处开始执行。这时会从磁盘加载os代码,然后设置各种段寄存器的值,再转到保护模式。这里既然是host os,当然也可以这么干,不过这样做有两个问题:(1)现在有guestOS正在运行,擅自更改各种寄存器、GDT/IDT的基址,后续vmresume切回guestOS后还要想办法回复。这一来二去的麻烦,耗时; (2)这么多寄存器,还有GDT/IDT表,如果都自己设置,不麻烦么?尤其是GDT/IDT,还要新开辟内存,构造描述符;IDT表还要构造对应的中断处理代码,能行么? 有必要么?
- 所以这里偷个懒,这些关键寄存器(EIP和ESP除外)、GDT/IDT表设置成和当前运行guestOS一样,hostOS运行时遇到中断处理方法和guestOS一样(处理代码都在物理内存,双方很容易共享的);
- EIP和ESP为什么要单独设置?guestOS通过vm exit或vmcall退回到host,请求权限更高的上一级帮忙处理。这就涉及到个性化的处理方法了,所以EIP就是这些代码的入口地址; 既然是函数调用,自然少不了参数、局部变量和返回地址,这些都需要栈来保存,自然也要单独分配一个栈空间了;
核心代码如下:(1)因为代码在0环,各个段选择子的CPL是00,那么要求RPL也是00,所以要都要&0xFFF8,把最后3位清零0
Vmx_VmWrite(HOST_CR0, Asm_GetCr0()); Vmx_VmWrite(HOST_CR3, Asm_GetCr3()); Vmx_VmWrite(HOST_CR4, Asm_GetCr4()); Vmx_VmWrite(HOST_ES_SELECTOR, Asm_GetEs() & 0xFFF8); Vmx_VmWrite(HOST_CS_SELECTOR, Asm_GetCs() & 0xFFF8); Vmx_VmWrite(HOST_DS_SELECTOR, Asm_GetDs() & 0xFFF8); Vmx_VmWrite(HOST_FS_SELECTOR, Asm_GetFs() & 0xFFF8); Vmx_VmWrite(HOST_GS_SELECTOR, Asm_GetGs() & 0xFFF8); Vmx_VmWrite(HOST_SS_SELECTOR, Asm_GetSs() & 0xFFF8); Vmx_VmWrite(HOST_TR_SELECTOR, Asm_GetTr() & 0xFFF8); Vmx_VmWrite(HOST_TR_BASE, 0x80042000); Vmx_VmWrite(HOST_GDTR_BASE, GdtBase); Vmx_VmWrite(HOST_IDTR_BASE, IdtBase); Vmx_VmWrite(HOST_IA32_SYSENTER_CS, Asm_ReadMsr(MSR_IA32_SYSENTER_CS)&0xFFFFFFFF); Vmx_VmWrite(HOST_IA32_SYSENTER_ESP, Asm_ReadMsr(MSR_IA32_SYSENTER_ESP)&0xFFFFFFFF); Vmx_VmWrite(HOST_IA32_SYSENTER_EIP, Asm_ReadMsr(MSR_IA32_SYSENTER_EIP)&0xFFFFFFFF); // KiFastCallEntry,这里也直接简单粗暴”借用“guestOS的系统调用
/*
为什么要单独给栈分配空间?这里的代码还在驱动里面,驱动的entry一旦执行完,线程可能释放,堆栈也就没了,所以host最好单独开辟
一块内存作为栈使用;另外,host和guest的栈肯定也是要分开的,类似从3环提权进0环,栈都不会用同一个;
pStack是栈顶,pStack是栈顶+0x2000是栈底;
*/
Vmx_VmWrite(HOST_RSP, ((ULONG)g_VMXCPU.pStack) + 0x2000); //Host 临时栈 Vmx_VmWrite(HOST_RIP, (ULONG)VMMEntryPoint); //这里定义我们的VMM处理程序入口,相当于回调
3、guest state area:客户机状态设置
guestOS一旦产生exit事件,或主动调用vmcall,便会回退到host指定的函数处理这些事件。处理完后host会执行vmresume回到guest继续执行,这时该到guest的那里执行了?寄存器、GDT/IDT这些周边环境的上下文怎么恢复了?这里就需要挨个保存了;具体保存的环境信息如下:
从上面的要求可以看出:不但要求保存选择子可见的16位,剩余不可见的80位也要保存,这就麻烦了。作者偷了个懒,直接先把这些字段设置成不可用,后续在entry时再通过mov ax,cs;mov cs,ax的方式刷新选择子不可见的80位;
Vmx_VmWrite(GUEST_CR0, Asm_GetCr0()); Vmx_VmWrite(GUEST_CR3, Asm_GetCr3()); Vmx_VmWrite(GUEST_CR4, Asm_GetCr4()); Vmx_VmWrite(GUEST_DR7, 0x400); Vmx_VmWrite(GUEST_RFLAGS, Asm_GetEflags() & ~0x200); Vmx_VmWrite(GUEST_ES_SELECTOR, Asm_GetEs() & 0xFFF8); Vmx_VmWrite(GUEST_CS_SELECTOR, Asm_GetCs() & 0xFFF8); Vmx_VmWrite(GUEST_DS_SELECTOR, Asm_GetDs() & 0xFFF8); Vmx_VmWrite(GUEST_FS_SELECTOR, Asm_GetFs() & 0xFFF8); Vmx_VmWrite(GUEST_GS_SELECTOR, Asm_GetGs() & 0xFFF8); Vmx_VmWrite(GUEST_SS_SELECTOR, Asm_GetSs() & 0xFFF8); Vmx_VmWrite(GUEST_TR_SELECTOR, Asm_GetTr() & 0xFFF8); Vmx_VmWrite(GUEST_ES_AR_BYTES, 0x10000);//段选择子隐藏部分的属性attribute设置成不可用,避免cpu自行加载各种属性后导致出错;后续进入GuestEntry手动刷新获取 Vmx_VmWrite(GUEST_FS_AR_BYTES, 0x10000); Vmx_VmWrite(GUEST_DS_AR_BYTES, 0x10000); Vmx_VmWrite(GUEST_SS_AR_BYTES, 0x10000); Vmx_VmWrite(GUEST_GS_AR_BYTES, 0x10000); Vmx_VmWrite(GUEST_LDTR_AR_BYTES, 0x10000); Vmx_VmWrite(GUEST_CS_AR_BYTES, 0xc09b);//CS和TR不能像前面一样设置成不可用,然后进入GuestEntry刷新获取 Vmx_VmWrite(GUEST_CS_BASE, 0); Vmx_VmWrite(GUEST_CS_LIMIT, 0xffffffff); Vmx_VmWrite(GUEST_TR_AR_BYTES, 0x008b); Vmx_VmWrite(GUEST_TR_BASE, 0x80042000); Vmx_VmWrite(GUEST_TR_LIMIT, 0x20ab); Vmx_VmWrite(GUEST_GDTR_BASE, GdtBase); Vmx_VmWrite(GUEST_GDTR_LIMIT, Asm_GetGdtLimit()); Vmx_VmWrite(GUEST_IDTR_BASE, IdtBase); Vmx_VmWrite(GUEST_IDTR_LIMIT, Asm_GetIdtLimit()); Vmx_VmWrite(GUEST_IA32_DEBUGCTL, Asm_ReadMsr(MSR_IA32_DEBUGCTL)&0xFFFFFFFF); Vmx_VmWrite(GUEST_IA32_DEBUGCTL_HIGH, Asm_ReadMsr(MSR_IA32_DEBUGCTL)>>32); Vmx_VmWrite(GUEST_SYSENTER_CS, Asm_ReadMsr(MSR_IA32_SYSENTER_CS)&0xFFFFFFFF); Vmx_VmWrite(GUEST_SYSENTER_ESP, Asm_ReadMsr(MSR_IA32_SYSENTER_ESP)&0xFFFFFFFF); Vmx_VmWrite(GUEST_SYSENTER_EIP, Asm_ReadMsr(MSR_IA32_SYSENTER_EIP)&0xFFFFFFFF); // KiFastCallEntry,客户机系统调用的入口 Vmx_VmWrite(GUEST_RSP, ((ULONG)g_VMXCPU.pStack) + 0x1000); //Guest 临时栈 Vmx_VmWrite(GUEST_RIP, (ULONG)GuestEntry); // 客户机的入口点 Vmx_VmWrite(VMCS_LINK_POINTER, 0xffffffff); Vmx_VmWrite(VMCS_LINK_POINTER_HIGH, 0xffffffff);
以上便是VMCS结构体的设置。注意:这里用intel提供的vmwrite和vmread读写,自己用memset等函数时不行的;
- vmlaunch
执行后从hostOS进入guestOS。guest执行什么代码了?对于绝大部分开发人员来说,VT都是用来获取-1权限、达到调试和反调试目的。这种情况下就尽快让自定义的guest代码执行完毕,然后由guestOS拿到vCPU继续执行。所以guest代码如下:guest函数最后一句跳转到g_exit函数执行,但这个函数啥都没有,此时直接由原guestOS继续执行(以前该干啥,现在接着干,尽量不打扰);
这里用裸函数,避免了编译器擅自添加push ebp,mov ebp,esp, sub esp, xxxh等行为破坏堆栈平衡、改动esp等重要寄存器的值!
如果我们在vmware做测试,那么里面的os就是guestOS,vmware相当于物理机,我们自己写的VMMEntryPoint就是hostOS的代码入口,这个关系一定要捋清楚,否则后续很多代码逻辑是想不通的!
void g_exit(void); void __declspec(naked) GuestEntry() { __asm{ mov eax, cr3 mov cr3, eax mov ax, es mov es, ax mov ax, ds mov ds, ax mov ax, fs mov fs, ax mov ax, gs mov gs, ax mov ax, ss mov ss, ax } __asm{ jmp g_exit } }
- VMexit/Vmcall
guestOS在运行过程中可能会主动调用vmcall或 “无意间” 触发vmexit,退回到hostOS处理这些“异常”事件。hostOS一般需要根据不同的“异常”事件类型采取不同的动作。guestOS的哪些“异常”操作会触发vmexit,直接关系到hostOS需要接管和处理哪些“异常”,那么这些“异常”事件都是在哪定义的了?--- 同样是在VMCS结构里面,cpu-based vm excution control field能查到!
host的入口函数:
(1)同样用裸函数,避免栈平衡被破坏、esp等重要寄存器的值被篡改;
(2)VMCS结构保存了段选择子和其他部分重要信息,通用寄存器却没保存,先在内存保存好这些通用寄存器的值,hostOS处理完这些异常resume到guest时才好恢复。整个过程像不像线程切换?
(3)先在终于可以愉快地在hostOS处理guest的异常了,真正的处理函数是VMMEntryPointEbd;
void __declspec(naked) VMMEntryPoint(void) { __asm{ mov g_GuestRegs.eax, eax mov g_GuestRegs.ecx, ecx mov g_GuestRegs.edx, edx mov g_GuestRegs.ebx, ebx mov g_GuestRegs.esp, esp mov g_GuestRegs.ebp, ebp mov g_GuestRegs.esi, esi mov g_GuestRegs.edi, edi pushfd pop eax mov g_GuestRegs.eflags, eax mov ax, fs mov fs, ax mov ax, gs mov gs, ax } VMMEntryPointEbd(); __asm{ mov eax, g_GuestRegs.eax mov ecx, g_GuestRegs.ecx mov edx, g_GuestRegs.edx mov ebx, g_GuestRegs.ebx mov esp, g_GuestRegs.esp mov ebp, g_GuestRegs.ebp mov esi, g_GuestRegs.esi mov edi, g_GuestRegs.edi //vmresume __emit 0x0f __emit 0x01 __emit 0xc3 } }
退出处理函数的逻辑如下:从VMCS结构体中读取guestOS退出原因、产生退出那条指令的长度、退出时刻重要寄存器的值,紧接着根据退出原因分别处理;末尾处把新的EIP(原EIP+异常指令长度)、ESP、eflags寄存器重新写回VMCS,resume的时候CPU会根据这个结构体的信息接着运行;
个人观点:对于逆向、安全攻防人员而言,这个方法是最核心和重要的。前面做了大量的工作,就是为了让guestOS执行的时候出各种“异常”,好由hostOS接管,开发人员就可以通过这个entryPoit“为所欲为”了,比如通过改寄存器或内存的值达到hook的目的;或则把页面的读和写分开,达到无痕挂钩(shadow walker)的目的等等;
static void VMMEntryPointEbd(void) { ULONG ExitReason; ULONG ExitInstructionLength; ULONG GuestResumeEIP; ExitReason = Vmx_VmRead(VM_EXIT_REASON);//guestOS退出的原因编号 ExitInstructionLength = Vmx_VmRead(VM_EXIT_INSTRUCTION_LEN);//产生退出那条指令的长度 g_GuestRegs.eflags = Vmx_VmRead(GUEST_RFLAGS);//退出时各大重要寄存器的值 g_GuestRegs.esp = Vmx_VmRead(GUEST_RSP); g_GuestRegs.eip = Vmx_VmRead(GUEST_RIP); g_GuestRegs.cr3 = Vmx_VmRead(GUEST_CR3); switch(ExitReason) { case EXIT_REASON_CPUID: HandleCPUID(); //Log("EXIT_REASON_CPUID", 0) break; case EXIT_REASON_VMCALL: HandleVmCall(); //Log("EXIT_REASON_VMCALL", 0) break; case EXIT_REASON_CR_ACCESS: HandleCrAccess(); //Log("EXIT_REASON_CR_ACCESS", 0) break; case EXIT_EPT_VIOLATION: *test_data = 0x5678; *hook_ept_pt = ((hook_pa.LowPart & 0xFFFFF000) | 0x37); break; default: __asm int 3 break; } //Resume: GuestResumeEIP = g_GuestRegs.eip + ExitInstructionLength; Vmx_VmWrite(GUEST_RIP, GuestResumeEIP); Vmx_VmWrite(GUEST_RSP, g_GuestRegs.esp); Vmx_VmWrite(GUEST_RFLAGS, g_GuestRegs.eflags); }
- EPT:extend page table
上面所有的功能都可以看成是计算虚拟化,本质上就是让物理CPU在hostOS和guestOS之间来回切换执行代码。hostOS权限最高,可以监控和干预guestOS的执行;除了计算虚拟化,还有一块很重要的就是“内存虚拟化”,也就是EPT:extend page table;
在没有虚拟化的时候,3环的exe或0环驱动通过memalloc、exallocatepage等函数分配的地址都是虚拟地址,需要通过页表转成物理地址才能完成最终的读写;OS需要做2件事:(1)生成并维护页表 (页表每项直接都是物理地址,在os进入保护模式前就要在内存设置好) (2)页表基址写入CR3; 至于代码执行时虚拟地址转换成物理地址的过程全程由CPU负责,不需要开发人员额外提供啥;
引入虚拟化后,guestOS的虚拟地址最重要转换成hostOS的物理地址才能顺利地读写数据,这个过程是怎么实现的了?guestOS的虚拟地址转成guestOS的物理地址方式不变(否则市面上现有主流OS做虚拟机OS时都要重新适配地址转换规则,改动太大,伤筋动骨,兼容性一点都不好),guestOS的物理地址(简称GPA)是怎么转成hostOS的物理地址(简称HPA)的了?
hostOS也需要建立并维护一个多级表,每次guestOS需要转换物理地址的时候,都需要在这个多级表中逐级查询。举个栗子:GPA中的PML4E index要转成HPA,需要经过下面PML4E\PDPTE\PDE\PTE一共4级才能最终达到HPA;GPA其他诸如PDPTE\PDE\PTE等也要通过这种转换才能读写HPA,所以guestOS一个虚拟地址转成HPA,一共需要4*4=16次转换,这时虚拟机效率打折的重要原因之一;
EPT表建立的关键代码如下;注意:虚拟机内存大小不同,4级表内的entry数量也不同,这份代码不能直接简单粗暴复制;
void initEptPagesPool() { pagesToFree = ExAllocatePoolWithTag(NonPagedPool, 12*1024*1024, 'ept'); if(!pagesToFree) __asm int 3 RtlZeroMemory(pagesToFree, 12*1024*1024); } static ULONG64* AllocateOnePage() { PVOID page; page = ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'ept'); if(!page) __asm int 3 RtlZeroMemory(page, PAGE_SIZE); pagesToFree[index] = page; index++; return (ULONG64 *)page; } extern PULONG test_data; PHYSICAL_ADDRESS hook_pa; ULONG64 *hook_ept_pt; /*自己维护从PML4E到PA的转换表*/ ULONG64* MyEptInitialization() { ULONG64 *ept_PDPT, *ept_PDT, *ept_PT; PHYSICAL_ADDRESS FirstPtePA, FirstPdePA, FirstPdptePA; int a, b, c; hook_pa = MmGetPhysicalAddress(test_data); initEptPagesPool(); ept_PML4T = AllocateOnePage(); ept_PDPT = AllocateOnePage(); FirstPdptePA = MmGetPhysicalAddress(ept_PDPT); *ept_PML4T = (FirstPdptePA.QuadPart) + 7;//最后12位是属性,7=0111,表示可读可写可执行;类似linux下chmod 777; for (a = 0; a < 4; a++) { ept_PDT = AllocateOnePage(); FirstPdePA = MmGetPhysicalAddress(ept_PDT); *ept_PDPT = (FirstPdePA.QuadPart) + 7; ept_PDPT++; for (b = 0; b < 512; b++) { ept_PT = AllocateOnePage(); FirstPtePA = MmGetPhysicalAddress(ept_PT); *ept_PDT = (FirstPtePA.QuadPart) + 7; ept_PDT++; for (c = 0; c < 512; c++) { *ept_PT = ((a << 30) | (b << 21) | (c << 12) | 0x37) & 0xFFFFFFFF;// 0x37:可读可写可执行,并且有缓存,write-back; if ((((a << 30) | (b << 21) | (c << 12) | 0x37) & 0xFFFFF000) == (hook_pa.LowPart & 0xFFFFF000)) { *ept_PT = 0; hook_ept_pt = ept_PT; } ept_PT++; } } } return ept_PML4T; }
自己建立EPT表后,还要在VMCS结构里面”注册“,代码如下:
//自己维护的地址转换表,在VMCS中注册一下; Vmx_VmWrite(EPT_POINTER, (EPTP | 6 | (3 << 3)) & 0xFFFFFFFF); Vmx_VmWrite(EPT_POINTER_HIGH, (EPTP | 6 | (3 << 3)) >> 32); Vmx_VmWrite(EPT_POINTER_HIGH, EPTP >> 32); Vmx_VmWrite(SECONDARY_VM_EXEC_CONTROL, VmxAdjustControls(0x2, MSR_IA32_VMX_PROCBASED_CTLS2)); //for EPT with PAE; /* 29912:前面2bit对应4项,这4项写入VMCS;注意:c0600000是CR3的PDE入口,是虚拟地址,要转换成物理地址; */ Vmx_VmWrite(GUEST_PDPTR0, MmGetPhysicalAddress((PVOID)0xc0600000).LowPart | 1); Vmx_VmWrite(GUEST_PDPTR0_HIGH, MmGetPhysicalAddress((PVOID)0xc0600000).HighPart); Vmx_VmWrite(GUEST_PDPTR1, MmGetPhysicalAddress((PVOID)0xc0601000).LowPart | 1); Vmx_VmWrite(GUEST_PDPTR1_HIGH, MmGetPhysicalAddress((PVOID)0xc0601000).HighPart); Vmx_VmWrite(GUEST_PDPTR2, MmGetPhysicalAddress((PVOID)0xc0602000).LowPart | 1); Vmx_VmWrite(GUEST_PDPTR2_HIGH, MmGetPhysicalAddress((PVOID)0xc0602000).HighPart); Vmx_VmWrite(GUEST_PDPTR3, MmGetPhysicalAddress((PVOID)0xc0603000).LowPart | 1); Vmx_VmWrite(GUEST_PDPTR3_HIGH, MmGetPhysicalAddress((PVOID)0xc0603000).HighPart);
还有个问题随之而来:为什么需要EPT转换一下?为什么不让GPA直接等于HPA? 这个和guestOS内部的分页原理是一样的。OS内部多进程同时运行,进程都有自己的4GB虚拟地址空间。每个进程都可以直接用低2GB的空间,而不同担心和其他进程冲突;核心就是有页转换;在不同进程中,即使有同样的虚拟地址,但进程之间的页表是不同的,隐射到的物理地址自然也不一样;一个物理机可以同时运行多个虚拟机,虚拟机之间可以有相同的GPA,但是经过hostOS的EPT转换后得到不同的HPA,完美规避了不同虚拟机GPA一样的冲突和尴尬!
以上便是VT最简单框架的核心代码,理解起来其实并不难: 物理CPU在host和guest之间的切换可以和线程切换类比,EPT的地址转换可以和CR3分页类比!
参考:1、https://space.bilibili.com/37877654/channel/detail?cid=70349 miniVT框架