AxVisor 深度学习笔记-ARM 虚拟化硬件原理

这是整个 AxVisor 的物理基础。不理解硬件机制,代码就只是一堆符号。下面用原理讲解 + AxVisor 代码对照的方式,把六大知识点全部讲透。


1.1 Exception Level 模型

原理

ARMv8-A 定义了 4 个特权等级,从低到高:

复制代码
┌─────────────────────────────────────────────────┐
│  EL3  │  Secure Monitor (ATF/TF-A)              │  ← 最高特权,安全世界切换
├───────┼─────────────────────────────────────────┤
│  EL2  │  Hypervisor (AxVisor 运行在这里)         │  ← 虚拟化控制
├───────┼─────────────────────────────────────────┤
│  EL1  │  Guest OS Kernel (Linux/ArceOS)          │  ← 操作系统内核
├───────┼─────────────────────────────────────────┤
│  EL0  │  Guest Userspace (应用程序)               │  ← 用户态
└───────┴─────────────────────────────────────────┘

关键设计思想:EL2 是"虚拟化专用层"。ARM 的虚拟化不像 x86 那样用 VMX root/non-root 两种模式,而是直接用 Exception Level 来分层。Guest OS 在 EL1 跑,以为自己就是最高特权的内核;Hypervisor 在 EL2 监控,拦截 Guest 的敏感操作。

为什么需要 EL2? 因为 EL1 的操作系统内核会执行特权指令(比如配置页表、操作中断控制器),这些指令如果不被拦截,Guest 就能直接控制物理硬件,破坏隔离性。EL2 通过配置 trap 位,让这些指令触发异常,交给 Hypervisor 处理。

代码对照

AxVisor 中 EL2 身份的体现------vCPU 的 init_hv() 设置 SPSR(Saved Program Status Register),决定 eret 后 Guest 回到哪个 EL:

复制代码
// arm_vcpu/src/vcpu.rs:160-167
fn init_hv(&mut self, config: Aarch64VCpuSetupConfig) {
    self.ctx.spsr = (SPSR_EL1::M::EL1h
        + SPSR_EL1::I::Masked
        + SPSR_EL1::F::Masked
        + SPSR_EL1::A::Masked
        + SPSR_EL1::D::Masked)
        .value;

逐字段解读:

  • M::EL1h = Guest 将运行在 EL1,使用 SP_EL1 作为栈指针(h = handler mode)
  • I::Masked = IRQ 中断屏蔽(Guest 启动时不希望被中断打断)
  • F::Masked = FIQ 中断屏蔽
  • A::Masked = SError 异步异常屏蔽
  • D::Masked = Debug 异常屏蔽

当 Hypervisor 执行 eret 指令时,CPU 会把 SPSR 的值恢复到 PSTATE,于是 CPU 降级到 EL1 开始执行 Guest 代码。


1.2 Stage-2 地址翻译

原理

这是虚拟化最核心的硬件机制。ARM 实现了两级地址翻译

复制代码
Guest 虚拟地址 (VA)
    │
    │  Stage-1 翻译 (Guest 自己的页表, TTBR0_EL1/TTBR1_EL1)
    │  Guest 以为这就是物理地址
    ▼
Guest 物理地址 (GPA / IPA)
    │
    │  Stage-2 翻译 (Hypervisor 的页表, VTTBR_EL2)
    │  由 Hypervisor 控制,Guest 完全不知道
    ▼
Host 物理地址 (HPA / PA)
    │
    ▼
  实际物理内存

关键寄存器

  • VTTBR_EL2(Virtualization Translation Table Base Register):指向 Stage-2 页表的根物理地址。每个 VM 有自己的 VTTBR_EL2 值。
  • VTCR_EL2(Virtualization Translation Control Register):控制 Stage-2 翻译的参数------IPA 空间大小、页大小、起始级别等。

页表级数:根据 IPA 空间大小不同,页表可以是 3 级或 4 级:

  • 3 级(SL0=1, T0SZ=25):IPA 空间 39 位 = 512GB
  • 4 级(SL0=0, T0SZ=16):IPA 空间 48 位 = 256TB

代码对照

arm_vcpuprobe_vtcr_support() 自动探测硬件支持并配置 VTCR:

复制代码
// arm_vcpu/src/vcpu.rs:444-463
fn probe_vtcr_support() -> u64 {
    let pa_bits = pa_bits();

    let mut val = match max_gpt_level(pa_bits) {
        4 => VTCR_EL2::SL0::Granule4KBLevel0 + VTCR_EL2::T0SZ.val(64 - 48),
        _ => VTCR_EL2::SL0::Granule4KBLevel1 + VTCR_EL2::T0SZ.val(64 - 39),
    };
    // ... PS 字段根据 pa_bits 设置

逻辑:先读 ID_AA64MMFR0_EL1.PARange 获取 CPU 支持的物理地址位数,如果 >= 44 位就用 4 级页表(覆盖 48 位 IPA),否则用 3 级(覆盖 39 位 IPA)。

然后 init_vm_context() 把 VTCR 加上缓存属性:

复制代码
// arm_vcpu/src/vcpu.rs:184-189
self.guest_system_regs.vtcr_el2 = probe_vtcr_support()
    + (VTCR_EL2::TG0::Granule4KB
        + VTCR_EL2::SH0::Inner
        + VTCR_EL2::ORGN0::NormalWBRAWA
        + VTCR_EL2::IRGN0::NormalWBRAWA)
        .value;
  • TG0::Granule4KB:使用 4KB 页粒度
  • SH0::Inner:页表 walk 使用 Inner Shareable
  • ORGN0/IRGN0::NormalWBRAWA:页表 walk 的缓存策略为 Write-Back Read-Allocate Write-Allocate

Stage-2 页表条目(PTE)的 Rust 实现在 axaddrspace 中:

复制代码
// axaddrspace/src/npt/arch/aarch64.rs
pub struct A64PTEHV(u64);

impl GenericPTE for A64PTEHV {
    fn new_page(paddr: HostPhysAddr, flags: MappingFlags, is_huge: bool) -> Self {
        let mut attr = DescriptorAttr::from(flags) | DescriptorAttr::AF;
        if !is_huge { attr |= DescriptorAttr::NON_BLOCK; }
        Self(attr.bits() | (paddr.as_usize() as u64 & Self::PHYS_ADDR_MASK))
    }
    fn new_table(paddr: HostPhysAddr) -> Self {
        let attr = DescriptorAttr::NON_BLOCK | DescriptorAttr::VALID;
        Self(attr.bits() | (paddr.as_usize() as u64 & Self::PHYS_ADDR_MASK))
    }

每个 PTE 是一个 64 位值,其中 bit[47:12] 存放物理地址,低位存放属性标志。new_table 创建指向下一级页表的条目,new_page 创建指向物理页的叶子条目。

页表级数由 feature flag 控制:

复制代码
// axaddrspace/src/npt/arch/aarch64.rs:217-228
impl PagingMetaData for A64HVPagingMetaData {
    #[cfg(not(feature = "4-level-ept"))]
    const LEVELS: usize = 3;
    #[cfg(feature = "4-level-ept")]
    const LEVELS: usize = 4;

    #[cfg(not(feature = "4-level-ept"))]
    const VA_MAX_BITS: usize = 40;
    #[cfg(feature = "4-level-ept")]
    const VA_MAX_BITS: usize = 48;

实战经验 :这正是我们之前 QEMU 6.2 出问题的地方------arm_vcpu 探测到 48 位 PA 选了 4 级页表,axaddrspace 也开了 4-level-ept feature 生成 4 级页表,但 QEMU 6.2 的 TCG 模拟器对 4 级 Stage-2 翻译有 bug。升级到 QEMU 9.0.2 后问题解决。


1.3 HCR_EL2:虚拟化控制总开关

原理

HCR_EL2(Hypervisor Configuration Register)是 EL2 最重要的控制寄存器,它决定了哪些 Guest 操作会被 trap 到 Hypervisor。可以把它想象成一个巨大的"开关面板",每个位控制一类操作的拦截行为。

名称 含义
0 VM Stage-2 地址翻译总开关。=1 启用,Guest 的所有内存访问都经过 Stage-2
3 FMO FIQ 路由。=1 物理 FIQ 路由到 EL2,同时启用虚拟 FIQ
4 IMO IRQ 路由。=1 物理 IRQ 路由到 EL2,同时启用虚拟 IRQ
19 TSC Trap SMC。=1 Guest 执行 SMC 指令会 trap 到 EL2
31 RW 执行状态。=1 EL1 使用 AArch64;=0 使用 AArch32

代码对照

复制代码
// arm_vcpu/src/vcpu.rs:191-203
let mut hcr_el2 =
    HCR_EL2::VM::Enable + HCR_EL2::TSC::EnableTrapEl1SmcToEl2 + HCR_EL2::RW::EL1IsAarch64;

if !config.passthrough_interrupt {
    hcr_el2 += HCR_EL2::IMO::EnableVirtualIRQ + HCR_EL2::FMO::EnableVirtualFIQ;
}

self.guest_system_regs.hcr_el2 = hcr_el2.into();

逐位解读:

  • VM::Enable:启用 Stage-2 翻译。这是虚拟化的核心------没有这个位,Guest 直接访问物理内存
  • TSC::EnableTrapEl1SmcToEl2:Guest 执行 SMC(Secure Monitor Call)时 trap 到 EL2。这样 Hypervisor 可以拦截 PSCI 调用(电源管理)
  • RW::EL1IsAarch64:Guest 运行 AArch64 模式
  • IMO + FMO(非直通模式时):物理中断路由到 EL2,Hypervisor 再通过虚拟中断注入给 Guest

两种中断模式的设计选择:

  1. passthrough_interrupt = false(默认):物理中断 → EL2 → Hypervisor 决定 → 虚拟中断注入 Guest。好处是 Hypervisor 完全掌控中断分发
  2. passthrough_interrupt = true:物理中断直接到 EL1 Guest。好处是低延迟,适合实时场景

1.4 异常向量表

原理

ARMv8 的异常向量表是一个 4×4 矩阵 ,按 [异常来源] × [异常类型] 组织:

复制代码
偏移    来源                    类型
─────────────────────────────────────
0x000   Current EL, SP_EL0     Synchronous
0x080   Current EL, SP_EL0     IRQ
0x100   Current EL, SP_EL0     FIQ
0x180   Current EL, SP_EL0     SError
0x200   Current EL, SP_ELx     Synchronous    ← Host 自身的同步异常
0x280   Current EL, SP_ELx     IRQ            ← Host 自身收到 IRQ
0x300   Current EL, SP_ELx     FIQ
0x380   Current EL, SP_ELx     SError
0x400   Lower EL, AArch64      Synchronous    ← Guest vmexit: 同步异常
0x480   Lower EL, AArch64      IRQ            ← Guest vmexit: 中断
0x500   Lower EL, AArch64      FIQ
0x580   Lower EL, AArch64      SError
0x600   Lower EL, AArch32      Synchronous
0x680   Lower EL, AArch32      IRQ
0x700   Lower EL, AArch32      FIQ
0x780   Lower EL, AArch32      SError

每个 entry 是 128 字节(32 条指令)。VBAR_EL2 寄存器指向这张表的基地址。

对于 Hypervisor 来说最重要的两个 entry 是

  • Lower EL AArch64 Synchronous(偏移 0x400):Guest 的同步异常,包括 Data Abort、HVC、SMC、System Register access 等
  • Lower EL AArch64 IRQ(偏移 0x480):Guest 执行期间收到物理中断

代码对照

exception.S 完美映射了这个 4×4 矩阵:

复制代码
; arm_vcpu/src/exception.S:104-131
.p2align 11
.global exception_vector_base_vcpu
exception_vector_base_vcpu:
    ; current EL, with SP_EL0     ← 第 1 行:不应该发生,全部 INVALID
    INVALID_EXCP_EL2 0 0
    INVALID_EXCP_EL2 1 0
    INVALID_EXCP_EL2 2 0
    INVALID_EXCP_EL2 3 0

    ; current EL, with SP_ELx     ← 第 2 行:Host 自身的异常
    HANDLE_CURRENT_SYNC             ; Hypervisor 自己的同步异常(bug → panic)
    HANDLE_CURRENT_IRQ              ; Hypervisor 收到中断(定时器等)
    INVALID_EXCP_EL2 2 1
    INVALID_EXCP_EL2 3 1

    ; lower EL, aarch64            ← 第 3 行:★ Guest 的 vmexit
    HANDLE_LOWER_SYNC_VCPU          ; Guest 同步异常 → vmexit_trampoline
    HANDLE_LOWER_IRQ_VCPU           ; Guest 中断 → vmexit_trampoline
    INVALID_EXCP_EL2 2 2
    INVALID_EXCP_EL2 3 2

    ; lower EL, aarch32            ← 第 4 行:不支持 AArch32 Guest
    INVALID_EXCP_EL2 0 3
    INVALID_EXCP_EL2 1 3
    INVALID_EXCP_EL2 2 3
    INVALID_EXCP_EL2 3 3

.p2align 11 确保向量表按 2048 字节(2^11)对齐------这是 ARM 架构的硬性要求。

Guest vmexit 时的处理路径(以同步异常为例):

复制代码
; arm_vcpu/src/exception.S:95-101
.macro HANDLE_LOWER_SYNC_VCPU
.p2align 7
    SAVE_REGS_FROM_EL1              ; 保存 Guest 的 x0-x30 + ELR + SPSR
    mov    x0, {exception_sync}     ; x0 = 0 (TrapKind::Synchronous)
    bl     vmexit_trampoline        ; 跳到蹦床函数,恢复 Host 栈
.endm

1.5 ESR_EL2 解码

原理

ESR_EL2(Exception Syndrome Register)在每次同步异常发生时由硬件自动填写,告诉 Hypervisor "发生了什么"。它是一个 32 位寄存器:

复制代码
┌──────┬────┬──────────────────────────┐
│ EC   │ IL │          ISS             │
│[31:26]│[25]│        [24:0]           │
└──────┴────┴──────────────────────────┘
  6 bit  1 bit       25 bits

EC (Exception Class):标识异常类型,常见值:

EC 含义 AxVisor handler
0x16 22 HVC64 指令 handle_psci_call / Hypercall
0x17 23 SMC64 指令 handle_smc64_exception
0x18 24 MRS/MSR trap handle_system_register
0x20 32 Instruction Abort from lower EL 未处理(导致我们之前的 panic)
0x24 36 Data Abort from lower EL handle_data_abort

ISS (Instruction Specific Syndrome):每种 EC 有不同的 ISS 格式。以 Data Abort 为例:

  • bit[5:0] = DFSC (Data Fault Status Code):翻译错误级别
  • bit[6] = WnR:0=读,1=写
  • bit[24:20] = SRT:源/目标寄存器编号
  • bit[22:21] = SAS:访问宽度(00=byte, 01=halfword, 10=word, 11=doubleword)

代码对照

handle_exception_sync() 就是按 EC 分发的路由器:

复制代码
// arm_vcpu/src/exception.rs:88-141
pub fn handle_exception_sync(ctx: &mut TrapFrame) -> AxResult<AxVCpuExitReason> {
    match exception_class() {
        Some(ESR_EL2::EC::Value::DataAbortLowerEL) => {     // EC=0x24
            // ... handle_data_abort
        }
        Some(ESR_EL2::EC::Value::HVC64) => {                 // EC=0x16
            // ... Hypercall / PSCI
        }
        Some(ESR_EL2::EC::Value::TrappedMsrMrs) => {         // EC=0x18
            handle_system_register(ctx)
        }
        Some(ESR_EL2::EC::Value::SMC64) => {                  // EC=0x17
            handle_smc64_exception(ctx)
        }
        _ => {
            panic!("handler not presents for EC_{} ...");      // 未处理的 EC
        }
    }
}

Data Abort 的 ISS 解析:

复制代码
// arm_vcpu/src/exception.rs:144-198
fn handle_data_abort(context_frame: &mut TrapFrame) -> AxResult<AxVCpuExitReason> {
    let addr = exception_fault_addr()?;           // HPFAR_EL2: 故障的 GPA
    let access_width = exception_data_abort_access_width();  // ISS.SAS
    let is_write = exception_data_abort_access_is_write();   // ISS.WnR
    let reg = exception_data_abort_access_reg();             // ISS.SRT
    // ...
    if is_write {
        return Ok(AxVCpuExitReason::MmioWrite { addr, width, data: ... });
    }
    Ok(AxVCpuExitReason::MmioRead { addr, width, reg, ... })
}

这里 Hypervisor 把硬件给的原始 ESR 信息翻译成了上层可以理解的 AxVCpuExitReason------这就是架构相关代码到架构无关代码的桥梁。


1.6 GIC 虚拟化

原理

GIC(Generic Interrupt Controller)是 ARM 的中断控制器。GICv3 架构如下:

复制代码
┌──────────────────────────────────────────────────┐
│                  Distributor (GICD)               │  ← 全局唯一,管理 SPI
├─────────┬─────────┬─────────┬──────────┬─────────┤
│ Redist 0│ Redist 1│ Redist 2│ Redist 3 │ ...     │  ← 每核一个,管理 SGI/PPI
├─────────┼─────────┼─────────┼──────────┼─────────┤
│ CPU IF 0│ CPU IF 1│ CPU IF 2│ CPU IF 3 │ ...     │  ← CPU Interface
└─────────┴─────────┴─────────┴──────────┴─────────┘

GICv3 的虚拟化支持:

  • Virtual CPU Interface:硬件提供虚拟中断寄存器(ICH_LR0~15),Hypervisor 往 List Register (LR) 写入虚拟中断号,硬件自动把中断呈现给 Guest
  • vGIC:当 Guest 访问 GICD/GICR 的 MMIO 地址时,Stage-2 翻译故障 trap 到 Hypervisor,由软件模拟的 vGIC 响应

中断注入的两种方式

  1. 硬件注入:往 ICH_LRn 写入中断号,CPU 退出 EL2 后硬件自动向 Guest 发送虚拟中断
  2. 软件模拟:Guest 读写 GIC 寄存器 → MMIO trap → vGIC 软件模拟 Distributor/Redistributor 行为

代码对照

AxVisor 的中断注入实现:

复制代码
// arm_vcpu/src/vcpu.rs:147-150
fn inject_interrupt(&mut self, vector: usize) -> AxResult {
    axvisor_api::arch::hardware_inject_virtual_interrupt(vector as u8);
    Ok(())
}

通过 axvisor_api 的回调机制调用到 AxVisor 的 HAL 层。HAL 层根据 GIC 版本选择注入方式(GICv2 用 GICH_LR,GICv3 用 ICH_LR)。


1.7 上下文保存/恢复:把所有硬件概念串联起来

GuestSystemRegisters:Guest 的完整硬件状态

context_frame.rs 中的 GuestSystemRegisters 结构体就是 Guest 在 EL1 运行时的全部硬件状态快照

复制代码
// arm_vcpu/src/context_frame.rs:151-211
pub struct GuestSystemRegisters {
    // ====== 定时器相关(Guest 的虚拟时钟)======
    pub cntvoff_el2: u64,     // 虚拟计数器偏移(让 Guest 看到"自己的"时间)
    cntp_cval_el0: u64,       // 物理定时器比较值
    cntv_cval_el0: u64,       // 虚拟定时器比较值
    pub cnthctl_el2: u64,     // 定时器 trap 控制(是否直通给 Guest)

    // ====== 虚拟化标识 ======
    pub vmpidr_el2: u64,      // Guest 看到的 CPU ID(MPIDR 的虚拟化版本)

    // ====== EL1 寄存器(Guest 内核状态)======
    pub sp_el0: u64,          // Guest 用户态栈指针
    sp_el1: u64,              // Guest 内核态栈指针
    elr_el1: u64,             // Guest 内核的异常返回地址
    pub sctlr_el1: u32,       // Guest 的系统控制寄存器(MMU、缓存开关等)
    ttbr0_el1: u64,           // Guest 的 Stage-1 页表基地址(用户空间)
    ttbr1_el1: u64,           // Guest 的 Stage-1 页表基地址(内核空间)
    tcr_el1: u64,             // Guest 的 Stage-1 翻译控制
    vbar_el1: u64,            // Guest 自己的异常向量表基地址

    // ====== Hypervisor 控制寄存器(EL2 层面)======
    pub hcr_el2: u64,         // ★ 虚拟化控制总开关(知识点 1.3)
    pub vttbr_el2: u64,       // ★ Stage-2 页表根地址(知识点 1.2)
    pub vtcr_el2: u64,        // ★ Stage-2 翻译控制(知识点 1.2)
}

每次 vmenter 前调用 restore() 把这些值写回硬件寄存器:

复制代码
// arm_vcpu/src/context_frame.rs:278-314
pub unsafe fn restore(&self) {
    asm!("msr CNTV_CVAL_EL0, {0}", in(reg) self.cntv_cval_el0);
    asm!("msr SCTLR_EL1, {0:x}", in(reg) self.sctlr_el1);
    asm!("msr TTBR0_EL1, {0}", in(reg) self.ttbr0_el1);
    asm!("msr TTBR1_EL1, {0}", in(reg) self.ttbr1_el1);
    asm!("msr VBAR_EL1, {0}", in(reg) self.vbar_el1);
    // ★ 关键:恢复 EL2 控制寄存器,切换到这个 VM 的地址空间
    asm!("msr VTCR_EL2, {0}", in(reg) self.vtcr_el2);
    asm!("msr VTTBR_EL2, {0}", in(reg) self.vttbr_el2);
    asm!("msr HCR_EL2, {0}", in(reg) self.hcr_el2);
    asm!("msr VMPIDR_EL2, {0}", in(reg) self.vmpidr_el2);
}

每次 vmexit 后调用 store() 把硬件寄存器保存回结构体,这样下次调度到另一个 VM 时,可以恢复那个 VM 的状态。


阶段 1 小结

硬件概念 核心寄存器 AxVisor 中的代码位置
Exception Level SPSR_EL2 vcpu.rs:init_hv() 设置 SPSR.M=EL1h
Stage-2 翻译 VTTBR_EL2, VTCR_EL2 vcpu.rs:probe_vtcr_support() + aarch64.rs:A64PTEHV
虚拟化控制 HCR_EL2 vcpu.rs:init_vm_context() 设置 VM/TSC/RW/IMO/FMO
异常向量 VBAR_EL2 exception.S:exception_vector_base_vcpu 4×4 矩阵
异常解码 ESR_EL2 exception.rs:handle_exception_sync() 按 EC 分发
GIC 虚拟化 ICH_LRn, HCR_EL2.IMO vcpu.rs:inject_interrupt() + HAL
上下文保存 所有 context_frame.rs:GuestSystemRegisters.store()/restore()
相关推荐
三品吉他手会点灯2 小时前
C语言学习笔记 - 5.C概述 - C的应用领域
c语言·笔记·学习
Wenzar_2 小时前
**发散创新:基于算子融合的深度学习推理优化实战**在现代AI推理场景中,模型性能瓶颈往往不是由单一算子决定的,而是多个连续算子之间数
java·人工智能·深度学习
360智汇云2 小时前
AI标注平台TLP:AI预标+人工精修,重塑数据标注效率
人工智能·深度学习·机器学习
深蓝海拓2 小时前
基于QtPy (PySide6) 的PLC-HMI工程项目(十一)框架的进一步完善:UI的自动周期更新以及下行数据的生成和处理
网络·笔记·python·学习·ui·plc
lkbhua莱克瓦242 小时前
记单词功能深度优化项目 - 业务需求开发文档(BRD+技术拆分版)
笔记
子午2 小时前
蔬菜识别~Python+深度学习+卷积网络算法+图像识别+2026原创+蔬菜识别
python·深度学习·算法
我是无敌小恐龙2 小时前
Java SE 零基础入门 Day02 运算符与流程控制超详细笔记
java·数据结构·spring boot·笔记·python·spring·spring cloud
三品吉他手会点灯2 小时前
C语言学习笔记 - 4.C概述 - C的特点
c语言·笔记·学习
CinzWS2 小时前
UVM验证环境构建:CPU验证的方法论——从零构建ARM A53验证帝国的艺术
arm开发·架构·芯片验证·原型验证·a53