3. 虚拟化扩展概览
在 ARMv8-A 架构中,虚拟化并不是靠“软件模拟”硬撑出来的,而是由处理器硬件直接提供的一整套异常级别、寄存器与地址翻译机制。理解这套机制,是后续编写 KVM/ARM64 hypervisor、调试 Guest 启动失败、分析 Stage-2 缺页的起点。本节把最核心的四件事串起来:异常级别模型、HCR_EL2 控制、VTTBR_EL2 基址、Stage-2 翻译与 VMID。
一、异常级别:EL0~EL3 的分工
ARMv8-A 定义了四个异常级别。EL0 是用户态,EL1 是 Guest 内核态,EL2 是 hypervisor 态,EL3 是安全监控态。KVM 运行在 EL2,Guest 内核运行在 EL1,Guest 用户进程运行在 EL0。当 Guest 执行敏感指令或访问未映射的物理地址时,硬件会把异常陷入 EL2,由 KVM 接管处理。
关键点在于:Guest 看到的“物理地址”其实是 IPA(Intermediate Physical Address),不是真正的 PA。CPU 需要经过两级翻译才能到达真实内存:Stage-1 由 Guest 自己控制(VA→IPA),Stage-2 由 hypervisor 控制(IPA→PA)。这正是虚拟化扩展最本质的能力。
二、HCR_EL2:虚拟化的总开关
HCR_EL2(Hypervisor Configuration Register)是 EL2 最重要的控制寄存器之一。它决定了 Guest 的哪些行为会陷入 EL2、Stage-2 翻译是否启用、以及 Guest 能看到怎样的系统视图。常见关键位如下:
| 位域 | 名称 | 作用 |
|---|---|---|
| bit 0 | VM | 启用 Stage-2 翻译,Guest 的 IPA 需经 Stage-2 转为 PA |
| bit 1 | PTW | 保护 Stage-2 页表写入,防止 Guest 篡改 |
| bit 2 | FMO | 物理 IRQ 路由到 EL2 |
| bit 3 | IMO | 物理 IRQ 路由到 EL2 |
| bit 4 | AMO | SError 路由到 EL2 |
| bit 27 | TGE | 启用 EL0 的 trap 到 EL2,常用于 Guest 用户态陷入 |
| bit 31 | RW | EL1 执行状态,1 表示 AArch64 |
例如 KVM 在初始化 vCPU 时,通常会设置 HCR_EL2.VM=1 和 HCR_EL2.RW=1,并根据需要打开 IMO/FMO/AMO 把中断路由到 EL2。若忘记设置 VM 位,Guest 的 IPA 会被当作 PA 直接使用,Stage-2 形同虚设,Guest 可能直接访问到宿主物理内存,这是非常危险的配置错误。
三、VTTBR_EL2:Stage-2 页表的入口
VTTBR_EL2(Virtualization Translation Table Base Register)保存 Stage-2 翻译表的基地址,同时携带 VMID。它的结构大致为:低 48 位左右是页表基址(按对齐要求低位为 0),高位字段是 VMID。KVM 为每个 VM 分配一个 Stage-2 页表,并把根地址写入 VTTBR_EL2。
/* 简化示意:设置 VTTBR_EL2 */
vttbr = (u64)vm->stage2_pgd | ((u64)vm->vmid << VTTBR_VMID_SHIFT);
write_sysreg(vttbr, vttbr_el2);
isb();
当 Guest 访问内存时,硬件先做 Stage-1(VA→IPA),再用 VTTBR_EL2 指向的页表做 Stage-2(IPA→PA)。如果 Stage-2 中该 IPA 没有映射,就会产生 Stage-2 translation fault,陷入 EL2,由 KVM 决定是分配内存、注入 MMIO,还是直接报错。
四、VMID:让 TLB 不再“串味”
VMID(Virtual Machine Identifier)是 Stage-2 翻译的地址空间标识。TLB 中会缓存 Stage-2 的翻译结果,如果不同 VM 共用同一个 VMID,切换 VM 时就必须刷新 TLB,否则会命中错误的翻译。VMID 让不同 VM 的 TLB 条目可以共存,切换时无需全量刷新,显著降低虚拟化开销。
VMID 的位宽由 ID_AA64MMFR0_EL1.VMIDBits 决定,常见为 8 位或 16 位。KVM 会维护一个 VMID 分配器,回收不再使用的 VMID。若 VMID 耗尽,KVM 需要刷新 TLB 并复用旧值。
核心链路:Guest VA → Stage-1(Guest 页表)→ IPA → Stage-2(VTTBR_EL2 + VMID)→ PA。HCR_EL2.VM 决定 Stage-2 是否启用,VTTBR_EL2 决定用哪张 Stage-2 页表,VMID 决定 TLB 如何区分不同 VM。
五、Stage-2 翻译流程与缺页处理
Stage-2 翻译由硬件 MMU 自动完成,但缺页时会产生异常。典型流程如下:
- Guest 访问 VA,Stage-1 翻译得到 IPA。
- 硬件用 VTTBR_EL2 找到 Stage-2 根页表,逐级查找 IPA 对应的 PA。
- 若 Stage-2 表项无效,产生 Stage-2 translation fault,ESR_EL2 记录故障 IPA 与访问类型。
- KVM 读取 ESR_EL2、HPFAR_EL2,得到故障 IPA,判断是内存还是 MMIO。
- 若是内存,分配物理页并填充 Stage-2 页表;若是 MMIO,模拟设备访问并推进 Guest PC。
- 返回 Guest,重新执行触发缺页的指令。
下面是一段简化的缺页处理伪代码:
static int handle_stage2_fault(struct kvm_vcpu *vcpu)
{
u64 esr = read_sysreg(esr_el2);
u64 hpfar = read_sysreg(hpfar_el2);
u64 ipa = (hpfar & HPFAR_MASK) << 8;
u64 far = read_sysreg(far_el2);
if (is_mmio(ipa))
return kvm_mmio_emulate(vcpu, ipa, esr);
return kvm_alloc_and_map(vcpu, ipa, esr);
}
六、典型场景与常见误区
场景:一个 Guest 内核启动时访问 UART 寄存器。Stage-1 把 VA 翻译成 IPA,Stage-2 发现该 IPA 没有映射,触发 fault。KVM 根据 IPA 判断这是 UART MMIO 区域,于是模拟一次写操作,把字符输出到宿主串口,然后返回 Guest。Guest 完全感知不到自己被“骗”了。
误区一:认为 Stage-2 只是“多一层页表”。实际上它还承担了内存隔离、MMIO 拦截、脏页跟踪等职责,是虚拟化安全边界的关键。
误区二:忽略 VMID 的 TLB 影响。在 VM 切换频繁的场景中,VMID 配置不当会导致大量 TLB 刷新,性能明显下降。
误区三:HCR_EL2 位域设置错误。例如把 IMO/FMO 配错,会导致中断无法正确注入 Guest,表现为 Guest 卡死或时钟异常。
调试 Stage-2 问题时,优先查看 ESR_EL2 的 EC 字段(是否为 0x24/0x25 数据/指令 abort)、HPFAR_EL2 的故障 IPA,以及 VTTBR_EL2 是否指向正确的 Stage-2 根页表。再确认 HCR_EL2.VM 是否已置位。
七、小结与检查清单
- EL2 是 hypervisor 的运行级别,KVM 在此接管 Guest 的敏感操作。
- HCR_EL2.VM 启用 Stage-2 翻译,是虚拟化内存隔离的总开关。
- VTTBR_EL2 保存 Stage-2 页表基址,并携带 VMID。
- Stage-2 把 IPA 翻译为 PA,缺页时陷入 EL2 由 KVM 处理。
- VMID 用于区分不同 VM 的 TLB 条目,减少切换开销。
- 调试时关注 ESR_EL2、HPFAR_EL2、VTTBR_EL2 与 HCR_EL2 的配置一致性。
把上面这张图记在脑子里,再回头看 KVM/ARM64 的代码,你会发现 kvm_arch_vcpu_load、stage2_set_pte、kvm_vmid_alloc 这些函数其实都在围绕 HCR_EL2、VTTBR_EL2 和 VMID 做文章。虚拟化扩展并不神秘,它只是把“隔离”和“翻译”这两件事交给了硬件,而 hypervisor 负责在缺页时做出正确的决策。