Minos 虚拟化: ARMv8-A 虚拟化基础
本文介绍 Minos Hypervisor 虚拟化的基础知识。
1. EL 层级与异常模型
1.1 概念
ARMv8-A 定义 4 个异常等级(Exception Level):
| EL | 典型用途 |
|---|---|
| EL0 | 用户态(Apps) |
| EL1 | 操作系统内核(Guest Linux / Minos OS) |
| EL2 | Hypervisor(Minos 运行于此) |
| EL3 | Secure Monitor(固件/安全世界,本项目中不用) |
异常(Exception)发生的本质:处理器因事件暂停当前执行,切到更高或同级 EL,
从异常向量表取指执行。三个关键寄存器:
- SPSR_ELx:保存被中断时 PSTATE(含返回的 EL 编码、DAIF 中断屏蔽位、执行状态)
- ELR_ELx:保存被中断指令的返回地址(
eret时回到这里) - ESR_ELx:异常综合寄存器(Exception Syndrome Register),描述异常原因,
最关键的是 EC 字段(Exception Class),标识这是哪种异常(HVC/SVC/DABT/WFx…)
异常返回用 eret 指令:把 ELR_ELx → PC,把 SPSR_ELx → PSTATE。
1.2 Minos 代码位置
| 位置 | 内容 |
|---|---|
arch/aarch64/core/boot.S:76-91 |
CurrentEl 检测当前 EL;EL3 → panic;CONFIG_VIRT 下非 EL2 → panic |
arch/aarch64/core/boot.S:146-151 |
构造 SPSR_EL2 值 + ELR_EL2 后 eret 降级到 EL1 |
arch/aarch64/core/vector.S:71-76 |
异常入口把 SP_EL0 / SPSR_EL2 / ELR_EL2 压栈 |
arch/aarch64/core/aarch64_sync.c:87-92 |
bad_mode():未知模式 panic 出口 |
arch/aarch64/core/aarch64_sync.c:46-85 |
esr_class_str[]:ESR EC 字段 → 字符串的全表 |
1.3 精读 boot.S:76-151(当前 EL 检测与降级)
1 | mrs x0, CurrentEl // 读当前异常等级 |
要点:
- 启动时 Minos 已经由 U-Boot(跑在 EL3 或 EL2 上层)通过 PSCI 带到了 EL2
- 虚拟化必须从 EL2 开始,因为只有 EL2 才有
HCR_EL2.VM、Stage2 MMU 等能力
1 | drop_to_el1: |
要点:
- 非
CONFIG_VIRT构建(纯 OS 模式)时 Minos 会主动从 EL2 降级到 EL1 运行 AARCH64_SPSR_EL1h表示返回后处于 EL1 + SP_EL1(不是 SP_EL0)- F/I/A = 屏蔽 FIQ/IRQ/SError,确保进入 C 世界前中断全关
- 这是软件主动用
eret完成降级的经典写法
1.4 要点
- 异常 = 控制流切换,由硬件自动完成保存(写 SPSR/ELR),由软件决定处理
- Minos 必须运行在 EL2(虚拟化需要),这是 Type-1 Hypervisor 的基石
- ESR 的 EC 字段是异常分发的钥匙,贯穿整个 Phase 3(trap 机制)
eret是唯一从异常返回的指令,SPSR+ELR 配对使用
2. HCR_EL2:虚拟化控制寄存器
2.1 概念
HCR_EL2(Hypervisor Configuration Register)是 EL2 控制虚拟化的总开关。
它决定:哪些来自 EL1/EL0 的操作会被 trap(陷入) 到 EL2 由 Hypervisor 处理,
以及虚拟化的基本行为(VM 使能、中断路由等)。
2.2 Minos 代码位置
arch/aarch64/virt/arch_virt.c:187 arch_vcpu_state_init(),代码自带逐位中文注释,
是学习 HCR_EL2 最好的入门材料:
1 | /* |
2.3 关键位作用速查表
| 位 | 置位含义 | Minos 用法 |
|---|---|---|
| VM | 使能 Stage2 地址翻译(虚拟化核心开关) | 必置 |
| RW | EL1 使用 AArch64(0=AArch32) | 64 位 guest 必置 |
| TWI | trap 所有 WFI/WFE 指令 | 默认置位;NATIVE_WFI 的 VM 关闭 |
| TSC | trap SMC 指令 | 置位,SMC → EL2 处理 |
| IMO/FMO | 物理 IRQ/FIQ 路由到 EL2 | 置位(虚拟化中断的前提) |
| AMO | 物理 SError 路由到 EL2 | 置位 |
| TIDCP | trap IMPLEMENTATION DEFINED 功能 | 置位 |
| BSU_IS | barrier 指令共享属性升级 | 置位 |
| FB | TLB 维护指令强制广播 | 置位 |
| PTW | Stage1 页表访问被 trap(保护表遍历) | 置位 |
| TACR | trap 辅助控制寄存器 | 置位 |
2.4 要点
- HCR_EL2 是 per-VM 的——每个 vCPU 的
context->hcr_el2独立保存,
由 vmodule 机制在上下文切换时恢复(Phase 7) - 这些 trap 位决定了哪些指令会”掉进”
handle_vcpu_sync_exception()(Phase 3 精读) - trap 是虚拟化的根本手段:guest 想访问敏感资源 → 被 HCR trap 到 EL2 →
Hypervisor 模拟 → 返回 guest。HCR_EL2 就是这份”trap 清单”
3. Stage1 地址翻译
3.1 概念
地址翻译是把 CPU 看到的虚拟地址(VA)变成物理地址(PA)的过程。
ARMv8-A 支持两级:
- Stage1:VA → IPA(Intermediate Physical Address,中间物理地址)。
由 OS 自身管理(页表基址 TTBR0/TTBR1)。 - Stage2:IPA → PA。由 Hypervisor 管理(VTTBR_EL2)。
Stage1 需要 4 个控制寄存器配合:
| 寄存器 | 作用 |
|---|---|
| TTBR0/TTBR1 | 两级页表基址(TTBR0=低位 VA 空间,TTBR1=高位) |
| TCR | 翻译控制:T0SZ/T1SZ(VA 大小)、TGn(粒度)、PS(物理大小)等 |
| MAIR | 内存属性索引表:把页表描述符里的 AttrIdx 翻译成实际内存属性(Normal/Device/缓存策略) |
| SCTLR | 总开关(M=MMU 使能,C=dcache,I=icache) |
3.2 Minos 代码位置
| 位置 | 内容 |
|---|---|
arch/aarch64/core/boot.S:198-204 |
设置 TTBR0/TTBR1 指向 __stage1_page_table |
arch/aarch64/core/boot.S:206-221 |
设置 MAIR = 0xbbff440c0400(附注释:MT_* 属性表) |
arch/aarch64/core/boot.S:223-239 |
从 ID_AA64MMFR0_EL1 取 PA range,动态调整 TCR 的 IPS/PS 位 |
arch/aarch64/core/boot.S:302-315 |
SCTLR 置 M/C/I 位 → 开 MMU → br x26 跳到 mmu_on |
arch/aarch64/core/stage1.h |
全部 Stage1 描述符布局宏 |
arch/aarch64/core/stage1.c |
Stage1 页表建立/映射函数 |
3.3 精读 boot.S:198-239(页表基址 + MAIR + TCR)
1 | ldr x1, = __stage1_page_table |
MAIR 值 0xbbff440c0400 分解(每 8bit 一个 AttrIdx):
1 | AttrIdx0=0x00 Device-nGnRnE AttrIdx4=0x44 Normal, WB, Inner/Outer |
这与 stage1.h:51-56 的 MT_* 宏一一对应。
3.4 stage1.h 描述符要点
1 |
属性宏如 S1_PAGE_NORMAL:
1 |
3.5 要点
- MMU 开启前的关键动作:所有写系统寄存器必须用物理地址(
asm_vtop),
因为此时还没走翻译 - identity map 开 MMU:开 MMU 后 PC 不能变,必须保证当前 PC 在翻译前后
指向同一个物理地址,所以先 identity map,再跳到mmu_on - MAIR 是”属性字典”,页表描述符只存索引(AttrIdx),实际属性查 MAIR
- TCR 的 IPS 由硬件
ID_AA64MMFR0_EL1动态决定,保证跟 CPU 能力匹配
4. Stage2 地址翻译
4.1 概念
Stage2 是 Hypervisor 给 guest 用的第二层翻译:guest 的虚拟地址先过 Stage1
(guest 自己管)得 IPA,再过 Stage2(Hypervisor 管)得 PA。每个 VM 有独立的
Stage2 页表,通过 VMID 隔离。
| 寄存器 | 作用 |
|---|---|
| VTTBR_EL2 | Stage2 页表基址 + VMID(VMID 在 [63:48]) |
| VTCR_EL2 | Stage2 翻译控制(T0SZ=IPA 大小、SL0、TG0=粒度、PS=物理大小) |
| HCR_EL2.VM | Stage2 翻译的总开关(前面已讲) |
4.2 Minos 代码位置
| 位置 | 内容 |
|---|---|
arch/aarch64/virt/arch_virt.c:131-149 |
generate_vtcr_el2():40bit IPA、4K 粒度、1TB PA |
arch/aarch64/virt/arch_virt.c:151-155 |
generate_vttbr_el2():`base |
arch/aarch64/virt/stage2.h |
全部 Stage2 描述符/索引宏 |
arch/aarch64/virt/stage2.c(500 行) |
Stage2 映射/解映射/huge page/stage2_ipa_to_pa |
4.3 精读 arch_virt.c:131-161
1 | static inline uint64_t generate_vtcr_el2(void) |
要点:
- T0SZ=24 → 40bit IPA:guest 最多映射 1TB 的中间物理地址
- VMID << 48:VTTBR 高 16 位是 VMID,用于 TLB 中不同 VM 条目的隔离
- PS=1TB 表示物理地址空间是 1TB(对应 stage2.h 的
S2_PHYSICAL_SIZE)
4.4 stage2.h 与 stage1.h 的差异(易错点)
| 项 | Stage1 | Stage2 |
|---|---|---|
| AP 编码 | S1_AP_RW=0b00<<6(EL2 只用 AP[2]) |
S2_AP_RW=0b11<<6(两 bit 都要) |
| 共享属性 | 在描述符里(SH 字段) | 同样有 SH 字段 |
| 内存属性 | AttrIdx → 查 MAIR | 直接在描述符里写 MemAttr(S2_MEMATTR_*),不查 MAIR |
| 权限含义 | AP[2:1] 分 EL1/EL0 | S2_AP[1:0]:01=RO 10=WO 11=RW 00=无权限 |
1 |
注意:这就是之前踩过的坑(
LEARNING_PLAN.md中 Stage2 permission fault 修复)——
Stage2 缺 RW 权限时 default 分支给S2_AP_NON(无权限),guest 访问直接 fault。
4.5 分级索引
1 |
40bit IPA 用 4 级表(STAGE2_PGTABLE_LEVELS=3 表示从 PUD 开始,实际 walk 3 级)。
4.6 要点
- 两套翻译串行:guest VA –Stage1–> IPA –Stage2–> PA,都由硬件自动完成
- VMID 隔离是硬件级的:不同 VM 的同地址不会互相串扰(TLB 按 VMID 区分)
- Stage2 用 S2 自己的描述符格式,内存属性直接内嵌(不查 MAIR),AP 编码与 Stage1 不同
generate_vtcr_el2由 hypervisor 统一生成,所有 VM 用相同的 IPA 布局
5. 异常向量表与调用关系图
5.1 概念
ARMv8-A 的异常向量表是固定布局:基址(VBAR_EL2)+ 偏移。共 4 组 × 4 类型 = 16 入口,每个入口间隔 0x80 字节(128B)。
分组依据两个条件:
- 异常来自哪个 EL:当前 EL 还是更低 EL
- 使用的栈(仅当前 EL 相关):SP0(执行状态与当前 EL 不同的栈)还是 SPx
| 组 | 偏移 | 含义 |
|---|---|---|
| 0 | 0x000 | Current EL, SP0 |
| 1 | 0x200 | Current EL, SPx |
| 2 | 0x400 | Lower EL, AArch64 |
| 3 | 0x600 | Lower EL, AArch32 |
每组内再按异常类型分 4 个:
| 类型 | 偏移 |
|---|---|
| Synchronous(同步异常:DABT/IABT/HVC/SVC…) | +0x000 |
| IRQ(普通中断) | +0x080 |
| FIQ(快速中断) | +0x100 |
| SError(异步系统错误) | +0x180 |
5.2 Minos 代码位置
arch/aarch64/core/entry.S:36-84:16 入口布局(elx_vectors)arch/aarch64/core/vector.S:各入口的具体处理(寄存器保存/跳 C handler)arch/aarch64/core/boot.S:173-174:msr VBAR_EL2, elx_vectors设置向量表基址
5.3 精读 entry.S(向量表布局)
1 | .global elx_vectors |
要点:
BAD_MODE宏(entry.S:30-34)把 mode 号放 x29 后跳__bad_mode→bad_mode()panic- 虚拟化相关的真正处理在:cxsync/cxirq/l64sync/l64irq/l32sync/l32irq
5.4 vector.S 的关键宏
1 | .macro __SAVE_GP_REGS /* 异常入口:把 31 个通用寄存器 + SP0/SPSR/ELR 压栈 */ |
这个压栈布局就是 gp_regs 结构(include/minos/gp_regs.h),C 侧 handler 拿到的参数。
5.5 ★ 异常调用关系图(Phase 1 验证目标)
1 | guest EL1 / minos EL1 发生异常 |
C 侧 handler 对照表(trap.c / aarch64_sync.c):
| EC(异常类) | handler(guest 路径,trap.c:344-365) | handler(当前EL路径,aarch64_sync.c:112-114) |
|---|---|---|
| WFx (WFI/WFE) | wfi_wfe_handler → vcpu_idle |
— |
| HVC64/HVC32 | aarch64_hypercall_handler |
SVC64 → 调度 |
| SMC64/SMC32 | aarch64_smccall_handler |
— |
| SYS64 (MSR/MRS) | access_system_reg_handler(模拟系统寄存器) |
— |
| DABT_LOW | dataabort_tfl_handler(MMIO 模拟入口) |
kernel_mem_fault |
| IABT_LOW | insabort_tfl_handler |
kernel_mem_fault |
| 其他 | unknown_handler 等 |
unknown_trap_handler |
5.6 要点
- 向量表布局是硬性规定:必须 0x1000 对齐,16 入口各 0x80 间隔,不能错位
- 异常进入时由汇编负责保存全部寄存器(gp_regs),C 只负责处理,返回时汇编恢复
- guest 的同步异常走
handle_vcpu_sync_exception()(trap.c:392),
按 EC 查表分发给 22 个 handler——这就是 Phase 3 的主角 dataabort_tfl_handler(trap.c:241)是设备模拟(MMIO)的唯一入口,
guest 访问模拟设备地址 → Stage2 fault → 这里 →vdev_mmio_emulation
6. GICv3 中断虚拟化
6.1 概念
GIC(Generic Interrupt Controller)v3 是 ARM 标准中断控制器。核心概念:
| 概念 | 说明 |
|---|---|
| GICD | Distributor(分发器),全局控制 SPI 等 |
| GICR | Redistributor(重分发器),per-CPU,控制 PPI/SGI,含 GICR_SGI(SGI 触发) |
| *ICC_(CPU 接口) | 系统寄存器(ICC_IAR1/EOIR1/SGI1R…),per-CPU |
| IRQ 类型 | SGI(0-15) 核间中断 / PPI(16-31) 私有外设中断 / SPI(32+) 共享外设中断 |
| LR(List Register) | ICH_LR0-15:硬件把”要注入给 guest 的中断”放这里,guest 直接响应 |
| 优先级 | 数值越小优先级越高,GICD/GICR_IPRIORITYR 配置 |
6.2 Minos 代码位置
物理 GIC(hypervisor 自己用的):
drivers/irq-chips/gicv3.c(548 行)gicv3_init:GICD/GICR 初始化gicv3_read_irq(70):ICC_IAR1_EL1读中断号gicv3_eoi_irq(58):ICC_EOIR1_EL1结束中断gicv3_set_irq_priority(129):GICD/GICR_IPRIORITYRgicv3_set_irq_affinity(143):GICD_IROUTER__gicv3_send_sgi_list(157):ICC_SGI1R 发 SGI
虚拟 GIC(给 guest 的):
virt/virq_chips/vgicv3.c(1075 行)gicv3_send_virq:把 virq 写入 ICH_LR(硬件注入路径)vgic_mmio_handler(416):GICD/GICR 的 MMIO 读写模拟(软件路径)gicv3_state_save/restore:切换时保存/恢复 LRs + APR + VMCR
6.3 两种虚拟化路径(Phase 5 的预习)
路径 A:硬件注入(快)
1 | host 收到物理 IRQ → gicv3_send_virq() 把 vINTID 写入 ICH_LR |
路径 B:软件模拟(MMIO trap)
1 | guest 访问 GICD_* / GICR_* 地址 → Stage2 fault → dataabort_tfl_handler |
vgicv3.c:221-283 里能看到 GICD_ISENABLER/ICENABLER/IPRIORITYR/ICFGR 等的模拟。
6.4 要点
- LR 是硬件虚拟化的核心:Hypervisor 把中断”排队”进 LR,硬件替它注入给 guest
- GIC 虚拟化 = 物理中断转发(LR)+ 控制寄存器模拟(MMIO)两条腿
- vGIC 是 vmodule 之一:进出 guest 时
state_save/restore保存 LR/APR/VMCR(Phase 7) - 物理 GIC(hypervisor 自己用)和虚拟 GIC(guest 用)是两套独立实现
7. Generic Timer 定时器虚拟化
7.1 概念
ARM generic timer 提供:
- CNTP:物理计数器/定时器(EL1 可见,hypervisor 自己的时间基准)
- CNTV:虚拟计数器/定时器(guest 用的”虚拟时间”)
虚拟化关键机制:
- CNTVOFF_EL2:虚拟时间偏移。
CNTVCT = 物理时间 - CNTVOFF。
Hypervisor 可通过改 CNTVOFF 给每个 guest 制造不同的虚拟时间 - CNTV_CVAL_EL0:比较值。
CNTVCT >= CNTV_CVAL时触发虚拟定时器中断 - CNTV_CTL_EL0:控制(ENABLE / IMASK / ISTATUS)
- trap:guest 访问 CNTP 系列寄存器被 trap 到 EL2,由 hypervisor 用软件 timer 模拟
7.2 Minos 代码位置
arch/aarch64/virt/vtimer.c(341 行):
vtimer_state_restore(97):写CNTVOFF_EL2(104) +CNTV_CVAL_EL0(105) +CNTV_CTL_EL0(106)vtimer_state_save(110):读 CNTV_CVAL/CTL 存到 context,然后清零 CNTV_CTLphys_timer_expire_function(73):物理定时器到期 →send_virq_to_vcpuvirt_timer_expire_function(84):虚拟定时器到期 →wake()唤醒 vCPU- 虚拟定时器通过
core/timer.c的软件定时器机制模拟
7.3 精读 vtimer.c:97-120
1 | static void vtimer_state_restore(struct vcpu *vcpu, void *context) |
7.4 要点
- **CNTVOFF 实现”虚拟时间”**:给 guest 一个可偏移的时间视图(可以加速/倒流)
- vtimer 是 vmodule:进出 guest 时保存/恢复 CNTV_CVAL/CTL/CNTVOFF
- 物理定时器(CNTP)被 hypervisor 自己占用;guest 的虚拟定时器用软件 timer 模拟
- 虚拟定时器到期走
virt_timer_expire_function→wake(),等 vCPU 被调度回来
再通过硬件 CNTV 重新触发,避免在 EL2 里直接注入
8. Barrier / TLB / Cache
8.1 概念
内存屏障(Memory Barrier)解决多核/内存系统的顺序问题:
| 指令 | 全称 | 作用 |
|---|---|---|
| DSB | Data Synchronization Barrier | 等所有之前的访存/维护指令完成(最严格) |
| DMB | Data Memory Barrier | 保证之前访存按序,但不等待完成 |
| ISB | Instruction Synchronization Barrier | 刷流水线,保证之后指令能看到之前的系统寄存器/页表修改 |
TLB(Translation Lookaside Buffer):地址翻译缓存。修改页表后必须失效对应 TLB,
否则旧映射残留。
Cache 维护指令:ic(指令缓存)、dc(数据缓存),配合 set/way 或 by-VA 操作。
8.2 Minos 代码位置
| 位置 | 内容 |
|---|---|
arch/aarch64/include/asm/barrier.h |
isb/dmb/dsb 及全套 mb/rmb/wmb/smp_* 变体 |
arch/aarch64/core/boot.S:181-191 |
inv_dcache_all + tlbi alle2/vmalle1 |
arch/aarch64/core/cache.S |
flush_dcache_range / inv_dcache_range / cache 维护宏 |
arch/aarch64/core/boot.S:318-320 |
ic ialluis(开 MMU 后失效 icache) |
8.3 barrier.h 精读
1 |
选项说明:
sy:全系统(full system)ld/st:只等读/只等写ish/ishld/ishst:inner-shareable(多核一致域内)
8.4 要点
- 修改系统寄存器/页表后必须 isb/dsb:boot.S 里几乎每个
msr后面都跟dsb + isb - TLB 失效范围:
tlbi alle2(EL2 全部)/vmalle1(EL1 全部)/ 按 VMID 失效 - guest 修改自己的 Stage1 页表,由 guest 自己管 TLB;Hypervisor 改 Stage2 才需要刷
- 共享内存通信(vmbox ring)依赖
mb()/wmb()/rmb()保证双端可见性
9. Phase 1 验证清单
对应
LEARNING_PLAN.mdPhase 1 checklist。
- EL0-EL3、异常模型、SPSR/ELR/ESR —— 见本笔记第 1 节
- HCR_EL2 各位作用 —— 见第 2 节(
arch_virt.c:187注释) - 地址翻译:TTBR/TCR/MAIR、Stage1 4 级 —— 见第 3 节
- Stage2:VTTBR_EL2、VTCR_EL2、VMID、IPA —— 见第 4 节
- 异常向量表布局(4 组 × 4 类型)—— 见第 5 节
- 画出”异常向量表 → 处理函数”调用关系图 —— 见第 5.5 节
- GICv3:GICD/GICR/LR/优先级 —— 见第 6 节
- ARM generic timer:CNTVOFF/CNTV_CTL/CVAL —— 见第 7 节
- barrier/TLB/cache 指令 —— 见第 8 节
动手建议(可加深理解)
- 用 gdb 连 QEMU,在
handle_vcpu_sync_exception()(trap.c:392)打断点,
观察 guest 触发一个 HVC 后栈上的gp_regs - 在 guest 里跑
sleep 1,在vtimer_state_restore(vtimer.c:97)打断点,
观察 CNTVOFF 的值 - 阅读
boot.S时对照asm-offset.c理解PCPU_*偏移量来源
附:核心寄存器速查表
| 寄存器 | 一句话作用 | 精读位置 |
|---|---|---|
SPSR_EL2 |
保存被中断的 PSTATE | boot.S:149, vector.S:73 |
ELR_EL2 |
异常返回地址 | boot.S:148, vector.S:75 |
ESR_EL2 |
异常原因(EC 字段=分发钥匙) | aarch64_sync.c:137, trap.c:403 |
VBAR_EL2 |
异常向量表基址 | boot.S:174, entry.S |
HCR_EL2 |
虚拟化总开关(trap 清单) | arch_virt.c:187 |
TTBR0/1_EL2 |
Stage1 页表基址 | boot.S:201-202 |
MAIR_EL2 |
内存属性表 | boot.S:221 |
TCR_EL2 |
Stage1 翻译控制 | boot.S:237 |
VTTBR_EL2 |
Stage2 页表基址 + VMID | arch_virt.c:151 |
VTCR_EL2 |
Stage2 翻译控制(IPA 宽度/粒度) | arch_virt.c:131 |
CNTVOFF_EL2 |
虚拟时间偏移 | vtimer.c:104 |
ICH_LR0-15 |
注入给 guest 的中断 | vgicv3.c:583 |
ICC_IAR1/EOIR1 |
CPU 接口:ack/eoi | gicv3.c:58,70 |