Minos 虚拟化: 异常与 trap 机制

本文介绍 Minos Hypervisor 的 trap 机制。

0. 全景图

phase3_flow

一条铁律一切虚拟化都是 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
2
3
4
5
6
7
8
context->hcr_el2 = value | HCR_EL2_VM | HCR_EL2_TIDCP | HCR_EL2_IMO |
HCR_EL2_FMO | HCR_EL2_BSU_IS | HCR_EL2_FB | HCR_EL2_PTW |
HCR_EL2_TSC | HCR_EL2_TACR | HCR_EL2_AMO;

if (!(vcpu->vm->flags & VM_FLAGS_NATIVE_WFI))
context->hcr_el2 |= HCR_EL2_TWI; // trap WFI
if (vcpu->vm->os->type == OS_TYPE_XNU)
context->hcr_el2 |= HCR_EL2_TDZ; // trap DC ZVA(XNU 特殊)

guest 触发 trap 的完整因果链

1
2
3
4
5
6
7
8
9
10
11
12
13
hypervisor 设 HCR_EL2.xxx = 1("我要拦这个操作")


guest 在 EL1 执行敏感操作(WFI / 读写敏感寄存器 / SMC / 访问未映射内存...)


硬件检查 HCR_EL2 陷阱位 → 命中 → 自动陷入 EL2


硬件写 ESR_EL2(记录异常原因 EC+ISS)→ ELR_EL2(返回地址)→ 跳向量表


(本 phase 后面的内容全从这里开始)

关键点

  1. **HCR_EL2 陷阱位 = 虚拟化的”拦截清单”**——hypervisor 想拦什么就置哪一位。
  2. guest 触发陷阱 → 硬件写 ESR_EL2——ESR 是”为什么 trap”的记录,正是本 phase 第 4 节解密的对象。
  3. HCR_EL2 是每 vCPU 一份context->hcr_el2 保存在 vCPU 上下文里,由 vmodule 在切换时恢复(Phase 7 详讲)。
  4. 不是所有 guest 操作都会 trap:只有 HCR_EL2 置位的那几类才陷,其余 guest 正常在 EL1 跑(这就是虚拟化性能的关键——能直通就直通,必要才 trap)。

2. 异常入口

源码位置:arch/aarch64/core/vector.S

2.1 入口路径

上一 phase 学过向量表(entry.S)16 个入口。guest 触发的异常走 Lower EL 组

1
2
3
4
l64sync:  // Lower EL using AArch64,同步异常
b __sync_exception_from_lower_el
l64irq: // Lower EL using AArch64,IRQ
b __irq_exception_from_lower_el

2.2 SAVE_GP_REGS

异常发生时,CPU 硬件只自动做了三件事(写 ELR_EL2/SPSR_EL2,跳到向量表),其余寄存器全部要汇编手动保存

1
2
3
4
5
6
7
8
9
10
11
12
13
.macro __SAVE_GP_REGS
stp x27, x28, [sp, #-16]! // 从 x28 往上逐个压栈
stp x25, x26, [sp, #-16]!
... // 一直到 x0
str x0, [sp, #-8]!
mrs x0, SP_EL0 // 保存 guest 的 SP
str x0, [sp, #-8]!
mrs x0, SPSR_EL2 // 保存 guest 的 PSTATE
str x0, [sp, #-8]!
mrs x0, ELR_EL2 // 保存 guest 的返回地址
str x0, [sp, #-8]!
dsb nsh
.endm

压栈顺序 → gp_regs 结构。压栈的布局正好对应 tcb.h:6struct aarch64_regs

1
2
3
4
5
6
7
struct aarch64_regs {   // gp_regs,就是栈上的布局!
uint64_t pc; // ELR_EL2(guest 返回地址)
uint64_t pstate; // SPSR_EL2(guest 状态)
uint64_t sp; // SP_EL0(guest 栈指针)
uint64_t x0; // 然后 x0~x28、lr
...
};

关键理解gp_regs 不是”结构体内存”,它就是异常入口在栈上压出来的那串值。C 侧的 handler 拿到 gp_regs *regs,就是在读栈顶的这份现场。

2.3 LOAD_GP_REGS

顺序与保存相反,最后 eret

1
2
3
4
5
6
7
ldr	x0, [sp], #8
msr ELR_EL2, x0 // 恢复返回地址
ldr x0, [sp], #8
msr SPSR_EL2, x0 // 恢复状态
ldp x0, x1, [sp], #16 // 恢复 x0-x29
...
eret // ★ PC=ELR_EL2, PSTATE=SPSR_EL2 → 回到 guest

2.4 关键知识点

  1. 异常 = 现场保存 + 分发 + 现场恢复:硬件只自动保存 ELR/SPSR,其余全靠汇编。
  2. gp_regs 就是栈布局:理解它 = 理解异常入口压了什么。
  3. 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
2
3
4
5
6
7
8
9
10
11
vfunc __sync_exception_from_lower_el
SAVE_GP_REGS // ① 现场压栈 → gp_regs
PCPU_LOAD_CURRENT_TASK // ② x18 = 当前任务
bl task_exit_from_user // ③ "离开用户态"钩子
mov x0, sp // ④ 参数:gp_regs 地址
bl sync_exception_from_lower_el // ⑤ ★ 调 C 分发函数
mov x0, sp // ⑥ 参数:gp_regs 地址
bl task_return_to_user // ⑦ "回到用户态"钩子
LOAD_GP_REGS // ⑧ 恢复现场
eret // ⑨ 返回 guest
vfunc_end __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->modeIN_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
2
3
4
5
6
7
8
9
void sync_exception_from_lower_el(gp_regs *regs)
{
#ifdef CONFIG_VIRT
if ((current->flags & TASK_FLAGS_VCPU) && !(read_hcr_el2() & HCR_EL2_TGE))
handle_vcpu_sync_exception(regs); // ★ 是 vCPU 任务 → 走虚拟化分发
else
#endif
handle_sync_exception(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vfunc __sync_exception_from_current_el
SAVE_GP_REGS // ① 现场压栈 → gp_regs
mov x0, sp
str x0, [x18, #TASK_STACK_OFFSET] // ② 把 sp 存进当前任务(调度可能切走)

mrs x1, ESR_EL2 // ③ 读 ESR
ubfx x2, x1, #ESR_ELx_EC_SHIFT, #ESR_ELx_EC_WIDTH // 取 EC
cmp x2, #ESR_ELx_EC_SVC64 // ④ 是不是 SVC(主动系统调用/调度请求)?
b.eq __sync_current_out // 是 → 直接走 exception_return(调度路径)

bl sync_exception_from_current_el // ⑤ 不是 SVC → C 处理(通常 panic)

__sync_current_out:
b exception_return // ⑥ 统一走异常返回(可能发生调度)
vfunc_end __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_elhandle_sync_exception

1
2
3
4
5
6
7
8
static void handle_sync_exception(gp_regs *regs)
{
esr_value = read_esr(); // CONFIG_VIRT 下读 ESR_EL2
ec_type = ESR_ELx_EC(esr_value);
ec = process_sync_descs[ec_type]; // ★ 查的是 process_sync_descs[],不是 guest_sync_descs[]
regs->pc += ec->ret_addr_adjust;
ec->handler(regs, ec_type, esr_value);
}

process_sync_descs(aarch64_sync.c:116-120)只有几个条目:

1
2
3
4
5
static struct sync_desc *process_sync_descs[] = {
[0 ... ESR_ELx_EC_MAX] = &sync_desc_trap_unknown, // 默认 → panic
[ESR_ELx_EC_IABT_CUR] = &sync_desc_trap_kernel_ia, // 取指故障 → panic
[ESR_ELx_EC_DABT_CUR] = &sync_desc_trap_kernel_da, // 数据故障 → panic
};

本质:hypervisor 自己的异常基本全是 bug → 直接 panickernel_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 关键知识点

  1. 同一个异常入口,两种身份:vCPU(guest) vs 普通任务(hypervisor 自己),靠 TASK_FLAGS_VCPU + HCR_EL2.TGE 区分。
  2. **guest 的一切同步异常最终都汇到 handle_vcpu_sync_exception**。
  3. current EL 异常基本是 bug → panicprocess_sync_descs[] 只有 unknown/kernel_ia/kernel_da 三个 panic 条目。
  4. 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
2
3
4
5
ESR_EL2 (64-bit)
┌──────────┬───────┬───────────────────────────────┐
│ EC[31:26]│ IL[25]│ ISS[24:0] │
│ 异常类别 │ 指令长 │ 异常综合信息(依赖 EC) │
└──────────┴───────┴───────────────────────────────┘
  • 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
2
#define ESR_ELx_EC_MASK		(ULONG(0x3F) << 26)   // EC 取位
#define ESR_ELx_EC(esr) (((esr) & ESR_ELx_EC_MASK) >> 26)

4.1.1 SYS64(EC=0x18)的 ISS 细分:esr_sysreg

EC=0x18 时,ISS 的 25 位不再是”综合信息”,而是系统寄存器访问的完整编码。minos 用一个位域结构体直接解码:

1
2
3
4
5
6
7
8
9
10
11
12
struct esr_sysreg {          // trap.h:104
unsigned long read:1; /* bit0 Direction */
unsigned long crm:4; /* bit1-4 CRm */
unsigned long reg:5; /* bit5-9 Rt */
unsigned long crn:4; /* bit10-13 CRn */
unsigned long op1:3; /* bit14-16 Op1 */
unsigned long op2:3; /* bit17-19 Op2 */
unsigned long op0:2; /* bit20-21 Op0 */
unsigned long res0:3; /* bit22-24 RES0 */
unsigned long len:1; /* bit25 IL */
unsigned long ec:6; /* bit26-31 EC */
};

**核心:(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
2
3
4
5
6
7
struct esr_sysreg *sysreg = (struct esr_sysreg *)&esr_value;   // 直接用位域解码
reg_name = esr_value & ESR_SYSREG_REGS_MASK; // 取低 24 位做 switch key
reg_value = sysreg->read ? 0 : get_reg_value(reg, regindex); // MSR 则从 Rt 取写入值

switch (reg_name) { /* CNTP_CTL_EL0 / ICC_SGI1R_EL1 / ... */ }

if (sysreg->read) set_reg_value(reg, regindex, reg_value); // MRS 则把模拟结果写回 Rt

即:读系统寄存器(MRS)时,hypervisor 模拟出值再写回 Rt;写系统寄存器(MSR)时,从 Rt 取出要写的值交给对应的模拟函数——方向由 read 位区分,目标寄存器由 reg(Rt)指出。

4.2 分发表:guest_sync_descs[]

**这是虚拟化的”分诊台”**——按 EC 索引,选对应 handler:

1
2
3
4
5
6
7
8
9
static struct sync_desc *guest_sync_descs[] = {
[0 ... ESR_ELx_EC_MAX] = &sync_desc_guest_ESR_ELx_EC_UNKNOWN, // 默认
[ESR_ELx_EC_WFx] = &sync_desc_guest_ESR_ELx_EC_WFx, // WFI/WFE
[ESR_ELx_EC_SYS64] = &sync_desc_guest_ESR_ELx_EC_SYS64, // 系统寄存器
[ESR_ELx_EC_DABT_LOW] = &sync_desc_guest_ESR_ELx_EC_DABT_LOW, // 数据异常
[ESR_ELx_EC_HVC64] = &sync_desc_guest_ESR_ELx_EC_HVC64, // hypercall
[ESR_ELx_EC_SMC64] = &sync_desc_guest_ESR_ELx_EC_SMC64, // SMC
...
};

每个描述符由宏 DEFINE_SYNC_DESC 生成(trap.c:344-365),含 handler 函数指针和 ret_addr_adjust(返回地址调整量)。

4.3 分发函数:handle_vcpu_sync_exception

1
2
3
4
5
6
7
8
void handle_vcpu_sync_exception(gp_regs *regs)
{
esr_value = read_esr_el2(); // ① 读 ESR_EL2
ec_type = (esr_value & ESR_ELx_EC_MASK) >> 26; // ② 取 EC
ec = guest_sync_descs[ec_type]; // ③ 查表拿 handler
regs->pc += ec->ret_addr_adjust; // ④ 调整返回地址
ec->handler(regs, ec_type, esr_value); // ⑤ 调用 handler
}

④ 为什么 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 关键知识点

  1. ESR_EL2 的 EC 字段是分发的钥匙,一个 6 位查表搞定 22 类异常。
  2. regs->pc += ret_addr_adjust 防止重复执行——这是每个 hypervisor 都要处理的坑。
  3. 本节的”主角”是 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
2
3
struct esr_sysreg *sysreg = (struct esr_sysreg *)&esr_value;
reg_name = esr_value & ESR_SYSREG_REGS_MASK; // 编码了 Op0/Op1/CRn/CRm/Op2
reg_value = sysreg->read ? 0 : get_reg_value(reg, regindex); // 写操作取目标寄存器值

5.2 处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (reg_name) {
case ESR_SYSREG_ICC_SGI1R_EL1: // guest 发核间中断 → 交给 vGIC
arm_data->sgi1r_el1_trap(vcpu, reg_value); // vgicv3_send_sgi
break;
case ESR_SYSREG_CNTP_CTL_EL0: // guest 访问物理定时器 → 软件模拟
arm_data->phy_timer_trap(vcpu, reg_name, read, &reg_value);
break;
default:
if (arm_data->sysreg_emulation) // 兜底:交给各 VM 自定义模拟
ret = arm_data->sysreg_emulation(vcpu, reg_name, read, &reg_value);
}
if (sysreg->read) // 读操作要把结果写回 guest 寄存器
set_reg_value(reg, regindex, reg_value);

5.3 关键知识点

  1. SYS64 trap 的本质:guest 想”碰”一个敏感寄存器 → hypervisor 用软件模拟它(这就是你 Phase 1 学的”CNTP 被 trap 软件模拟”的代码实现)。
  2. ISS 字段解码:从 esr 里解析出”哪个寄存器、读/写”,再分发给对应模拟器。
  3. 后续延伸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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int dataabort_tfl_handler(gp_regs *regs, int ec, uint32_t esr_value)
{
dfsc = esr_value & ESR_ELx_FSC_TYPE; // ① fault 状态码
if ((dfsc != FSC_FAULT) && (dfsc != FSC_PERM)) // 只要 FAULT/PERM
goto out_fail;

iswrite = dabt_iswrite(esr_value); // ② 从 ISS 判断读写
reg = ESR_ELx_SRT(esr_value); // ③ 从 ISS 拿目标寄存器号
value = iswrite ? get_reg_value(regs, reg) : 0;
vaddr = read_sysreg(FAR_EL2); // ④ 故障虚拟地址
ipa = guest_va_to_ipa(vaddr, 1); // ⑤ VA → IPA(guest 地址)

ret = vdev_mmio_emulation(regs, iswrite, ipa, &value); // ⑥ ★ 交给设备模拟
if (!iswrite)
set_reg_value(regs, reg, value); // ⑦ 读结果写回 guest 寄存器
return 0;
}

⑤ 地址翻译: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
2
3
out_fail:
vcpu_fault(current_vcpu, regs); // 向 guest 注入一个同步错误(类似信号)
return -EFAULT; // 表示"这个 vCPU 别返回 guest 了"

触发 out_fail 的情况:

  • external abortdabt_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 关键知识点

  1. **MMIO 模拟 = “地址匹配 + 设备回调”**:guest 访问无映射地址 → 找到对应虚拟设备 → 模拟读写。
  2. ISS 字段解码读写方向 + 目标寄存器:靠 WNR(写)和 SRT(寄存器号)。
  3. 地址翻译链:FAR(VA)→ IPA(guest_va_to_ipa 或 HPFAR)→ 匹配 vdev。

7. IRQ 路径

源码位置:arch/aarch64/core/aarch64_IRQ.ccore/irq.cvector.S

7.1 中断汇编入口:__irq_exception_from_lower_el

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vfunc __irq_exception_from_lower_el
SAVE_GP_REGS // ① 现场压栈 → gp_regs
PCPU_LOAD_CURRENT_TASK // ② x18 = 当前任务
ldr x1, [x18, #TASK_INFO_FLAGS_OFFSET]
orr x1, x1, #__TIF_HARDIRQ_MASK // ③★ 置"硬中断"标记
str x1, [x18, #TASK_INFO_FLAGS_OFFSET]
dsb sy
mov x0, sp
str x0, [x18, #TASK_STACK_OFFSET] // ④ 存栈(可能被调度切走)
bl task_exit_from_user // ⑤ 退出钩子(同 3.1)
mov x0, sp
bl irq_from_lower_el // ⑥ C 中断分发
ldr x1, [x18, #TASK_INFO_FLAGS_OFFSET]
and x1, x1, #(~__TIF_HARDIRQ_MASK) // ⑦★ 清"硬中断"标记
str x1, [x18, #TASK_INFO_FLAGS_OFFSET]
dsb sy
b exception_return // ⑧ 走异常返回(检查重调度)
vfunc_end __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
2
3
// aarch64_IRQ.c
void irq_from_lower_el(gp_regs *regs) { irq_handler(regs); }
static inline void irq_handler(gp_regs *regs) { do_irq_handler(); }

7.3 中断分发

1
2
3
4
5
6
7
8
9
int do_irq_handler(void)
{
while (1) {
irq = irq_chip->get_pending_irq(); // ① 读 ICC_IAR1 拿中断号
if (irq >= BAD_IRQ) return 0; // 没有待处理中断了
irq_desc = get_irq_desc_cpu(cpuid, irq); // ② 查中断描述符
do_handle_host_irq(cpuid, irq_desc); // ③ 执行 handler
}
}

和虚拟化的衔接:如果这个物理中断对应 guest 的虚拟中断(virq),会走到 guest_irq_handler(virq.c:120)→ send_virq → 标 pending → kick_vcpu(你 Phase 1 已学过这条注入路径)。

7.4 关键知识点

  1. IRQ 和同步异常走不同的 handler:同步异常查 guest_sync_descs[],中断走 do_irq_handler
  2. 中断也可能是虚拟化的输入:物理中断到来 → 可能是要注入给 guest 的(send_virq)。
  3. **中断入口独有的 __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 件事)

  1. 一切虚拟化都是 trap 出来的——这条路径是 hypervisor 生命线。
  2. ESR_EL2 的 EC 字段 = 分发钥匙,6 位查表分 22 类。
  3. regs->pc += ret_addr_adjust 按 EC 调整(WFI/DABT 跳过指令,HVC 因硬件已指向下一条无需调整)。
  4. 两个最重要的 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,中断确认寄存器) 读它拿到当前最高优先级待处理中断号