Step 2: VMCS 配置 - equation314/RVM-Tutorial GitHub Wiki
在本阶段,我们将介绍并配置 VMX 中的一个重要数据结构 VMCS,做好运行 guest 代码之前的最后准备。
代码:https://github.com/equation314/RVM-Tutorial/commit/d5808f7b3e52bd5350dc6a220e7831732cc2c411
架构手册:Intel 64 and IA-32 Architectures Software Developer’s Manual (SDM) Vol. 3C, Chapter 24
在硬件对虚拟化的实现中,一般只有一套完整的机器状态,而虚拟化要求在一个物理 CPU 上同时运行 host 和 guest。这就需要在 VM entry/VM exit 时对 host/guest 的机器状态进行切换。而切换时需要保存的状态就被存储在一块专门的内存 VMCS 中。
VMCS 是一个存储在内存中的数据结构,和 vCPU 一一对应,每有一个 vCPU 都需要配置其相应的 VMCS。其中的字段除了用于存储 guest/host 状态外,还用于控制虚拟化的行为,具体包括以下几类:
- Guest-state area:在 VM exit 时,guest 的状态会被保存到该区域,并在 VM entry 时从该区域恢复。
- Host-state area:在 VM exit 时,host 的状态会从该区域恢复 (VM entry 时不保存)。
- VM execution/entry/exit control fields:对运行、进入、退出 non-root 模式时处理器的行为进行配置,如配置哪些情况下会发生 VM exit。
- VM exit information fields:只读字段,报告 VM exit 发生时的一些信息,如发生 VM exit 的原因。我们将在 Step 3 中用到此类字段。
VMCS 所在的内存被称为 VMCS region,与 VMXON region 一样,也是 4KB 大小 4KB 对齐的,且需要向前 4 字节写入 VMCS revision identifier。VMCS region 的物理地址被称为 VMCS pointer。
物理 CPU 在首次进入 non-root 模式之前,需要先激活 vCPU 对应的 VMCS,方法是依次执行以下两条指令 (VMCS pointer 作为操作数):
-
VMCLEAR
:将该 VMCS 设为“干净的”,使得其可以被激活。 -
VMPTRLD
:将该 VMCS 激活,即将该 VMCS 与当前物理 CPU 绑定,该 VMCS 就被称为“当前 VMCS”,之后的VMREAD
/VMWRITE
/VMLAUNCH
/VMRESUME
指令只对当前 VMCS 生效。对应的还有VMPTRST
指令,用于取得当前的 VMCS pointer (在本项目中不需要用到)。
访问 VMCS 中的字段不能直接使用内存读写指令,而是要用专门的 VMREAD
/VMWRITE
指令。
VMCS host 状态会在发生 VM exit 而从 non-root 切换回 root 时,从 VMCS 自动加载进处理器中,但无需在 VM entry 时保存。这些状态主要包括:
- 控制寄存器:
CR0
、CR3
、CR4
。 - 指令指针与栈指针:
RIP
、RSP
。 - 段选择子 (selector):
CS
、SS
、DS
、ES
、FS
、GS
、TR
。 - 段基址 (base address):
FS base
、GS base
、TR base
、GDTR base
、IDTR base
。 - 一些 MSR,如
IA32_PAT
、IA32_EFER
。
在配置时,一般直接将它们设为当前 (host) 的状态即可。但是 RIP
应该设为 VM exit 处理函数的地址,RSP
应该设为处理 VM exit 时的栈指针。
需要注意,VMCS 也不是包含了所有的机器状态,如 RAX
、RBX
等通用寄存器就不在 VMCS 字段中,因此它们需要在 VM entry/exit 时由软件实现保存与恢复。
VMCS guest 状态会在发生 VM entry 时从 VMCS 自动加载进处理器中,并在 VM exit 时自动保存到 VMCS 中。这些状态主要包括:
- 控制寄存器:
CR0
、CR3
、CR4
。 -
RIP
、RSP
、RFLAGS
。 - 完整的段寄存器:即
CS
、SS
、DS
、ES
、FS
、GS
、TR
段的选择子、基址、界限 (limit) 与访问权限 (access rights)。 -
GDTR
与IDTR
的基址与界限。 - 一些 MSR,如
IA32_PAT
、IA32_EFER
。
在设置时,还需要考虑 guest 的运行模式。在 x86 下,运行模式包括 16 位实模式、32 位保护模式、64 位 IA-32e 模式,不同的运行模式需要配置好对应的段寄存器访问权限。其中 CS
段寄存器的访问权限还指示了 guest 的当前特权级 (CPL)。
这些字段用于控制在 non-root 模式运行时处理器的行为。常用的有以下几个:
- Pin-based VM-execution controls:用于配置 hypervisor 对 guest 异步事件的拦截 (例如中断)。
- Processor-based VM-execution controls:又可分为 primary processor-based 和 secondary processor-based,用于配置 hypervisor 对 guest 同步事件的拦截 (例如执行特定的指令)。
- Exception bitmap:用于配置 hypervsior 对 guest 异常的拦截。
- I/O-bitmap address:用于配置 hypervisor 对 guest 读写特定 I/O 端口的拦截。
- MSR-bitmap address:用于配置 hypervisor 对 guest 读写特定 MSR 的拦截。
- Extended-page-table pointer:指定扩展页表 (EPT) 的基址。(Step 4)
以上字段除了带 address、pointer 外的都是一个 bitmap,将相应的位设为 1 或 0 就表示启用或关闭了相应的功能。
需要注意,对 pin-based、primary processor-based、secondary processor-based VM-execution controls 以及下文的 VM-exit controls、VM-entry controls 的配置较为繁琐。因为同一个字段,不同的 CPU 对各个位的支持情况 (默认值和允许值) 不同,需要根据支持情况设置未用到的位的默认值。感兴趣的参见代码或 SDM Vol. 3D 附录 A.2 ~ A.5。
这些字段用于控制在 VM exit 发生时处理器的行为。除了 VM-exit controls 外,还有:
- VM-exit MSR-store count、VM-exit MSR-store address:VM exit 时要保存的 (guest) MSR 数量与内存区域。
- VM-exit MSR-load count、VM-exit MSR-load address:VM exit 时要载入的 (host) MSR 数量与内存区域。
因为除了 IA32_EFER
、IA32_PAT
等少量 MSR 外,VMCS 中没有空间存储其他 MSR 了,因此在这里指定了一个要额外保存与恢复的 MSR 的列表。这样的设计也使得在 VM exit 时避免切换不常用的 MSR,从而减少 guest 与 host 间的切换开销。VM-entry control fields 中也有类似的字段。
这些字段用于控制在 VM entry 发生时处理器的行为。除了 VM-entry controls 外,还有:
- VM-entry MSR-load count、VM-entry MSR-load address:VM entry 时要载入的 (guest) MSR 数量与内存区域。
- VM-entry interruption-information field:用于向 guest 注入虚拟中断或异常。(Step 6)
本阶段主要工作是在 vcpu.rs 中实现了一个 VmxVcpu
结构,并完成了对 VMCS 的配置。注意此时的 VMCS 配置不是最终的,为了完成相应的功能并保证 guest 正确运行,我们会在之后的阶段对一些字段进行微调。
VmxVcpu
结构如下,包含该 vCPU 对应的 VMCS,以及所有通用寄存器:
#[repr(C)]
pub struct VmxVcpu<H: RvmHal> {
guest_regs: GeneralRegisters,
vmcs: VmxRegion<H>,
}
在 VmxVcpu::new()
中,首先分配并初始化了 VMCS,然后调用 setup_vmcs
配置 VMCS。
在 setup_vmcs
中,首先执行 VMCLEAR
和 VMPTRLD
指令激活 VMCS,然后又分 host、guest、control 三部分进行分别配置。其中对 VMCS 字段的读写实现在 vmcs.rs 中。
在 setup_vmcs_host
中,大多数 host 状态通过直接读取当前对应的系统状态进行配置,除了 RSP
和 RIP
,它们会在 Step 3 中被设为合适的值。
在 setup_vmcs_guest
中,我们先配置 guest 的 CR0
禁用保护模式与分页,并只设置 CR4
的 VMXE
位。同时还配置了 CR0
/CR4
的 guest/host masks 和 read shadows,这些字段会影响对 guest 读写 CR0
/CR4
的拦截 (详见 SDM Vol. 3C, Section 24.6.6)。之后,我们使用一个宏 set_guest_segment!
来配置 guest 的段寄存器,其中基址与选择子全为 0,界限全为 0xffff
,访问权限为 16 位。其他系统状态也都设为了默认值。
在 setp_vmcs_control
中,我们通过 vmcs::set_control
函数,可设置或清除指定控制字段的某些位,包括:
- 在 pin-based controls 设置了 NMI 中断拦截,取消外部中断拦截。
- 在 primary processor-based controls 取消了
CR3
读写拦截。 - 在 secondary processor-base controls 中设置了 guest 能使用
RDTSCP
和INVPCID
指令。 - 在 exit controls 中设置了退出时为 64 位 host、自动切换 host/guest 的
IA32_PAT
与IA32_EFER
。 - 在 entry controls 中设置了进入时自动载入 guest
IA32_PAT
与IA32_EFER
。
此外,我们将不会在 entry/exit 时保存与恢复其他 MSR,因为 hypervisor 没有用到其他 MSR,且目前只有一个 vCPU 而不需要切换。
对于 exception 和 I/O bitmap 设为空,表示直通所有 guest 异常与 I/O 指令;而 MSR bitmaps 为空表示拦截所有 MSR 读写。我们将在 Step 5 启用 MSR bitmaps。
阅读 Intel SDM Vol. 3C, Chapter 24: Virtual-Machine Control Structures 相关小节,回答以下问题:
- 如果让要 hypervisor 实现以下功能,应该如何配置 VMCS?
- 拦截 guest
HLT
指令 - 拦截 guest
PAUSE
指令 - 拦截外部设备产生的中断,而不是直通给 guest
- 打开或关闭 guest 的中断
- 拦截 guest 缺页异常 (#PF)
- 拦截所有 guest I/O 指令 (x86
IN
/OUT
/INS
/OUTS
等) - *只拦截 guest 对串口的 I/O 读写 (I/O 端口为
0x3f8
) - 拦截所有 guest MSR 读写
- *只拦截 guest 对
IA32_EFER
MSR 的写入 - *只拦截 guest 对
CR0
控制寄存器PG
位 (31 位) 的写入
- 拦截 guest
- *如果要在单核 hypervisor 中交替运行两个 vCPU,应该如何操作 VMCS?