Windows Intel X86 VT学习 - VMCS 粗识
Chapter 25 — VMCS 学习笔记
一、从零到 Guest 跑起来的完整流程
先看全貌,后面每个知识点都是在填充这个流程的细节。
第一阶段:准备(普通驱动代码,还没进 VMX 模式)
1. CPUID(1) 检查 CPU 支持 VT
2. CR4.VMXE = 1(开启 VMX 指令支持)
3. 读 IA32_VMX_BASIC MSR(0x480) → 拿到 revision ID
第二阶段:进入 VMX 模式(配置 root)
4. 分配 VMXON region(4KB物理页)
5. 写入 revision ID
6. VMXON → 进入 VMX root operation
第三阶段:配置 Guest
7. 分配 VMCS region(4KB物理页)
8. 写入 revision ID
9. VMCLEAR → 初始化内部数据
10. VMPTRLD → 设为 current
11. VMWRITE × N → 填 Guest状态、Host状态、控制字段
第四阶段:运行 Guest
12. VMLAUNCH → 首次进入 Guest
13. [VM Exit → VMREAD → 处理 → VMRESUME] 循环
第五阶段:清理
14. VMCLEAR → 刷回内存
15. 释放 VMCS region
16. VMXOFF → 退出 VMX 模式
17. 释放 VMXON region
二、VMX 模式下的两块内存区域(别搞混!)
| VMXON region | VMCS region | |
|---|---|---|
| 用途 | 进入 VMX 模式的"入场券" | 存储 vCPU 的配置和运行状态 |
| 数量 | 每个物理CPU一个 | 每个vCPU一个 |
| 操作指令 | VMXON / VMXOFF | VMPTRLD / VMCLEAR / VMREAD / VMWRITE |
| revision ID 检查时机 | VMXON 时 | VMPTRLD 时 |
两块内存不能混用——VMPTRLD 传入 VMXON region 的地址会直接失败。
三、VMCS 是什么
VMCS:CPU 在 VMX 模式下的核心数据结构,类似 KPROCESS,用来存储一个虚拟核心的状态。
VMCS 负责三件事:
- VM Entry:从 VMCS 加载 Guest 的 CPU 状态
- VM Exit:保存 Guest 状态,加载 Host 状态
- Guest 执行期间:根据控制字段决定哪些操作触发 VM Exit
Q&A:VMCS Region 是我申请的,为什么不能直接读写?
分两个阶段:
VMPTRLD 之前 — 可以随便读写
void* vmcs = MmAllocateContiguousMemory(4096, ...);
*(UINT32*)vmcs = revision_id; // 完全合法,就是普通内存
此时 VMCS 是 inactive,CPU 不知道它的存在。
VMPTRLD 之后 — CPU 接管了
物理内存: [VMCS 数据] ← 你用 MOV 读写的是这里(可能是过期的)
↕ 不一定同步!
CPU 内部: [VMCS 数据的副本] ← CPU 实际用的是这里
VMPTRLD 后,CPU 可能把 VMCS 数据拷贝到芯片内部的专用存储(不是 L1/L2 cache)。
从此 CPU 读写 VMCS 都走内部副本,物理内存中的数据可能是旧的。
- 你用 MOV 读物理内存 → 读到旧数据(CPU 内部副本已更新但没写回)
- 你用 MOV 写物理内存 → CPU 看不到(它从内部副本读,不看物理内存)
所以不是"不让你读写",是"你读写的地方和 CPU 读写的地方不是同一个地方了"。
类比:DMA 缓冲区。硬件通过 DMA 写数据,CPU cache 里还是旧值,直接读会读到过期数据。
VMCS 更极端——数据在芯片内部专用存储里,连 cache flush 都没用,只能用专用指令:
| 你想做的事 | 怎么做 |
|---|---|
| 读 VMCS 字段 | VMREAD(知道去芯片内部读) |
| 写 VMCS 字段 | VMWRITE(知道往芯片内部写) |
| 强制同步回物理内存 | VMCLEAR(刷回内存 + 解除关联) |
四、操作 VMCS 的六条专用指令(暂时了解作用即可)
管理类
| 指令 | 作用 |
|---|---|
| VMPTRLD | "选中":设为 active + current,后续 VMREAD/VMWRITE 都针对它 |
| VMCLEAR | "放下":刷回内存 + launch state→clear + inactive |
| VMPTRST | "查一下":当前 current VMCS 的物理地址是什么 |
读写类 — 对 current VMCS 读写字段
| 指令 | 作用 |
|---|---|
| VMREAD | 读取 current VMCS 的某个字段 |
| VMWRITE | 写入 current VMCS 的某个字段 |
执行类 — 用 current VMCS 启动 Guest
| 指令 | 作用 |
|---|---|
| VMLAUNCH | 首次进入 Guest(要求 launch state = clear,成功后变 launched) |
| VMRESUME | 后续进入 Guest(要求 launched,比 VMLAUNCH 更快) |
五、VMCS Region 的内存布局(SDM 25.2)
偏移量 大小 内容
───────────────────────────────────
0x00 4 字节 [30:0] = Revision ID
[31] = Shadow-VMCS 标志位
0x04 4 字节 VMX-abort indicator
0x08 ~4KB VMCS data(格式不公开)
Revision ID(偏移 0x00, bits 30:0)
- CPU 的"版本号",从 IA32_VMX_BASIC MSR(0x480) 的 bits 30:0 读取
- VMXON region 和 VMCS region 都要写同一个 revision ID
- CPU 自己永远不写这个字段,只在 VMPTRLD 时读一次做校验
VMX-abort indicator(偏移 0x04)
- 正常运行时永远是 0
- VM Exit 过程本身出严重错误时 CPU 写错误码到这里,调试用
VMCS data(偏移 0x08)
- 所有 VMWRITE 写入的字段都存在这里(Guest RIP、Host CR3、控制位等)
- 内存布局 Intel 不公开,不能用偏移量直接读写,必须用 VMREAD/VMWRITE
六、VMCS 字段怎么写入?(步骤 11 详解)
核心规则
- Revision ID 是唯一一个用 MOV 写的字段(步骤 8,此时还没 VMPTRLD,还是普通内存)
- 其余所有字段都用 VMWRITE 一个一个写(步骤 11,VMPTRLD 之后)
- 每个字段有一个编号(field encoding),VMWRITE 靠编号找到要写哪个字段
代码长什么样
// 实际代码用宏定义,不需要记编号
#define VMCS_GUEST_RIP 0x681E
#define VMCS_GUEST_RSP 0x681C
#define VMCS_GUEST_CR3 0x6802
#define VMCS_HOST_RIP 0x6C16
#define VMCS_HOST_RSP 0x6C14
#define VMCS_PIN_BASED_CONTROLS 0x4000
__vmx_vmwrite(VMCS_GUEST_RIP, 0x1000); // Guest 从哪开始执行
__vmx_vmwrite(VMCS_GUEST_RSP, 0xFFFF); // Guest 的栈指针
__vmx_vmwrite(VMCS_HOST_RIP, &ExitHandler); // VM Exit 后跳到哪
__vmx_vmwrite(VMCS_PIN_BASED_CONTROLS, xxx); // 中断控制
// ... 几十个 VMWRITE
六组分类(SDM 25.3-25.9)
VMCS data 区域里存了几十个字段。Intel 按用途把它们分成六组,每组是什么、存的是什么:
| 组 | 是什么 | 存的是什么 | 谁写 | 谁读 |
|---|---|---|---|---|
| ① Guest-state | Guest CPU 的完整快照 | Guest 的寄存器值:RIP/RSP/CR3/RFLAGS/段寄存器等 | 初始化时 VMM 写;运行中 CPU 自动更新 | CPU(Entry时加载) |
| ② Host-state | VMM CPU 的恢复点 | VMM 的寄存器值:RIP/RSP/CR3/段选择子等 | VMM 写一次,基本不改 | CPU(Exit时加载) |
| ③ Execution Controls | 拦截规则表 | 一堆开关位:拦不拦 CPUID?拦不拦 IO?拦哪些 MSR? | VMM 写一次,基本不改 | CPU(Guest运行时持续检查) |
| ④ Exit Controls | Exit 过程的配置 | 开关位:Host 是 64-bit?要保存哪些 MSR? | VMM 写一次 | CPU(Exit时按配置执行) |
| ⑤ Entry Controls | Entry 过程的配置 | 开关位:Guest 是 64-bit?要注入中断? | VMM 写(可能每次 Entry 前改) | CPU(Entry时按配置执行) |
| ⑥ Exit Info | Exit 的报告单 | 退出原因编号、触发地址、指令长度等 | CPU 自动写(每次 Exit 时) | VMM 读(决定怎么处理) |
六组不是六个独立功能,是给 VM Entry → Guest 运行 → VM Exit 这个循环的三个阶段用的:
⑤ Entry Controls ─┐
① Guest-state ────┤ VM Entry Guest 开始跑
└─ ═══════════ → │
│ ③ Execution Controls
│ (持续监控:拦什么?)
│
↓ 触发拦截条件
② Host-state ────┐
④ Exit Controls ──┤ VM Exit VMM 接管
⑥ Exit Info ─────┘ ═══════════ → │
(CPU自动写) ↓
VMM 读⑥知道为什么被拦
处理完 → VMRESUME → 回到顶部
| 阶段 | 用到哪些组 | 回答什么问题 |
|---|---|---|
| A. 进入 Guest | ① Guest-state | Guest 的 CPU 长什么样?(RIP/RSP/CR3/段寄存器) |
| ⑤ Entry Controls | 进入时怎么配置?(64-bit?注入中断?) | |
| B. Guest 在跑 | ③ Execution Controls | Guest 做什么时要拦下来?(CPUID?IO?MSR?) |
| C. 退出到 VMM | ② Host-state | VMM 的 CPU 长什么样?(RIP/RSP/CR3) |
| ④ Exit Controls | 退出时怎么配置?(64-bit?保存哪些 MSR?) | |
| ⑥ Exit Info | 为什么退出的?(exit reason,CPU 自动填,只读) |
每组写什么(具体字段举例)
| 组 | 核心字段 | 说明 |
|---|---|---|
| ① Guest-state | RIP, RSP, CR0/CR3/CR4, RFLAGS, CS/SS 等段寄存器, GDTR/IDTR, SYSENTER MSRs, Activity State | VM Entry 加载、VM Exit 保存(双向) |
| ② Host-state | RIP, RSP, CR0/CR3/CR4, CS/SS/GS 等段选择子, GS Base, GDTR/IDTR base | 只在 VM Exit 时加载(单向) |
| ③ Execution Controls | Pin-Based(中断/NMI), Primary(HLT/CPUID/IO/MSR拦截), Secondary(EPT/VPID), Exception Bitmap, I/O Bitmap, MSR Bitmap | Guest 运行期间持续生效 |
| ④ Exit Controls | Host 64-bit 模式, 保存/加载哪些 MSR | 控制 Exit 过程 |
| ⑤ Entry Controls | Guest 64-bit 模式, Event Injection(给 Guest 注入中断/异常) | 控制 Entry 过程 |
| ⑥ Exit Info | Exit Reason, Exit Qualification, Instruction Length | 不用写!CPU 自动填,VMM 只读 |
总结:分组不是什么机制,就是同一个循环里三个阶段各自需要的数据。

浙公网安备 33010602011771号