4. 同步异常处理:数据中止、指令中止、未定义指令、SVC/HVC/SMC 调用

同步异常,说白了就是 CPU 执行指令时自己“撞上”的问题。跟外部中断不一样,这类异常是程序主动或被动触发的,异常返回地址能精确告诉你哪条指令出了问题。我在调试中遇到的大部分“死机”现象,追根溯源都是同步异常没处理好。

ARMv8 的同步异常种类不少,但最常用的就这五类:数据中止、指令中止、未定义指令、SVC、HVC、SMC。咱们一个一个拆开讲。

4.1 数据中止(Data Abort)

数据中止发生在 CPU 访问内存的时候。读数据、写数据,只要地址有问题,MMU 就会拉异常。常见的场景:访问了未映射的地址、权限不够、或者地址对齐有问题。

核心要点:数据中止的 ESR(Exception Syndrome Register)会告诉你具体原因。ESR_ELx 的位 24:0 是异常综合征信息,其中 DFSC(Data Fault Status Code)字段指明了故障类型。

我个人习惯,一进数据中止处理函数,第一件事就是读 ESR。你看这个代码片段:

// 数据中止处理函数入口
void data_abort_handler(void) {
    uint64_t esr, far, elr;
    
    // 读取异常综合征寄存器
    asm volatile("mrs %0, esr_el1" : "=r"(esr));
    // 读取错误地址寄存器
    asm volatile("mrs %0, far_el1" : "=r"(far));
    // 读取返回地址
    asm volatile("mrs %0, elr_el1" : "=r"(elr));
    
    uint32_t dfsc = esr & 0x3F;  // DFSC 在低 6 位
    uint32_t write_not_read = (esr >> 6) & 1;  // 第 6 位:0=读,1=写
    
    // 根据 DFSC 判断具体原因
    switch(dfsc) {
        case 0b000001: // 地址尺寸错误
        case 0b000100: // 未映射的页
        case 0b001101: // 权限错误(一级页表)
        case 0b001111: // 权限错误(三级页表)
        case 0b010000: // 同步外部异常
        // ... 其他情况
    }
}

避坑指南:我曾经遇到一个坑——数据中止后,FAR_EL1 不一定有效。如果 ESR 里的 FnV 位(位 11)为 1,说明 FAR 的值不可靠。这时候你只能靠 ELR 回溯指令,反汇编看是哪条 load/store 出的问题。

4.2 指令中止(Prefetch Abort)

指令中止跟数据中止很像,区别在于它发生在 CPU 取指令的时候。说白了,CPU 去内存里拿指令,结果地址不对或者权限不够,那就指令中止了。

为什么会单独区分指令中止?你想想看,数据访问失败可能只是某个变量读错了,但指令取不到,CPU 根本不知道下一步该干啥。所以指令中止的优先级比数据中止高,处理起来也更紧急。

对比项 数据中止 指令中止
触发时机 load/store 指令执行时 取指令阶段
ESR 编码 DFSC(位 24:0) IFSC(位 24:0)
FAR 有效性 通常有效(看 FnV 位) 无效(FAR 不更新)
典型场景 空指针解引用、数组越界 跳转到非法地址、代码段被覆盖

指令中止的 ESR 里,IFSC 字段跟 DFSC 的编码规则一样。但有个关键区别——FAR 在指令中止时不会更新。我记得有次调试一个跳转表错误,一直读 FAR 发现是 0,后来才反应过来指令中止得看 ELR。

4.3 未定义指令

未定义指令,就是 CPU 不认识你写的这条指令。ARMv8 的指令集是固定长度的(AArch64 下都是 32 位),但并不是所有 32 位组合都是有效指令。遇到无效编码,CPU 就抛未定义指令异常。

这个异常在实际项目中挺常见的。我遇到过两种情况:

  • 协处理器指令没实现:比如你写了 MRS/MSR 访问某个系统寄存器,但当前异常等级没权限,或者这个寄存器根本不存在。
  • SIMD/FP 指令没使能:CPACR_EL1 没配好,FPEN 位为 0,一执行浮点运算就崩。

注意:未定义指令异常的处理函数里,ESR 的位 24:0 是 ISS(Instruction Specific Syndrome),不是 DFSC。你得根据 ESR 的位 25(IL 位)判断指令长度,再结合 ISS 反推是哪条指令出了问题。

处理未定义指令时,我建议先判断是不是合法的“未定义”。什么意思?有些操作系统会用未定义指令异常来实现软件断点或功能扩展。比如你在 EL2 的 hypervisor 里,可以捕获 EL1 的未定义指令,然后模拟执行——这就是虚拟化里常见的“trap-and-emulate”手法。

4.4 SVC、HVC、SMC 调用

这三类异常是主动触发的。程序执行 SVC、HVC、SMC 指令,CPU 就会同步地陷入更高的异常等级。说白了,这是用户程序“请愿”的方式——我想让内核帮我干点特权事。

SVC(Supervisor Call)

SVC 是 EL0 调用 EL1 的接口。你在用户态程序里写:

// 触发 SVC #0x12,参数通过 x0-x5 传递
asm volatile("svc #0x12" : : : "x0", "x1", "x2", "x3", "x4", "x5");

CPU 就会陷入 EL1,SVC 处理函数通过 ESR 的 imm16 字段(位 20:5)拿到立即数 0x12,然后根据这个号分发到不同的系统调用服务。

我见过不少新手在 SVC 处理函数里忘了保存 x0-x5 的值。你想想看,用户程序把参数放在寄存器里,结果你一进异常处理就把寄存器覆盖了,那参数不就丢了吗?所以 SVC 入口第一件事就是保存现场。

HVC(Hypervisor Call)

HVC 是 EL1 调用 EL2 的接口。在虚拟化场景下,Guest OS(EL1)想操作硬件或切换虚拟 CPU,就得通过 HVC 陷入 Hypervisor(EL2)。

HVC 的用法跟 SVC 几乎一样,只是异常等级不同。ESR 里同样有 imm16 字段,Hypervisor 根据这个号判断 Guest 想要什么服务。

关键区别:SVC 的返回地址是 ELR_EL1,HVC 的返回地址是 ELR_EL2。处理完 HVC 后,你要用 ERET 指令返回 EL1,而不是直接跳转。

SMC(Secure Monitor Call)

SMC 是切换到安全世界的入口。ARM 的 TrustZone 架构里,普通世界(Non-secure)通过 SMC 指令陷入 Monitor 模式(EL3),然后切换到安全世界执行敏感操作。

SMC 的调用约定跟 SVC/HVC 不太一样。它使用 SMCCC(SMC Calling Convention),参数通过 x0-x17 传递,返回值也在 x0 里。x0 的低 8 位是 Function ID,告诉 Monitor 你要干啥。

指令 触发等级 目标等级 典型用途
SVC EL0 EL1 系统调用
HVC EL1 EL2 Hypervisor 调用
SMC EL0/EL1/EL2 EL3 安全世界调用

4.5 同步异常处理的核心流程

不管哪种同步异常,处理流程都差不多。我画了个流程图,你看一眼就明白了:

同步异常处理核心流程 同步异常触发 保存现场(寄存器入栈) 读取 ESR 判断异常类型 根据 ESR 分发到具体处理函数 数据/指令中止处理 未定义指令处理 SVC/HVC/SMC 处理 恢复现场 → ERET 返回

这个流程里有个细节容易被忽略——异常返回地址的修正。数据中止和指令中止的返回地址指向出问题的那条指令,如果你不修正就直接 ERET,CPU 会反复执行同一条指令,形成死循环。正确的做法是:如果能修复问题(比如缺页异常补上页表了),就原地址返回重试;如果修复不了,就跳过这条指令或者杀掉当前任务。

SVC/HVC/SMC 的情况正好相反。返回地址指向 SVC 指令的下一条指令,因为 SVC 本身已经执行完了。你不需要修正 ELR,直接 ERET 就行。

调试小技巧:我在项目里经常用 SVC 来测试异常处理链路。写个简单的 SVC #0,然后在 EL1 的处理函数里打印 ESR 和 ELR,就能验证整个异常入口、分发、返回的流程是否正常。这比直接等程序崩溃再调试要高效得多。

4.6 实战中的常见陷阱

最后总结几个我踩过的坑,你遇到了可以少走弯路:

  • ESR 没保存就被覆盖:一进异常处理函数就调用了其他函数,结果 ESR 被新函数里的 MRS 覆盖了。我建议在异常入口的汇编代码里就把 ESR 保存到栈上。
  • SP 切换导致栈回溯失败:ARMv8 有 SP_EL0 和 SP_ELx 两个栈指针。异常处理时如果切换了 SP,调试器可能找不到调用栈。我习惯在异常入口统一使用 SP_ELx。
  • 忽略 SError:同步异常处理得好好的,结果系统被异步的 SError 搞挂了。SError 虽然不属于同步异常,但它的处理流程跟数据中止很像,别忘了在异常向量表里也给它留个位置。

嗯,同步异常这部分内容比较多,但核心就一句话:读 ESR,看类型,修现场,再返回。把这四个步骤练熟了,大部分异常问题都能搞定。