Minos 虚拟化: 异常与 trap 机制
本文介绍 Minos Hypervisor 的 trap 机制。
0. 全景图
一条铁律:一切虚拟化都是 trap 出来的——guest 想碰敏感资源 → 被 trap 到 EL2 → hypervisor 模拟/处理 → 返回。这条路径就是 hypervisor 的”生命线”。
1. 背景
源码位置:
arch/aarch64/virt/arch_virt.c:187-227
在讲异常入口之前,先回答一个根本问题:guest 好好地跑在 EL1,为什么突然陷入 EL2?
答案在 HCR_EL2(Phase 1 学过)。它是 EL2 控制虚拟化的”总开关”,其中各种 T* 陷阱位决定”EL1 执行哪些操作会陷入 EL2”。hypervisor 在创建 vCPU 时配置它(arch_virt.c:187):
1 | context->hcr_el2 = value | HCR_EL2_VM | HCR_EL2_TIDCP | HCR_EL2_IMO | |
guest 触发 trap 的完整因果链:
1 | hypervisor 设 HCR_EL2.xxx = 1("我要拦这个操作") |
关键点:
- **HCR_EL2 陷阱位 = 虚拟化的”拦截清单”**——hypervisor 想拦什么就置哪一位。
- guest 触发陷阱 → 硬件写 ESR_EL2——ESR 是”为什么 trap”的记录,正是本 phase 第 4 节解密的对象。
- HCR_EL2 是每 vCPU 一份:
context->hcr_el2保存在 vCPU 上下文里,由 vmodule 在切换时恢复(Phase 7 详讲)。 - 不是所有 guest 操作都会 trap:只有 HCR_EL2 置位的那几类才陷,其余 guest 正常在 EL1 跑(这就是虚拟化性能的关键——能直通就直通,必要才 trap)。
2. 异常入口
源码位置:
arch/aarch64/core/vector.S
2.1 入口路径
上一 phase 学过向量表(entry.S)16 个入口。guest 触发的异常走 Lower EL 组:
1 | l64sync: // Lower EL using AArch64,同步异常 |
2.2 SAVE_GP_REGS
异常发生时,CPU 硬件只自动做了三件事(写 ELR_EL2/SPSR_EL2,跳到向量表),其余寄存器全部要汇编手动保存:
1 | .macro __SAVE_GP_REGS |
压栈顺序 → gp_regs 结构。压栈的布局正好对应 tcb.h:6 的 struct aarch64_regs:
1 | struct aarch64_regs { // gp_regs,就是栈上的布局! |
关键理解:gp_regs 不是”结构体内存”,它就是异常入口在栈上压出来的那串值。C 侧的 handler 拿到 gp_regs *regs,就是在读栈顶的这份现场。
2.3 LOAD_GP_REGS
顺序与保存相反,最后 eret:
1 | ldr x0, [sp], #8 |
2.4 关键知识点
- 异常 = 现场保存 + 分发 + 现场恢复:硬件只自动保存 ELR/SPSR,其余全靠汇编。
gp_regs就是栈布局:理解它 = 理解异常入口压了什么。eret是唯一返回指令:eret= PC←ELR_EL2,PSTATE←SPSR_EL2。
3. 同步异常分发
源码位置:
arch/aarch64/core/aarch64_sync.c
3.1 从 lower EL 进入
完整链路:向量表 l64sync → 汇编入口 __sync_exception_from_lower_el(vector.S:160)→ C 函数 sync_exception_from_lower_el(aarch64_sync.c:156)。
3.1.1 汇编入口:__sync_exception_from_lower_el
1 | vfunc __sync_exception_from_lower_el |
每步作用:
| 步 | 动作 | 作用 |
|---|---|---|
| ① | SAVE_GP_REGS |
把 x0-x30 / SP_EL0 / SPSR / ELR 压栈,形成 gp_regs(见 2.2) |
| ② | PCPU_LOAD_CURRENT_TASK |
从本核 pcpu 取出当前任务 → x18(guest 可能破坏 x18,必须重新加载) |
| ③ | task_exit_from_user |
退出钩子:vcpu->mode: IN_GUEST_MODE → IN_ROOT_MODE,触发 OS_HOOK_EXIT_FROM_GUEST |
| ④⑤ | x0=sp; bl sync_exception_from_lower_el |
核心:把 gp_regs 传给 C,C 侧做身份判断与分发(见 3.1.2) |
| ⑥⑦ | x0=sp; bl task_return_to_user |
进入钩子:vcpu->mode: IN_ROOT_MODE → IN_GUEST_MODE,触发 OS_HOOK_ENTER_TO_GUEST(中断注入落点) |
| ⑧⑨ | LOAD_GP_REGS; eret |
恢复现场,跳回 guest 继续执行 |
关键:③⑦ 两个钩子就是虚拟化的”进出开关”——vcpu->mode 在 IN_GUEST_MODE ↔ IN_ROOT_MODE 间切换,各模块(vGIC/vtimer)通过 OS_HOOK_EXIT_FROM_GUEST / OS_HOOK_ENTER_TO_GUEST 在进出时干活(Phase 1 学的”进 guest 前注入中断”就发生在 ⑦)。
3.1.2 C 分发函数:sync_exception_from_lower_el
1 | void sync_exception_from_lower_el(gp_regs *regs) |
判断分叉的两个条件:
current->flags & TASK_FLAGS_VCPU:当前任务是不是 vCPU(guest 跑在 EL1 时,当前 task 就是 vCPU)HCR_EL2.TGE:TGE=1 表示”当前在跑 hypervisor 自己的任务”(不用虚拟化),TGE=0 表示在跑 guest
两者都满足 vCPU 条件 → handle_vcpu_sync_exception(trap.c:392),这是 Phase 3 的核心分发函数。
3.2 从当前 EL 进入
完整链路:向量表 cxsync → 汇编入口 __sync_exception_from_current_el(vector.S:141)→ C 函数 sync_exception_from_current_el(aarch64_sync.c:151)。
和 3.1(lower EL)相比,这是”当前 EL 自己出异常”,有三个关键差异:
① 没有进出钩子(不用切 vCPU 状态);② 有 SVC 调度特判;③ 查的是另一张表process_sync_descs[]。
3.2.1 汇编入口:__sync_exception_from_current_el
1 | vfunc __sync_exception_from_current_el |
每步作用:
| 步 | 动作 | 作用 |
|---|---|---|
| ① | SAVE_GP_REGS |
压现场(与 3.1 相同) |
| ② | str x0, [x18, #TASK_STACK_OFFSET] |
把栈指针存进当前任务——因为后面可能触发调度,任务被切走前要保存栈 |
| ③④ | 读 ESR、取 EC、判断 SVC64 | SVC 特判:SVC 是”主动请求”(调度/系统调用),不是错误 |
| ⑤ | sync_exception_from_current_el |
SVC 以外的异常 → C 处理(多是 panic,见 3.2.2) |
| ⑥ | exception_return |
和 3.1 的 LOAD_GP_REGS + eret 不同:走 exception_return,它会先检查是否需要重新调度(vector.S:120) |
为什么 SVC 要特判?——SVC 是”主动请求”,不是”错误”
这里容易困惑:既然 EC≠SVC64 才进 C handler(大多 panic),那 SVC 去了哪?答案是它被当成”合法的主动调用”,直接走调度/返回路径,不进 panic handler。
SVC(Supervisor Call)是 AArch64 的”软中断/系统调用”指令,用途远比”调度”多。对照 Linux 就很好理解——Linux 里 SVC 几乎无处不在:
| SVC 用途 | 谁在用 | 说明 |
|---|---|---|
| 系统调用(syscall) | 用户态进程 | EL0 执行 svc #0 进入内核,Linux 靠它实现 read/write/open 等几百个 syscall |
| 主动让出 CPU(schedule) | 内核/任务 | 内核主动请求调度(minos 里 sched() 就是靠 SVC 触发) |
| 唤醒/睡眠 | 内核 | 任务主动进入睡眠或唤醒别的任务 |
**所以 SVC 的”语义”是”我是来请求的,不是出错了”**——系统调用、调度请求都靠它。
minos 为什么这里只关心 SVC 而不细分? 因为 minos 是 micro-kernel,它的”系统调用”机制很简单:
- SVC 进入 current EL 异常后 → minos 判断 EC==SVC64 → 不当作错误 → 走
exception_return(它会检查是否该重新调度) - 具体的”请求内容”(要干什么)在
exception_return_handler里处理,不是在异常 handler 里
换句话说:**
__sync_exception_from_current_el只负责把 SVC 和”真错误”分开**,真正的 SVC 处理(调度等)在exception_return路径里。这是 minos 把”异常入口”和”调度入口”合并的巧妙设计——SVC 主动调度请求复用异常返回路径,省了一套专门的调度入口代码。
对比 Linux 理解:Linux 的 el0_svc / el1_svc 异常向量专门处理系统调用,有完整的 syscall 表分发(几百个);**minos 精简到只判断”是 SVC 就放行到返回路径”**,因为它不需要支持那么多系统调用——它的”系统调用”主要就是调度。
3.2.2 C 处理:sync_exception_from_current_el → handle_sync_exception
1 | static void handle_sync_exception(gp_regs *regs) |
process_sync_descs(aarch64_sync.c:116-120)只有几个条目:
1 | static struct sync_desc *process_sync_descs[] = { |
本质:hypervisor 自己的异常基本全是 bug → 直接 panic(kernel_mem_fault / unknown_trap_handler)。
3.2.3 与 3.1 的差异对照
| 维度 | 3.1 lower EL(guest) | 3.2 current EL(hypervisor 自己) |
|---|---|---|
| 入口 | l64sync/l32sync |
cxsync |
| 汇编入口 | __sync_exception_from_lower_el |
__sync_exception_from_current_el |
| 进出钩子 | 有(task_exit/return_to_user) | 无(不切 vCPU 状态) |
| SVC 特判 | 无 | 有(SVC → 调度路径) |
| 返回方式 | LOAD_GP_REGS + eret |
exception_return(会检查重调度) |
| C 分发 | handle_vcpu_sync_exception(按 vCPU 判断) |
handle_sync_exception(直接查表) |
| 查的表 | guest_sync_descs[](trap.c) |
process_sync_descs[](aarch64_sync.c) |
| 处理结果 | 模拟/注入/返回 | 基本全是 panic |
3.3 关键知识点
- 同一个异常入口,两种身份:vCPU(guest) vs 普通任务(hypervisor 自己),靠
TASK_FLAGS_VCPU+HCR_EL2.TGE区分。 - **guest 的一切同步异常最终都汇到
handle_vcpu_sync_exception**。 - current EL 异常基本是 bug → panic:
process_sync_descs[]只有 unknown/kernel_ia/kernel_da 三个 panic 条目。 - SVC 是”主动请求”不是错误:current EL 的 SVC 走调度路径(
exception_return),不进 panic handler。
4. trap.c:ESR 解码与 22 个 handler
源码位置:
arch/aarch64/virt/trap.c
4.1 ESR_EL2 结构:EC / ISS 字段
ESR_EL2(Exception Syndrome Register)描述异常原因,关键两段:
1 | ESR_EL2 (64-bit) |
- EC(Exception Class):6 位,这是分发的钥匙。比如 0x18=SYS64、0x01=WFx、0x24=DABT_LOW、0x16=HVC64。
- ISS(Instruction Specific Syndrome):25 位,内容依赖 EC。比如 SYS64 的 ISS 编码了”是哪个系统寄存器、读还是写”;DABT 的 ISS 编码了”读写方向、目标寄存器、fault 状态码”。
宏定义(aarch64_common.h):
1 |
4.1.1 SYS64(EC=0x18)的 ISS 细分:esr_sysreg
EC=0x18 时,ISS 的 25 位不再是”综合信息”,而是系统寄存器访问的完整编码。minos 用一个位域结构体直接解码:
1 | struct esr_sysreg { // trap.h:104 |
**核心:(op0, op1, CRn, CRm, op2) 五元组唯一确定”访问了哪个系统寄存器”**,这就是 ARMv8 的系统寄存器地址编码:
1 | 系统寄存器编码 = op0:op1:CRn:CRm:op2 |
| 字段 | 位宽 | 位 | 含义 |
|---|---|---|---|
read |
1 | 0 | 方向:0=写系统寄存器(MSR),1=读系统寄存器(MRS) |
crm |
4 | 1-4 | CRm:与 CRn 组合,细分寄存器功能(如定时器组 CRm=2 是物理、CRm=3 是虚拟) |
reg |
5 | 5-9 | Rt:MSR 的源寄存器 / MRS 的目标寄存器(x0-x31) |
crn |
4 | 10-13 | CRn:寄存器功能大类(如 14=定时器、12=GIC、1=控制寄存器) |
op1 |
3 | 14-16 | Op1:所属异常级别(0=EL1、3=EL0、4=EL2、6=EL3) |
op2 |
3 | 17-19 | Op2:最细粒度区分(定时器组 op2=1 是 CTL、op2=2 是 CVAL) |
op0 |
2 | 20-21 | Op0:命名空间索引,标准系统寄存器恒为 3 |
res0 |
3 | 22-24 | 保留,恒为 0 |
len |
1 | 25 | IL:指令长度,0=32 位指令,1=64 位指令 |
ec |
6 | 26-31 | EC:固定为 0x18(SYS64) |
五元组 (op0, op1, CRn, CRm, op2) 即系统寄存器地址编码,如 CNTP_CTL_EL0 = op0:3, op1:3(EL0), CRn:14(定时器), CRm:2(物理), op2:1(CTL)。
举例(access_system_reg_handler 里 switch 的常见寄存器编码):
| 寄存器 | op0 | op1 | CRn | CRm | op2 |
|---|---|---|---|---|---|
CNTPCT_EL0 |
3 | 3 | 14 | 0 | 1 |
CNTP_CTL_EL0 |
3 | 3 | 14 | 2 | 1 |
CNTP_CVAL_EL0 |
3 | 3 | 14 | 2 | 2 |
ICC_SGI1R_EL1 |
3 | 0 | 12 | 11 | 5 |
minos 中的用法(trap.c:143-190):
1 | struct esr_sysreg *sysreg = (struct esr_sysreg *)&esr_value; // 直接用位域解码 |
即:读系统寄存器(MRS)时,hypervisor 模拟出值再写回 Rt;写系统寄存器(MSR)时,从 Rt 取出要写的值交给对应的模拟函数——方向由 read 位区分,目标寄存器由 reg(Rt)指出。
4.2 分发表:guest_sync_descs[]
**这是虚拟化的”分诊台”**——按 EC 索引,选对应 handler:
1 | static struct sync_desc *guest_sync_descs[] = { |
每个描述符由宏 DEFINE_SYNC_DESC 生成(trap.c:344-365),含 handler 函数指针和 ret_addr_adjust(返回地址调整量)。
4.3 分发函数:handle_vcpu_sync_exception
1 | void handle_vcpu_sync_exception(gp_regs *regs) |
④ 为什么 regs->pc += ret_addr_adjust? 不同异常下,ELR_EL2 保存的返回地址语义不同,需要按需调整避免死循环/重复执行:
| EC | ret_addr_adjust |
原因 |
|---|---|---|
| HVC / SVC / SMC | 0 | ARM 硬件规定:HVC/SVC/SMC 异常时 ELR 已指向下一条指令,返回后自然从下一条继续,无需调整 |
| WFI / WFE | 4 | 硬件把 ELR 设为 WFI 指令本身,若不跳过,eret 后会再次执行 WFI → 死循环 |
| DABT(数据异常) | 4 | MMIO 模拟完成后,跳过这条导致异常的内存访问指令(不重放) |
对照源码(trap.c:344-365):DEFINE_SYNC_DESC(..., wfi_wfe_handler, 1, 4) 最后一个参数就是 ret_addr_adjust;
HVC64 是 ..., aarch64_hypercall_handler, 1, 0)。
4.4 22 个 handler 的完整映射
| EC | 值 | handler | 触发场景 |
|---|---|---|---|
| WFx | 0x01 | wfi_wfe_handler |
guest 执行 WFI/WFE |
| CP15_32 | 0x03 | mcr_mrc_cp15_handler |
AArch32 协处理器访问 |
| SYS64 | 0x18 | access_system_reg_handler |
系统寄存器访问(关键) |
| DABT_LOW | 0x24 | dataabort_tfl_handler |
内存访问故障(MMIO 入口,关键) |
| IABT_LOW | 0x20 | insabort_tfl_handler |
指令取指故障 |
| HVC64 | 0x16 | aarch64_hypercall_handler |
hypercall |
| SMC64 | 0x17 | aarch64_smccall_handler |
SMC 调用 |
| SERROR | 0x2F | serror_handler |
SError 异步错误 |
| … | … | … | (其余为 bad_mode/panic 类) |
4.5 关键知识点
- ESR_EL2 的 EC 字段是分发的钥匙,一个 6 位查表搞定 22 类异常。
regs->pc += ret_addr_adjust防止重复执行——这是每个 hypervisor 都要处理的坑。- 本节的”主角”是 SYS64(系统寄存器模拟) 和 DABT_LOW(MMIO 模拟),下面两节展开。
5. 重点 handler 1:access_system_reg_handler
源码位置:
trap.c:143-191
5.1 场景
guest 访问一个被 HCR_EL2 或 CPTR_EL2 设置成”trap”的系统寄存器(比如 CNTP_*、ICC_SGI1R),会以 EC=SYS64 陷入。此时 ESR 的 ISS 字段编码了是哪个寄存器、读还是写:
1 | struct esr_sysreg *sysreg = (struct esr_sysreg *)&esr_value; |
5.2 处理流程
1 | switch (reg_name) { |
5.3 关键知识点
- SYS64 trap 的本质:guest 想”碰”一个敏感寄存器 → hypervisor 用软件模拟它(这就是你 Phase 1 学的”CNTP 被 trap 软件模拟”的代码实现)。
- ISS 字段解码:从 esr 里解析出”哪个寄存器、读/写”,再分发给对应模拟器。
- 后续延伸:
phy_timer_trap(vtimer.c:63)的具体模拟逻辑 → Phase 6 定时器虚拟化;sgi1r_el1_trap→ Phase 5 vGIC。本节只讲 trap 与分发。
6. 重点 handler 2:dataabort_tfl_handler
源码位置:
trap.c:241-294
6.1 场景
guest 访问一段没有 Stage2 映射的内存(如 MMIO 设备地址)→ 数据异常陷入。这是设备虚拟化的总入口(你 Phase 1 学过 vGIC 的 GICD/GICR 模拟、Phase 5 会学 vdev,都从这里进)。
6.2 处理流程
1 | int dataabort_tfl_handler(gp_regs *regs, int ec, uint32_t esr_value) |
⑤ 地址翻译:FAR_EL2 给的是 guest 的虚拟地址(VA),但要找设备得转成 IPA。两个途径:
guest_va_to_ipa:走 guest 自己的 Stage1 页表翻译(trap.c:270)get_faulting_ipa:直接用 HPFAR_EL2(硬件在 trap 时已经算好的 IPA,trap.c:224)
⑥ 核心:vdev_mmio_emulation(vdev.c:198)遍历该 VM 的 vdev 列表,按地址匹配设备,调用设备的 read/write 回调。这就是你 Phase 1 学的”vGIC 的 GICD_ISENABLER 写入”进入的路径。
⑧ 失败兜底(out_fail):设备模拟不是总能成功。几个失败场景会走到 out_fail(trap.c:247-261, 273-293):
1 | out_fail: |
触发 out_fail 的情况:
- external abort(
dabt_isextabt,trap.c:247)——外部总线错误,无法恢复 - 不支持的 FSC(trap.c:252)——不是 FAULT/PERM 的异常
- ISS 无有效症状(
!ESR_ELx_ISV,trap.c:258)——硬件没记录读写信息,无法模拟 vdev_mmio_emulation返回 -EACCES(trap.c:273)——设备拒绝访问
vcpu_fault 会把异常转成 guest 的同步异常注入回去(类似给 guest 一个”segfault”),让 guest 自己的异常处理去处理——而不是 hypervisor 直接崩溃。
6.3 关键知识点
- **MMIO 模拟 = “地址匹配 + 设备回调”**:guest 访问无映射地址 → 找到对应虚拟设备 → 模拟读写。
- ISS 字段解码读写方向 + 目标寄存器:靠
WNR(写)和SRT(寄存器号)。 - 地址翻译链:FAR(VA)→ IPA(guest_va_to_ipa 或 HPFAR)→ 匹配 vdev。
7. IRQ 路径
源码位置:
arch/aarch64/core/aarch64_IRQ.c、core/irq.c、vector.S
7.1 中断汇编入口:__irq_exception_from_lower_el
1 | vfunc __irq_exception_from_lower_el |
每步作用:
| 步 | 动作 | 作用 |
|---|---|---|
| ① | SAVE_GP_REGS |
压现场(与同步异常相同) |
| ② | PCPU_LOAD_CURRENT_TASK |
x18 = 当前任务 |
| ③ | 置 __TIF_HARDIRQ_MASK |
★ 中断独有:给当前任务打”正在处理硬中断”标记(task_info.h:22),供调度器/软中断判断当前上下文 |
| ④ | str sp, [x18, TASK_STACK_OFFSET] |
存栈——中断可能触发调度,任务被切走前保存栈 |
| ⑤ | task_exit_from_user |
退出钩子(与 3.1 相同,vCPU 场景切 vcpu->mode) |
| ⑥ | irq_from_lower_el |
C 中断分发(见 7.2) |
| ⑦ | 清 __TIF_HARDIRQ_MASK |
中断处理完,解除硬中断标记 |
| ⑧ | exception_return |
走异常返回,检查是否需要重调度 |
与同步异常入口(3.1)的差异:
| 维度 | __sync_exception_from_lower_el(3.1) |
__irq_exception_from_lower_el(7.1) |
|---|---|---|
| 向量表入口 | l64sync / l32sync |
l64irq |
| 硬中断标记 | 无 | 有(__TIF_HARDIRQ_MASK 置/清) |
| C 调用 | sync_exception_from_lower_el |
irq_from_lower_el |
| 返回 | LOAD_GP_REGS + eret |
exception_return(会检查重调度) |
__TIF_HARDIRQ_MASK 的作用:置位表示”当前正在处理硬中断”。这样调度器、软中断处理等能区分”我在硬中断上下文”还是”正常上下文”(task_info.h:28 __TIF_IN_INTERRUPT 组合了硬/软中断标记)。
和虚拟化的衔接:中断的完整虚拟化(物理中断 → GIC → 注入 guest 的 LR)属于 Phase 5 vGIC,
本节只交代”中断从哪里进入 EL2”这条入口路径,与同步异常入口并列。
7.2 中断进入
1 | // aarch64_IRQ.c |
7.3 中断分发
1 | int do_irq_handler(void) |
和虚拟化的衔接:如果这个物理中断对应 guest 的虚拟中断(virq),会走到 guest_irq_handler(virq.c:120)→ send_virq → 标 pending → kick_vcpu(你 Phase 1 已学过这条注入路径)。
7.4 关键知识点
- IRQ 和同步异常走不同的 handler:同步异常查
guest_sync_descs[],中断走do_irq_handler。 - 中断也可能是虚拟化的输入:物理中断到来 → 可能是要注入给 guest 的(send_virq)。
- **中断入口独有的
__TIF_HARDIRQ_MASK**:标记硬中断上下文,供调度/软中断判断。
8. 本节小结
本节完成了什么:吃透了”guest 异常 → EL2 → 分发 → 返回”的完整闭环。核心链路:
| 步骤 | 位置 | 作用 |
|---|---|---|
| 1. 向量表入口 | entry.S → vector.S | 按异常类型跳转,SAVE_GP_REGS 压栈 |
| 2. 身份判断 | aarch64_sync.c:156 | 区分 vCPU / 普通任务 |
| 3. ESR 解码 | trap.c:392 | 读 ESR_EL2 → 取 EC |
| 4. 查表分发 | trap.c:367 | EC → handler |
| 5. 执行处理 | 各 handler | 模拟寄存器 / MMIO / hypercall / WFI |
| 6. 返回 guest | vector.S LOAD_GP_REGS + eret | 恢复现场回 EL1 |
核心知识点(要记住的 4 件事):
- 一切虚拟化都是 trap 出来的——这条路径是 hypervisor 生命线。
- ESR_EL2 的 EC 字段 = 分发钥匙,6 位查表分 22 类。
regs->pc += ret_addr_adjust按 EC 调整(WFI/DABT 跳过指令,HVC 因硬件已指向下一条无需调整)。- 两个最重要的 handler:SYS64(系统寄存器模拟)、DABT_LOW(MMIO 模拟入口)。
下一步是什么:Phase 4 内存虚拟化——理解 Stage2 页表怎么建、guest_va_to_ipa 背后怎么工作,以及 vdev_mmio_emulation 依赖的 VM 内存布局。
9. 术语速查
| 术语 | 含义 |
|---|---|
| trap | 因 guest 访问敏感资源而陷入 EL2 |
| EC(Exception Class) | ESR_EL2 的 [31:26] 位,异常类别,分发钥匙 |
| ISS | ESR_EL2 的 [24:0] 位,异常综合信息,内容依赖 EC |
| gp_regs | 异常入口压栈形成的现场结构(对应 struct aarch64_regs) |
| ret_addr_adjust | 返回地址调整量,跳过已执行的指令避免死循环 |
| FAR_EL2 / HPFAR_EL2 | 故障虚拟地址 / 故障 IPA(硬件 trap 时自动写入) |
| SRT | ISS 里的目标寄存器号字段 |
10. 本文寄存器速查
分类:① 控制寄存器(hypervisor 配置”拦什么”)、② 异常记录寄存器(硬件 trap 时自动写入)、③ 现场保存寄存器(异常入口压栈用)、④ guest 被模拟的寄存器(access_system_reg_handler 场景)。
10.1 控制寄存器:配置”拦什么”
HCR_EL2(Hypervisor Configuration Register,虚拟化配置寄存器)—— 虚拟化总开关,陷阱位全在这(arch_virt.c:187-227 配置,每 vCPU 一份):
| 位 | 名字 | 作用 |
|---|---|---|
| 0 | VM | 使能 stage2 地址翻译(虚拟化的前提) |
| 2 | PTW | 页表遍历(stage1)失败时 trap。注意:minos 不补页表内存,walk fault 走 MMIO 统一路径(S1PTW 强制当”写”,trap.c:238),匹配不到设备就直接关 VM(trap.c:288 → vfault.c:49) |
| 3 | FMO | 使能 FIQ 虚拟化(trap FIQ 到 EL2) |
| 4 | IMO | 使能 IRQ 虚拟化(trap IRQ 到 EL2) |
| 5 | AMO | 使能 SError 虚拟化 |
| 9 | FB | 强制广播 TLB 维护指令(TLBI 等)到所有核,保证映射变更全局生效(arch_virt.c:179) |
| 10-11 | BSU | 屏障广播级别(IS = 内共享) |
| 13 | TWI | trap WFI 指令(guest 空闲时陷入,可让出 CPU) |
| 19 | TSC | trap SMC 指令(guest 的 PSCI 调用全被拦下) |
| 20 | TIDCP | trap 访问 IMPLEMENTATION DEFINED 协处理器 |
| 21 | TACR | trap 访问 ACTLR_EL1 等辅助控制寄存器 |
| 27 | TGE | trap 来自 guest 的所有通用异常(”测试模式”) |
| 28 | TDZ | trap DC ZVA(清零 VA,XNU 需要) |
CPTR_EL2(Architectural Feature Trap Register,架构特性陷阱寄存器)—— 协处理器访问控制:控制浮点/高级 SIMD 等是否 trap 到 EL2(access_system_reg_handler 的 trap 来源之一,5.1 提及)。
CNTHCTL_EL2(Counter-timer Hypervisor Control Register,定时器虚拟化控制寄存器)—— 定时器访问控制:控制 EL1 读写定时器/计数寄存器是否 trap(arm_arch_timer.c:171 只放行 CNTPCT 读)。它是 CNTP_* 被 trap 的根源(见 11.4)。
10.2 异常记录寄存器:硬件 trap 时自动写入
ESR_EL2(Exception Syndrome Register,异常综合寄存器)—— 分发的钥匙。位布局(trap.h:25-29 / aarch64_common.h):
| 位段 | 宽度 | 含义 |
|---|---|---|
| EC [31:26] | 6 | Exception Class,异常类别 → 查 guest_sync_descs[] |
| IL [25] | 1 | 指令长度:0=32 位指令,1=64 位指令 |
| ISS [24:0] | 25 | 异常综合信息,内容依赖 EC(见下) |
ISS 依赖 EC 的两个关键细分(本章主角):
- SYS64(EC=0x18):
esr_sysreg(trap.h:104)——read[0]、crm[1-4]、reg/Rt[5-9]、crn[10-13]、op1[14-16]、op2[17-19]、op0[20-21]。见 4.1.1。 - DABT(EC=0x24):
WNR[6](0=读 1=写)、SRT[16-20](目标寄存器号)、ISV[24](ISS 是否有效)、FSC[5:0](fault 状态码,4=Translation fault,0xC=Permission fault,0x10=外部 abort)。
ELR_EL2(Exception Link Register,异常链接寄存器)—— 异常返回地址:guest 从哪个地址陷入,eret 回哪个地址。配合 ret_addr_adjust(4.3)。
SPSR_EL2(Saved Program Status Register,保存的程序状态寄存器)—— 异常返回时的 PSTATE:保存 guest 的 NZCV/DAIF 等状态,eret 时恢复。
FAR_EL2(Fault Address Register,故障地址寄存器)—— 故障虚拟地址:数据/指令 abort 时硬件写入 guest 的 VA(6.2 用它转 IPA)。
HPFAR_EL2(Hypervisor IPA Fault Address Register,虚拟机 IPA 故障地址寄存器)—— 故障 IPA:stage2 abort 时硬件直接给出翻译后的 IPA(6.2 提到可直接用,省一次软件翻译)。
10.3 现场保存寄存器:异常入口压栈用
| 寄存器(全名) | 作用 |
|---|---|
| VBAR_EL2(Vector Base Address Register,向量表基址寄存器) | 向量表基址,决定异常跳去哪 |
| SP_EL0(Stack Pointer EL0,EL0 栈指针) | EL2 栈指针(进入 EL2 后用 SP_EL0 压栈保存 guest 现场) |
10.4 guest 被模拟的寄存器(access_system_reg_handler 场景)
| 寄存器(全名) | 编码 (op0,op1,CRn,CRm,op2) | 谁 trap 它 | 模拟行为 |
|---|---|---|---|
| CNTPCT_EL0(Counter-timer Physical Count,物理计数寄存器) | (3,3,14,0,0) | CNTHCTL_EL2 | 返回虚拟物理计数(vtimer.c) |
| CNTP_TVAL_EL0(Counter-timer Physical Timer TimerValue,物理定时器装载值) | (3,3,14,2,0) | 同上 | 定时器装载值(vtimer.c) |
| CNTP_CTL_EL0(Counter-timer Physical Timer Control,物理定时器控制) | (3,3,14,2,1) | 同上 | 使能/屏蔽/状态(vtimer.c) |
| CNTP_CVAL_EL0(Counter-timer Physical Timer CompareValue,物理定时器比较值) | (3,3,14,2,2) | 同上 | 比较值(vtimer.c) |
| ICC_SGI1R_EL1(Interrupt Controller Group1 SGI Register,中断控制器组1软件中断寄存器) | (3,0,12,11,5) | HCR_EL2 | 转发为 vGIC 的 SGI(sgi1r_el1_trap) |
关于 MMIO 设备寄存器:第 6 节出现的
GICD_*、GICR_*属于外设内存映射寄存器(走 MMIO 模拟,不是系统寄存器),地址由 vdev 匹配,不属于本节范围。
10.5 IRQ 路径用到的寄存器
| 寄存器(全名) | 作用 |
|---|---|
| ICC_IAR1(Interrupt Controller Interrupt Acknowledge Register,中断确认寄存器) | 读它拿到当前最高优先级待处理中断号 |