ARM异常处理
深入讲解 ARM 异常处理机制,涵盖向量表、优先级链、LR 偏移调整及 Linux 内核下的异常分发。
1. 异常类型与触发条件
ARM 将异常定义为任何导致核心暂停正常执行、转而运行专用处理例程(exception handler)的条件。
- 当发生异常时,CPU 会跳转到该异常对应的异常处理程序(Exception Handler)执行。
- 异常处理程序的入口地址称为异常向量(Exception Vector),所有异常向量按固定偏移存放在异常向量表(Exception Vector Table)中。
- 异常向量表的基地址由特权软件配置到系统寄存器,CPU 发生异常时通过基地址 + 固定偏移定位对应的 Handler。
- ARM 支持为不同异常级别/安全状态配置独立的异常向量表(如 Secure PL1、Non-secure PL1、Monitor、Non-secure PL2),CPU 根据当前权限级别和目标安全状态选择对应的向量表。
- 异常处理程序既可以使用 ARM 指令集,也可以使用 Thumb 指令集,具体由 CP15 SCTLR.TE 位决定。
- 进入异常处理时,需要保存现场(处理器状态、寄存器等),异常处理完成后恢复现场,保证程序能够从异常发生处继续执行。
手册将异常分为六个大类:
| 异常 | 触发模式 | 触发条件 | 类型 |
|---|---|---|---|
| Reset | SVC | 复位信号(上电后) | 异步,不可屏蔽 |
| Undefined Instruction | UND | 执行未定义指令或未识别的协处理器指令 | 同步 |
| SVC(原 SWI) | SVC | 执行 SVC 指令(用户态请求 OS 服务) | 同步 |
| Prefetch Abort | ABT | 取指时 MMU 权限/翻译失败,且指令真正进入执行 | 同步 |
| Data Abort | ABT | Load/Store 执行时 MMU 权限/翻译失败或外部 Abort | 同步或异步 |
| IRQ | IRQ | 外部中断请求(经中断控制器汇聚) | 异步 |
| FIQ | FIQ | 高优先级外部中断请求(优先级高于 IRQ) | 异步 |
1.1 指令生成类异常:SVC、HVC、SMC、UNDEF
三类系统调用指令能按需触发显式异常——软件在需要升权时主动发起,而非被动响应外部事件:
- SVC(Supervisor Call,原 SWI):User 模式请求 OS 服务。指令中嵌入 24-bit(ARM)或 8-bit(Thumb)的调用号字段——SVC handler 通过读取触发异常的指令操作码评论字段来提取该值,实现系统调用的分派。参数也可直接通过寄存器传递(如 Linux 用 R7 存放 syscall number),comment field 方式作为备选
- HVC(Hypervisor Call,虚拟化扩展):Guest OS 在 PL1 陷入 PL2 Hypervisor——当 Guest Linux 尝试操作硬件而 Hypervisor 必须拦截时触发
- SMC(Secure Monitor Call,TrustZone 扩展):Normal World 跨安全状态进入 Secure World 的唯一合法通道
所有未识别指令触发 UNDEF 异常。手册列出三种典型使用场景:VFP/NEON 硬件的软件模拟(核心无相应协处理器时触发的 VFP 指令进入 UNDEF handler,handler 读取操作码在软件中完成浮点运算);协处理器的 lazy enable(首次使用 VFP 时触发 UNDEF → handler 启用 VFP 硬件 → 返回重试指令);以及用户断点——调试器通过将目标地址指令替换为 UNDEF 编码实现软件断点,UNDEF handler 检测到断点后将控制权交给调试器。
手册特别指出 ARM 的术语学:其他架构可能将 ARM 的所有异常统称为 “trap” 或 “interrupt”,但在 ARM 架构中这些词有特定含义——“interrupt” 专指 IRQ/FIQ,”trap” 很少使用。
六类异常类型已列齐。当多个异常同时触发时,优先级和向量表共同决定谁先被处理、跳到哪里。
2. 异常优先级与向量表
分类完成后,接下来回答”多个异常同时出现谁先处理”和”硬件如何找到对应的处理函数”——前者由优先级链和 CPSR 中断屏蔽位决定,后者由向量表结合 CP15 寄存器控制。
2.1 硬件自动完成的六步序列
当异常发生时,ARM 核心的硬件自动执行以下操作,无需任何软件干预:
- **拷贝 CPSR → SPSR_
**:将异常前的完整状态保存到目标模式的 banked SPSR - **保存返回地址到 LR_
**:存入 PC 的修正值(取决于异常类型) - 修改 CPSR[4:0] → 目标模式编码
- 设置其他 CPSR 位:
- T bit ← CP15 SCTLR.TE(保证异常处理在 ARM 或 Thumb 状态运行)
- J bit ← 0
- E bit ← CP15 SCTLR.EE(异常处理的端序独立于应用代码)
- 屏蔽中断:Reset → F=1 + I=1;FIQ → F=1 + I=1;其他 → I=1(IRQ 被禁用,FIQ 保持原样)
- PC ← 向量表中对应异常的入口地址
| 异常 | 模式 | CPSR 中断屏蔽 |
|---|---|---|
| Reset | SVC | F=1, I=1 |
| Undefined | UND | I=1 |
| SVC | SVC | I=1 |
| Prefetch Abort | ABT | I=1 |
| Data Abort | ABT | I=1 |
| IRQ | IRQ | I=1 |
| FIQ | FIQ | F=1, I=1 |
2.2 向量表的四种视图
异常向量表默认位于 0x00000000,但 Cortex-A 系列支持通过 CP15 SCTLR.V 移到 0xFFFF0000(Linux 内核的默认选择)。向量表有 8 个条目,每个 4 字节:
| 偏移 | 向量地址 | Non-secure | Secure | Hypervisor | Monitor |
|---|---|---|---|---|---|
0x00 |
0xFFFF0000 | 未使用 | Reset | Reset | 未使用 |
0x04 |
0xFFFF0004 | UNDEF | UNDEF | Hyp mode UNDEF | — |
0x08 |
0xFFFF0008 | SVC | SVC | SMC | SMC |
0x0C |
0xFFFF000C | Prefetch Abort | Prefetch Abort | Hyp Prefetch Abort | Prefetch Abort |
0x10 |
0xFFFF0010 | Data Abort | Data Abort | Hyp Data Abort | Data Abort |
0x14 |
0xFFFF0014 | 未使用 | 未使用 | Hyp Trap Entry | — |
0x18 |
0xFFFF0018 | IRQ | IRQ | IRQ | IRQ |
0x1C |
0xFFFF001C | FIQ | FIQ | FIQ | FIQ |
每个向量条目只有 4 字节——只能放一条指令(ARM)或两条 Thumb 指令。标准做法放一条 B <label>(PC 相对跳转,受 24-bit 偏移限制)或 LDR PC, [PC, #offset](加载 32-bit 绝对地址,代价多几个周期)。
2.3 优先级与嵌套
手册明确:ARM 架构不定义异步异常的取指时机,异步异常的优先级排序由具体实现决定。但有几条硬性规则:
- Reset 最高优先级,不可屏蔽
- FIQ 高于 IRQ 和 Abort
- 所有异常自动屏蔽 IRQ(置 CPSR.I=1),仅 Reset/FIQ 屏蔽 FIQ
这意味着:FIQ 可以打断正在执行的 Abort handler 或 IRQ handler。典型嵌套场景:Data Abort 先发生(优先级高于 FIQ)→ 核心进入 ABT 保存 LR → FIQ 立即打断 → FIQ handler 执行 → 返回 ABT → 继续处理 abort。
FIQ 相比 IRQ 有两个独特的速度优势:第一,FIQ 在向量表的最后一项(偏移 0x1C)——FIQ handler 代码可以直接放在 0x1C 位置开始执行,无需一条 B 指令的分支延迟。第二,FIQ 模式拥有私有的 R8_fiq–R12_fiq banked 寄存器——其他模式必须在 handler 入口处 PUSH 通用寄存器到栈上保护现场,FIQ 直接使用这些独立副本,节省了若干压栈周期。配合 FIQ 从不被内核屏蔽的优先级特性,FIQ 是为需要保证最快响应时间的单一高优先级外设保留的。
手册同时指出 FIQ 的核心限制:FIQ handler 不应触发任何其他异常——它的内存必须全映射完毕,不能使用 SVC 调用内核函数。这意味着 FIQ 无法调用内核 API,只能由不需要内核服务的独立设备驱动代码使用。这也正是 Linux 不使用 FIQ 的根本原因:内核的架构无关设计没有多级中断的概念,且所有中断处理路径最终都需要内核服务。
手册指出 SVC 和 UNDEF 互斥(都是指令触发),Prefetch Abort 和 SVC/UNDEF 也无法同时发生(被标记 abort 的指令无法又同时是 SVC)。
优先级和向量表决定了”去哪”,LR 决定了”怎么回来”——下一节解析异常返回时那条最关键的 SUBS PC, LR, #N 指令。
3. 异常返回与 LR 偏移调整
异常处理完了怎么回到被打断的代码?这看似简单的问题涉及 ARM 体系最微妙的细节——LR 寄存器里存的不是真正的返回地址,必须按异常类型做偏移修正。本节解析 -4/-8/0 三种修正值的流水线级来历,以及 S、^、RFE 三种返回路径的原子性保障。
3.1 为什么 LR 需要偏移修正?
异常触发时核心自动将返回地址存入 LR,但存入的不是下一条要执行的指令的地址——需要根据异常类型手动调整:
| 异常 | LR 调整 | 返回指令 | 返回的目标指令 |
|---|---|---|---|
| SVC | 0 | MOVS PC, R14 |
下一条指令 |
| UNDEF | 0 | MOVS PC, R14 |
下一条指令 |
| Prefetch Abort | -4 | SUBS PC, R14, #4 |
引发 abort 的指令(需重新取) |
| Data Abort | -8 | SUBS PC, R14, #8 |
引发 abort 的指令(精确时) |
| IRQ | -4 | SUBS PC, R14, #4 |
被打断的下一条指令 |
| FIQ | -4 | SUBS PC, R14, #4 |
被打断的下一条指令 |
手册指出这些值来自早期硬件的便利性而非设计规范——但要理解它们的工作方式,关键是知道 ARM 的三级流水线造成 PC 比当前执行指令超前 8 字节(ARM 状态)或 4 字节(Thumb 状态)。Data Abort 的 -8 修正正是为了回到 abort 指令本身(PC 超前 8,需回退 8);Prefetch Abort 需回到 abort 指令的地址(超前 4 个指令周期,LR 存的是 abort 指令 +4,回退 4)。
3.2 异常退出和三种返回机制
从异常返回时有两个操作必须原子完成:
- SPSR → CPSR 恢复原状态
- PC 跳回返回地址
ARM 提供三条路径从异常返回:
方法 1:带 S 后缀的数据处理指令写 PC
1 | SUBS PC, LR, #4 ; LR-4 → PC,同时 SPSR → CPSR(S 后缀是关键) |
注意 S 后缀——普通 SUB PC, LR, #4 只改 PC 不改 CPSR,异常返回时永远用 S 变体。
方法 2:带 ^ 修饰符的 LDM/POP
1 | LDMFD sp!, {R0-R12, PC}^ ; 恢复所有寄存器 + PC,同时 SPSR → CPSR |
^ 修饰符在 Load Multiple 写入 PC 时触发 CPSR 恢复。异常处理程序在入口处压栈所有工作寄存器 + 修正后的 LR,出口处一条指令完成全部恢复。
方法 3:RFE(Return From Exception)
1 | RFEFD sp! ; 从当前模式栈弹出 LR 和 SPSR 并跳转 |
手册 16-bit Thumb 指令不能用于异常返回——因为它们无法同时完成 PC 跳转和 CPSR 恢复。
配合 RFE 使用的还有 SRS(Store Return State) 指令。异常入口时,通常需要手动将 LR 和 SPSR 保存到栈上——SRS 将这两步合并为一条指令,且可以将返回状态压入任意模式的栈(由指令操作数指定),而不仅仅限于当前模式。这在异常处理程序需要切换模式后再保存状态的场景下尤为重要——例如从 IRQ 模式进入 SVC 模式处理、但需要把 LR_irq 和 SPSR_irq 保存到 SVC 栈上的场景。
1 | SRSFD sp!, #Mode_SVC ; 将 LR_irq 和 SPSR_irq 压入 SVC 模式栈 |
返回机制明确后,各个 handler 具体做什么?以下三节分述 Abort、UNDEF 和 SVC 处理器的软件职责。
4. 各异常处理器的软件职责
硬件把处理器切到了正确的模式和向量,接下来各个 handler 要在软件层面做什么?Abort handler 读 CP15 判断是缺页还是硬件错误,UNDEF handler 检查操作码决定是否软件模拟,SVC handler 解析调用号分派到内核函数——三者的处理逻辑截然不同。
4.1 Abort 处理器
Abort handler 通过 CP15 获取:
- DFAR/IFAR(Fault Address Register):保存发生 Data Abort 或 Prefetch Abort 时对应的 Fault Address(通常是导致异常的虚拟地址)
- DFSR/IFSR(Fault Status Register):异常原因——权限不足、外部 Abort、地址翻译失败
- LR(修正后):触发异常的指令地址
支持虚拟内存的系统中,handler 可以修复页表后返回重试(demand paging);不支持虚拟内存的系统中,Abort 意味着硬件错误,只能记录诊断信息后终止。
手册特别讨论了异步 Abort 与 CPSR A 位的交互机制。当 A=1 时,异步 Abort 不会立即进入异常,而是保持为 pending。
内核在重新使能异步 Abort(清除 A 位)或切换用户上下文前,通常会执行适当的同步屏障(如 DSB),确保此前内存访问产生的 pending asynchronous abort 被及时认领(避免进程 A 导致的 Abort 到进程 B 的上下文中才被处理)。如果需要因为 imprecise external abort 终止某个用户进程,异常应归属于真正发起该内存访问的进程,而不是之后被调度运行的进程。
4.2 UNDEF 处理器
当 CPU 尝试执行一条无法执行的指令时,就会进入 Undefined Instruction 异常。
常见情况有两种:
- 执行真正不存在(Undefined)的指令。
- 执行协处理器指令,但系统中没有对应的协处理器,或该协处理器当前无法处理该指令(例如编译器生成
VADD.F32,但 SoC 没有 VFP 硬件)。
UNDEF 的典型用途是实现软件模拟(Software Emulation)。例如核心没有 VFP 硬件时,执行 VFP 指令会触发 UNDEF,handler 根据指令操作码模拟对应的浮点运算。另一种典型用途是 Lazy Enable:首次执行 VFP 指令时,handler 启用 VFP 后返回重新执行该指令。
当系统需要模拟多个协处理器时,多个 UNDEF handler 可以链式调用(daisy-chain):当前 handler 无法识别某条指令时,将其交给下一个 handler 处理;如果最终没有任何 handler 能处理,则通常记录调试信息并终止程序。
1 | CPU 执行一条指令 |
4.3 SVC 处理器
SVC 是 Linux 系统调用的硬件入口。管理员调用(SVC)通常用于允许用户模式代码访问操作系统功能。例如,如果用户代码想要访问系统的特权部分(例如执行文件 I/O 操作),通常会使用管理员调用SVC指令来实现。
参数可以通过寄存器传递给SVC处理程序,或者(较少见的情况下)通过操作码中的注释字段传递。
手册给出了一段 Hello World 的完整示例:
1 | MOV R0, #1 @ stdout |
SVC handler 需区分 ARM/Thumb 状态——检查 SPSR[5](T bit)。这是因为 Thumb 的 SVC 指令仅 16-bit(8-bit 调用号,返回地址在 LR-2),而 ARM 的 SVC 是 32-bit(24-bit 调用号,LR-4 调整),在异常返回的时候要根据该状态找到正确的返回地址。
硬件层异常机制讲完,最后看 Linux 内核如何在这一套硬件基础上搭出架构无关的异常处理框架。
5. Linux 内核的异常处理架构
前四节覆盖了 ARM 硬件层面的异常机制——本节将这些机制映射到 Linux 内核的实际实现:异常 stub 如何统一路由到 SVC 模式、向量页如何通过 memcpy 部署、中断分发如何与调度器联动。
5.1 异常 stub:统一路由到 SVC 模式
手册 §12.5 揭示了 Linux ARM 端口的设计:所有异常(除 SVC 和 FIQ)首先进入一个汇编 stub,将核心切换到 SVC 模式后调用统一的 C 函数。这是因为 Linux 的跨平台异常处理框架不区分特权模式——所有处理逻辑都假设在 SVC 模式下运行。
5.2 向量页的初始化
手册给出了 Linux boot 过程中向量表的精确放置流程:
1 | // devicemaps_init() → arch/arm/mm/mmu.c: 分配 4KB 向量页,映射到 0xFFFF0000 |
三段内存布局:向量表在页首(0xFFFF0000)、异常 stub 在偏移 0x200、kuser helpers(内核提供的用户空间辅助代码页)在页尾。整个异常处理基础架构通过三次 memcpy 即可完成部署。
5.3 中断分发:__irq_usr 与 __irq_svc
IRQ 进入后由 __irq_usr(从 User 模式进入 IRQ)或 __irq_svc(从 SVC 模式进入 IRQ)处理。两者保存全部核心寄存器后进入轮询循环——用宏 get_irqnr_and_base 读取中断控制器、判断是否有待处理中断、有则调用 do_IRQ(arch/arm/kernel/irq.c)。循环持续到所有中断处理完毕。返回前检查是否需要调用内核调度器——中断可能唤醒了更高优先级的任务,需要上下文切换。
从 ARM 硬件的六步异常入口到 Linux 的三层 memcpy 向量部署——异常处理的完整链条以以下六条核心结论收束。
6. 学习要点总结
ARM 的异常模型是 mode-based 而非 vector-only。每种异常不仅提供向量地址,还将核心硬切换到专用模式——模式决定了 banked 寄存器和栈,这比 x86 的中断门机制更高效。
Prefetch Abort 不到执行不触发。取指时标记 aborted 的指令,如果流水线被冲刷(分支未命中),abort 永远不会发生——这是处理 speculative execution corner case 的关键。
Data Abort 的 -8 调整来自 ARM 三级流水线的历史遗产——PC 比正在执行的指令超前 8 字节,LR 保存的是 PC 值而非指令地址。理解偏移修正的本质是理解 ARM 流水线行为。
Imprecise Asynchronous Abort 是硬件的底线——一旦发生只能杀进程,无法修复返回。探测外设时需特别小心:对不存在的 Device 区域读可能触发 imprecise abort,即使页表标记为 Strongly-ordered 或 Device 也无法保证精确。
异常返回的两个操作(SPSR→CPSR + PC跳转)必须原子——
S后缀的数据处理指令、^修饰符的 LDM 和 RFE 是三条等效路径,但 16-bit Thumb 指令三者皆不具备。Linux 将所有异常路由到 SVC 模式——通过异常 stub 模式切换后进入统一的 C 处理框架。
devicemaps_init分配向量页后,trap_init用三条memcpy将向量表、stub 和 kuser helpers 拷贝到位。FIQ 不用于 Linux——内核架构无关的设计没有多级中断的概念。FIQ 在系统中优先级最高(内核从不屏蔽 FIQ),谨慎的系统设计可以将其用于极低延迟的专属外设。