在本文中,我想考虑基于Intel VMX硬件虚拟化技术创建一个
简单的管理程序的实际情况。
可以肯定的是,硬件虚拟化是系统编程的一个非常狭窄的专业领域,并且没有庞大的社区。 我希望本文中的材料将对那些希望发现硬件虚拟化及其提供的可能性的人有所帮助。 如开始时所说,我只想考虑实践方面,而不要深入理论,因此假定读者熟悉x86-64体系结构,并且至少具有VMX机制的一般概念。
文章的来源 。
让我们开始为管理程序设置目标:
- 在加载来宾操作系统之前运行
- 支持一个逻辑处理器和4 GB的来宾物理内存
- 确保来宾OS与物理内存区域中投影的设备正常工作
- VMexits处理
- 第一个命令中的来宾OS应该在虚拟环境中运行。
- 通过COM端口输出调试信息(通用方法,易于实现)
作为来宾操作系统,我选择了Windows 7 x32,在其中设置了以下限制:
- 仅涉及一个CPU内核
- 禁用PAE选项,该选项允许32位OS使用超过4GB的物理内存量
- BIOS在旧版模式下,已禁用UEFI
引导加载程序的说明
为了使虚拟机监控程序在PC启动时启动,我选择了最简单的方法,即,将引导加载程序写入安装了来宾OS的磁盘的MBR扇区中。 还必须将管理程序代码放在磁盘上的某个位置。 在我的情况下,原始MBR从2048扇区开始读取引导加载程序,这提供了一个有条件的自由区域,可写入(2047 * 512)Kb。 这足以容纳管理程序的所有组件。
下面是虚拟机管理程序在磁盘上的布局,所有值均以扇区为单位设置。

下载过程如下:

- loader.mbr从磁盘读取加载程序代码loader.main并将控制权转移给它。
- loader.main切换到长模式,然后读取已加载的loader.table元素的表,基于此表,会将虚拟机管理程序组件进一步加载到内存中。
- 引导加载程序完成在地址0x100000000的物理内存中的工作后,将有一个虚拟机监控程序代码,选择该地址是为了使0到0xFFFFFFFF的范围可用于直接映射到来宾物理内存。
- 原始Windows MBR在物理地址0x7C00引导。
我想提请大家注意,切换到长模式后的引导加载程序不能再使用BIOS服务来处理物理磁盘,因此我使用了“高级主机控制器接口”来读取磁盘。
有关哪个的更多详细信息,请参见
此处 。
虚拟机监控程序职位描述
系统管理程序获得控制后,其首要任务是初始化其必须工作的环境,为此,将依次调用这些函数:
- InitLongModeGdt() -创建并加载包含4个描述符的表:NULL,CS64,DS64,TSS64
- InitLongModeIdt(isr_vector) -通过通用处理程序(或更确切地说,其存根)初始化前32个中断向量
- InitLongModeTSS() -初始化任务状态段
- InitLongModePages() -初始化分页:
[0x00000000-0xFFFFFFFF]-页面大小2MB,禁用缓存;
[0x100000000-0x13FFFFFFF]-页面大小2 MB,缓存回写,全局页面;
[0x140000000-n]-不存在; - InitControlAndSegmenRegs() -重新加载段寄存器
接下来,您需要确保处理器支持VMX,并通过
CheckVMXConditions()函数执行验证:
- CPUID.1:ECX.VMX [位5]必须设置为1
- 在MSR寄存器IA32_FEATURE_CONTROL中,必须设置位2-在SMX操作之外启用VMXON,而位0-锁定(在Bochs中调试时相关)
如果一切正常,并且管理程序在支持硬件虚拟化的处理器上运行,请转到VMX的初始初始化,请参阅
InitVMX()函数:
- 创建了大小为4096字节的内存区域VMXON和VMCS(虚拟机控制数据结构)。 从MSR IA32_VMX_BASIC获取的VMCS修订标识符记录在每个区域的前31位中。
- 检查系统寄存器CR0和CR4中的所有位是否根据VMX的要求设置。
- 通过VMXON命令(VMXON区域的物理地址作为参数)将逻辑处理器置于vmx根模式。
- VMCLEAR(VMCS)命令将VMCS的启动状态设置为“清除”,并且该命令将特定于实现的值设置为VMCS。
- VMPTRLD(VMCS)命令将作为参数传递的当前VMCS地址加载到current-VMCS指针中。
来宾OS的执行将从地址0x7C00的实模式开始,正如我们还记得的,loader.main loader将win7.mbr放置在该地址。 为了重新创建与通常执行mbr的虚拟环境相同的虚拟环境,将
调用InitGuestRegisterState()函数
,该函数将vmx非根寄存器设置如下:
CR0 = 0x10 CR3 = 0 CR4 = 0 DR7 = 0 RSP = 0xFFD6 RIP = 0x7C00 RFLAGS = 0x82 ES.base = 0 CS.base = 0 SS.base = 0 DS.base = 0 FS.base = 0 GS.base = 0 LDTR.base = 0 TR.base = 0 ES.limit = 0xFFFFFFFF CS.limit = 0xFFFF SS.limit = 0xFFFF DS.limit = 0xFFFFFFFF FS.limit = 0xFFFF GS.limit = 0xFFFF LDTR.limit = 0xFFFF TR.limit = 0xFFFF ES.access rights = 0xF093 CS.access rights = 0x93 SS.access rights = 0x93 DS.access rights = 0xF093 FS.access rights = 0x93 GS.access rights = 0x93 LDTR.access rights = 0x82 TR.access rights = 0x8B ES.selector = 0 CS.selector = 0 SS.selector = 0 DS.selector = 0 FS.selector = 0 GS.selector = 0 LDTR.selector = 0 TR.selector = 0 GDTR.base = 0 IDTR.base = 0 GDTR.limit = 0 IDTR.limit = 0x3FF
应当注意的是,段寄存器DS和ES的描述符高速缓存的限制字段是0xFFFFFFFF。 这是使用虚幻模式的示例-一种x86处理器功能,可让您绕过实模式下的段数限制。 您可以
在此处了解更多信息。
在vmx非root模式下,来宾OS可能会遇到需要以vmx root模式将控制权交还给主机的情况。 在这种情况下,将发生VM退出,在此期间将保存vmx非root用户的当前状态并加载vmx-root。 vmx-root的初始化由
InitHostStateArea()函数执行,该函数设置寄存器的以下值:
CR0 = 0x80000039 CR3 = PML4_addr CR4 = 0x420A1 RSP = STACK64 RIP = VMEXIT_handler ES.selector = 0x10 CS.selector = 0x08 SS.selector = 0x10 DS.selector = 0x10 FS.selector = 0x10 GS.selector = 0x10 TR.selector = 0x18 TR.base = TSS GDTR.base = GDT64 IDTR.base = IDTR
接下来,
执行来宾物理地址空间的创建
(InitEPT()函数)。 这是创建虚拟机监控程序时最重要的时刻之一,因为在任何内存位置上错误设置大小或类型都可能导致错误,这些错误可能不会立即显现出来,但很有可能会导致来宾OS意外制动或冻结。 通常,这里没有什么令人愉快的地方,最好要充分注意调整内存。
下图显示了访客物理地址空间模型:

所以我们在这里看到的是:
- [0-0xFFFFFFFF]来宾地址空间的整个范围。 默认类型:回写
- [0xA0000-0xBFFFFF]-视频RAM。 类型:不可缓存
- [0xBA647000-0xFFFFFFFF]-设备内存。 类型:不可缓存
- [0x0000000-0xCFFFFFFF]-视频RAM。 类型:写合并
- [0xD0000000-0xD1FFFFFF]-视频内存。 类型:写合并
- [0xFA000000-0xFAFFFFFF]-视频RAM。 类型:写合并
我从RAMMap实用程序(“物理范围”选项卡)中获取了有关创建此类区域的信息,还使用了Windows设备管理器中的数据。 当然,在另一台PC上,地址范围可能会有所不同。 至于来宾内存类型,在我的实现中,该类型仅由EPT表中指定的值确定。 这很简单,但并非完全正确,通常应考虑来宾OS要在其页面寻址中安装的内存类型。
来宾地址空间的创建完成后,可以继续执行VM Execution控制字段
设置(InitExecutionControlFields()函数)。 这是一个相当大的选项集,可让您在vmx非root用户模式下设置来宾OS的操作条件。 例如,您可以跟踪对输入/输出端口的调用,或监视MSR寄存器中的更改。 但是在本例中,我仅使用控制CR0寄存器中某些位设置的功能。 事实是,对于vmx非根模式和vmx根模式,共有30(CD)和29(NW)位是通用的,并且如果guest虚拟机OS将这些位设置为1,将对性能产生负面影响。
配置虚拟机监控程序的过程几乎完成,仅保留建立对迁移到来宾模式vmx非根目录并返回到主机模式vmx根目录的控制。 在功能中进行设置:
InitVMEntryControl()设置,用于过渡到非root用户的vmx:
- 加载来宾IA32_EFER
- 加载来宾IA32_PAT
- 加载来宾MSR(IA32_MTRR_PHYSBASE0,IA32_MTRR_PHYSMASK0,IA32_MTRR_DEF_TYPE)
InitVMExitControl()设置用于切换到vmx根目录:
- 加载主机IA32_EFER;
- 保存来宾IA32_EFER;
- 加载主机IA32_PAT;
- 保存来宾IA32_PAT;
- Host.CS.L = 1,Host.IA32_EFER.LME = 1,Host.IA32_EFER.LMA = 1;
- 保存访客MSR(IA32_MTRR_PHYSBASE0,IA32_MTRR_PHYSMASKSK0,IA32_MTRR_DEF_TYPE);
- 加载主机MSR(IA32_MTRR_PHYSBASE0,IA32_MTRR_PHYSMASK0,IA32_MTRR_DEF_TYPE);
现在,所有设置均已完成,
VMLaunch()函数
将处理器置于vmx非root用户模式,并且来宾OS开始运行。 如前所述,可以在vm执行控制设置中设置条件,在这种情况下,虚拟机监控程序将以vmx根模式将控制权返回给自己。 在我的简单示例中,我为来宾操作系统提供了完全的操作自由度,但是,在某些情况下,系统管理程序仍将需要干预和调整操作系统。
- 如果来宾操作系统尝试更改CR0寄存器中的CD和NW位,则VM退出处理程序
纠正CR0中记录的数据。 还修改了CR0读取阴影字段,以便在读取CR0时,来宾OS接收记录的值。 - 执行xsetbv命令。 无论设置如何,此命令始终调用VM Exit,因此我只是在vmx根模式下添加了它的执行。
- 执行丘比特命令。 此命令还会调用无条件的VM Exit。 但是我对它的处理程序做了一点改动。 如果eax参数中的值为0x80000002-0x80000004,则cpuid不会返回处理器品牌的名称,而是返回一行: VMX Study Core :)结果可以在屏幕截图中看到:

总结
作为文章示例编写的虚拟机监控程序完全有能力支持来宾OS的稳定运行,尽管它当然不是一个完整的解决方案。 不使用Intel VT-d,仅支持一个逻辑处理器,无法控制外围设备的中断和操作。 通常,我几乎不使用英特尔为硬件虚拟化提供的丰富工具集中的任何东西。 但是,如果社区感兴趣,我将继续写有关Intel VMX的文章,特别是因为有一些要写的东西。
是的,我几乎忘记了,使用Bochs调试虚拟机管理程序及其组件很方便。 首先,它是必不可少的工具。 不幸的是,在Bochs中下载虚拟机管理程序不同于下载到物理PC。 一次,我进行了一次特殊的装配以简化此过程,我将尝试整理货源,并在不久的将来将它们与项目放在一起。
仅此而已。 谢谢您的关注。