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 0VM启用 Stage-2 翻译,Guest 的 IPA 需经 Stage-2 转为 PA
bit 1PTW保护 Stage-2 页表写入,防止 Guest 篡改
bit 2FMO物理 IRQ 路由到 EL2
bit 3IMO物理 IRQ 路由到 EL2
bit 4AMOSError 路由到 EL2
bit 27TGE启用 EL0 的 trap 到 EL2,常用于 Guest 用户态陷入
bit 31RWEL1 执行状态,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 自动完成,但缺页时会产生异常。典型流程如下:

  1. Guest 访问 VA,Stage-1 翻译得到 IPA。
  2. 硬件用 VTTBR_EL2 找到 Stage-2 根页表,逐级查找 IPA 对应的 PA。
  3. 若 Stage-2 表项无效,产生 Stage-2 translation fault,ESR_EL2 记录故障 IPA 与访问类型。
  4. KVM 读取 ESR_EL2、HPFAR_EL2,得到故障 IPA,判断是内存还是 MMIO。
  5. 若是内存,分配物理页并填充 Stage-2 页表;若是 MMIO,模拟设备访问并推进 Guest PC。
  6. 返回 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 的配置一致性。
ARMv8-A 虚拟化扩展:EL 级别与两级地址翻译 EL0 Guest 用户态 EL1 Guest 内核态 EL2 KVM Hypervisor EL3 Secure Monitor Guest VA Stage-1 页表(Guest 控制) IPA 中间物理地址 PA 真实物理地址 Stage-1 Stage-2 HCR_EL2 VM=1 启用 Stage-2;RW=1 使用 AArch64;IMO/FMO/AMO 路由中断 VTTBR_EL2 Stage-2 页表基址 + VMID VMID 区分不同 VM 的 TLB 条目,减少刷新 Stage-2 Translation Fault → 陷入 EL2 ESR_EL2 记录故障类型;HPFAR_EL2 提供故障 IPA;KVM 决定分配内存或模拟 MMIO 缺页

把上面这张图记在脑子里,再回头看 KVM/ARM64 的代码,你会发现 kvm_arch_vcpu_load、stage2_set_pte、kvm_vmid_alloc 这些函数其实都在围绕 HCR_EL2、VTTBR_EL2 和 VMID 做文章。虚拟化扩展并不神秘,它只是把“隔离”和“翻译”这两件事交给了硬件,而 hypervisor 负责在缺页时做出正确的决策。