Virtualbox源码分析4:VMM虚拟化框架实现源码分析
Intel和AMD都有自己VMM框架, Intel的叫做VMX, AMD的叫做SVM,两个实现原理类似,只是CPU指令,VMCS结构体不同,所以虚拟化软件需要同时支持VMX和SVM两套VMM。
VMX实现代码在VMM\VMMR0\HMVMXR0.cpp
SVM实现代码在VMM\VMMR0\HMSVMR0.cpp
本文已VMX为举例讲解:
4.1 VMX原理
Intel手册Vol 3C第23章到第33章详细讲解了VMX的原理,有兴趣可以下载下来看,下面先简单介绍一些重要的概念。
4.1.1 VMX的状态转化:
VMX有两种状态: root状态和non-root状态
其中几个重要的指令:
VMXON: 进入VMX root模式
VMXOFF:退出VMX root模式
VMExit:non-root模式进入到root模式 。这个可以由Guest主动调用,也可以是一些异常导致CPU主动触发。
VMEntry: root模式进入non-roo模式,即CPU开始运行Guest OS代码。
如果大家熟悉windows系统调用可以吧root模式理解成操作系统内核,non-root模式理解成操作系统应用层,vmexit理解成syscall或者异常处理,VMEntry理解成sysexit。
4.1.2 VMCS
在虚拟化中,为了实现vCPU,既要模拟CPU的运行,又要记录vCPU的状态(包括对vCPU运行的控制信息), vCPU的状态,就保护到一个叫做VMCS到结构体里,具体描述可以参考Intel手册24.3 ORGANIZATION OF VMCS DATA
VMM给每个vCPU都分配了一个VMCS结构体,每个VMCS都有三个状态(Launched,clear和active状态),每个实体CPU上只能有一个VMCS出于active/launched状态
下面是Virtual box里的定义:
hm_vmx.h
/** VMCS launch state clear. */
#define VMX_V_VMCS_LAUNCH_STATE_CLEAR RT_BIT(0)
/** VMCS launch state active. */
#define VMX_V_VMCS_LAUNCH_STATE_ACTIVE RT_BIT(1)
/** VMCS launch state current. */
#define VMX_V_VMCS_LAUNCH_STATE_CURRENT RT_BIT(2)
/** VMCS launch state launched. */
#define VMX_V_VMCS_LAUNCH_STATE_LAUNCHED RT_BIT(3)
下图是3个状态之间的转换关系
VMClEAR:传入一个物理地址,CPU会把当前VMCS里的数据拷贝到传入的物理地址中,并吧当前CPU的VMCS状态设置成clear状态
VMPTRLD(ACTIVE): 传入一个物理地址, 把当前CPU的VMCS指针指向这个物理地址
VMLAUNCH: 当VMM设置好VMCS之后,就可以执行VMLAUNCH让CPU切换到GuestOS状态,运行GuestOS里的代码。
在下一章的VMXR0SetupVM这个函数的解析里,可以看到使用代码是如何设置一个VMCS内容的。
VirtualBox里对VMCS对定义代码子啊HMInternal.h里
typedef struct VMXVMCSINFO
{
/** @name Auxiliary information.
* @{ */
/** Ring-0 pointer to the hardware-assisted VMX execution function. */
PFNHMVMXSTARTVM pfnStartVM;
/** Host-physical address of the EPTP. */
RTHCPHYS HCPhysEPTP; 这个地方保存了EPT Table的指针
/** The VMCS launch state, see VMX_V_VMCS_LAUNCH_STATE_XXX. */
uint32_t fVmcsState; 保存上面介绍的vmcs状态
/** The VMCS launch state of the shadow VMCS, see VMX_V_VMCS_LAUNCH_STATE_XXX. */
uint32_t fShadowVmcsState;
/** The host CPU for which its state has been exported to this VMCS. */
RTCPUID idHostCpuState; 主机物理CPU的状态信息
/** The host CPU on which we last executed this VMCS. */
RTCPUID idHostCpuExec;
/** Number of guest MSRs in the VM-entry MSR-load area. */
uint32_t cEntryMsrLoad;
/** Number of guest MSRs in the VM-exit MSR-store area. */
uint32_t cExitMsrStore;
/** Number of host MSRs in the VM-exit MSR-load area. */
uint32_t cExitMsrLoad;
/** @} */
/** @name Cache of execution related VMCS fields. 这些区域是控制Guest OS行为
* @{ */
/** Pin-based VM-execution controls. */
uint32_t u32PinCtls;
/** Processor-based VM-execution controls. */
uint32_t u32ProcCtls; 设置VMExit事件
/** Secondary processor-based VM-execution controls. */
uint32_t u32ProcCtls2; 也是设置VMExit事件
/** VM-entry controls. */
uint32_t u32EntryCtls; 对VM Entry的行为进行控制
/** VM-exit controls. */
uint32_t u32ExitCtls; 这个地方可以设置VMExit事件
/** Exception bitmap. */
uint32_t u32XcptBitmap; 设置异常事件的bitmap
/** Page-fault exception error-code mask. */
uint32_t u32XcptPFMask;
/** Page-fault exception error-code match. */
uint32_t u32XcptPFMatch;
/** Padding. */
uint32_t u32Alignment0;
/** TSC offset. */
uint64_t u64TscOffset;
/** VMCS link pointer. */
uint64_t u64VmcsLinkPtr;
/** CR0 guest/host mask. */
uint64_t u64Cr0Mask;
/** CR4 guest/host mask. */
uint64_t u64Cr4Mask;
/** @} */
/** @name Host-virtual address of VMCS and related data structures.
* @{ */
/** The VMCS. */
R0PTRTYPE(void *) pvVmcs; 当前VMCShost物理地址对应的虚拟地址,方便访问
/** The shadow VMCS. */
R0PTRTYPE(void *) pvShadowVmcs; 用于支持嵌套虚拟化nested-guest shadow VMCS
/** The virtual-APIC page. */
R0PTRTYPE(uint8_t *) pbVirtApic;
/** The MSR bitmap. */
R0PTRTYPE(void *) pvMsrBitmap; 设置msr事件的bitmap
/** The VM-entry MSR-load area. */
R0PTRTYPE(void *) pvGuestMsrLoad;
/** The VM-exit MSR-store area. */
R0PTRTYPE(void *) pvGuestMsrStore;
/** The VM-exit MSR-load area. */
R0PTRTYPE(void *) pvHostMsrLoad;
/** @} */
/** @name Real-mode emulation state. 实模式模拟的状态,多用于32位GuestOS在二进制翻译模式下系统启动时的模拟
* @{ */
/** Set if guest was executing in real mode (extra checks). */
bool fWasInRealMode;
/** Set if the guest switched to 64-bit mode on a 32-bit host. */
bool fSwitchedTo64on32Obsolete;
/** Padding. */
bool afPadding0[6];
struct
{
X86DESCATTR AttrCS;
X86DESCATTR AttrDS;
X86DESCATTR AttrES;
X86DESCATTR AttrFS;
X86DESCATTR AttrGS;
X86DESCATTR AttrSS;
X86EFLAGS Eflags;
bool fRealOnV86Active;
bool afPadding1[3];
} RealMode;
/** @} */
/** @name Host-physical address of VMCS and related data structures.
* @{ */
/** The VMCS. */
RTHCPHYS HCPhysVmcs;
/** The shadow VMCS. */
RTHCPHYS HCPhysShadowVmcs;
/** The virtual APIC page. */
RTHCPHYS HCPhysVirtApic;
/** The MSR bitmap. */
RTHCPHYS HCPhysMsrBitmap;
/** The VM-entry MSR-load area. */
RTHCPHYS HCPhysGuestMsrLoad;
/** The VM-exit MSR-store area. */
RTHCPHYS HCPhysGuestMsrStore;
/** The VM-exit MSR-load area. */
RTHCPHYS HCPhysHostMsrLoad;
/** @} */
/** @name R0-memory objects address for VMCS and related data structures.
* @{ */
/** R0-memory object for VMCS and related data structures. */
RTR0MEMOBJ hMemObj;
/** @} */
/** Padding. */
uint64_t au64Padding[2];
} VMXVMCSINFO;
因为传入CPU的地址需要是物理地址,为了VMM代码方便访问这些物理地址,可以看到很多内容同时保存了物理地址和虚拟地址的指针。
4.1.3 VMExit:VMX异常
VMM调用VMEntry进入Guest OS之后,GuestOS需要与Host交互(比如GuestOS调用设备的IN/OUT指令),则需要通过VMExit退回到VMM里,让host处理相关内容之后再返回GuestOS。
什么样的时间需要处理器触发VMExit异常,则需要在GuestOS启动之前在VMCS里设置好,如上一章的VMXVMCSINIF结构体的里u32ProcCtls项等。
VMExit解释可以具体参考Intel指令手册的第24章
VMCS设置好了VMExit handlle之后,调用
VMXWriteVmcs32(VMX_VMCS32_CTRL_PROC_EXEC, uProcCtls);
就完成了VMExit时间的设定,当GuestOS运行到指定的CPU指令的时候,CPU就会触发VMExit中断。
除了少数不可屏蔽VMExit中断,比如VMX_EXIT_CPUID之外,其他VMExit中断都是可以在GuestOS运行的时候动态控制的。
比如在某个时间,当CPU执行CR8写入指令的时候发现VMExit:
//设置fVal值
fVal |= VMX_PROC_CTLS_CR8_STORE_EXIT;
//commit到VMCS里
VMXWriteVmcs32(VMX_VMCS32_CTRL_PROC_EXEC, fVal);
从commit到VMCS之后,当CPU执行到CR8写入指令的时候,就会触发VMExit异常,进入VMM里。
同意,如果不想当CPU执行CR8写入指令的时候发生异常, 修改VMCS即可。
//设置fVal值
fVal &= ~VMX_PROC_CTLS_CR8_STORE_EXIT;
//commit到VMCS里
VMXWriteVmcs32(VMX_VMCS32_CTRL_PROC_EXEC, fVal);
从commit到VMCS之后,当CPU执行到CR8写入指令的时候,都不会触发VMExit异常。
注释:
VMExit数量和虚拟机性能的关系:
VMExit这条指令会让CPU从non-root模式切换到root模式,这个过程会保留GuestOS的运行上下文,然后切换到HostOS运行上下文,同样当vmexit处理完成从root模式切换到non-root模式的时候,也会触发上下文切换,所以如果GuestOS运行的时候,发生的vmexit过多,会导致GuestOS运行效率大大下降(因为大量的CPU时间片都用来处理上下文切换),所以VMExit的设置需要避免设置一些调用频繁的时间,比如gs切换,rdmsr等。
但如果真的需要监控rdmsr指令怎么办?比如VT模式可以通过rdmsr C0000082(syscall入口)过PG时,需要当PatchGuard代码执行rdmsr C0000082这条指令的时候发生vmexit中断。
Intel非常贴心的设置了各种细粒度控制的办法,比如对msr的访问,可以设置msr bitmap来控制哪些msr寄存器的访问需要产生vmexit中断。
void hmR0VmxSetMsrPermission(PVMCPU pVCpu, uint32_t uMsr, VMXMSREXITREAD enmRead, VMXMSREXITWRITE enmWrite)
{
uint8_t *pbMsrBitmap = (uint8_t *)pVCpu->hm.s.vmx.pvMsrBitmap;
/*
* MSR Layout:
* Byte index MSR range Interpreted as
* 0x000 - 0x3ff 0x00000000 - 0x00001fff Low MSR read bits.
* 0x400 - 0x7ff 0xc0000000 - 0xc0001fff High MSR read bits.
* 0x800 - 0xbff 0x00000000 - 0x00001fff Low MSR write bits.
* 0xc00 - 0xfff 0xc0000000 - 0xc0001fff High MSR write bits.
*/
//0x00000000 - 0x00001fff id
if (uMsr <= 0x00001fff)
iBit = uMsr;
//0xc0000000 - 0xc0001fff 是设置在pbMsrBitmap的0x400~0x800
else if (uMsr - UINT32_C(0xc0000000) <= UINT32_C(0x00001fff))
{
iBit = uMsr - UINT32_C(0xc0000000);
pbMsrBitmap += 0x400;
}
//msr读取拦截是设置在0 ~ 0x800里
if (enmRead == VMXMSREXIT_INTERCEPT_READ)
ASMBitSet(pbMsrBitmap, iBit);
else
ASMBitClear(pbMsrBitmap, iBit);
//msr写入拦截是设置在0x800 ~ 0xfff里
if (enmWrite == VMXMSREXIT_INTERCEPT_WRITE)
ASMBitSet(pbMsrBitmap + 0x800, iBit);
else
ASMBitClear(pbMsrBitmap + 0x800, iBit);
}
当需要拦截c0000082这个msr读取的时候,只需要设置MSR_K8_LSTAR写拦截,即可拦截到读取这个msr
hmR0VmxSetMsrPermission(pVCpu, MSR_K8_LSTAR, VMXMSREXIT_INTERCEPT_READ, VMXMSREXIT_INTERCEPT_WRITE);
参考资料:
https://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf