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 核心的硬件自动执行以下操作,无需任何软件干预

  1. **拷贝 CPSR → SPSR_**:将异常前的完整状态保存到目标模式的 banked SPSR
  2. **保存返回地址到 LR_**:存入 PC 的修正值(取决于异常类型)
  3. 修改 CPSR[4:0] → 目标模式编码
  4. 设置其他 CPSR 位
    • T bit ← CP15 SCTLR.TE(保证异常处理在 ARM 或 Thumb 状态运行)
    • J bit ← 0
    • E bit ← CP15 SCTLR.EE(异常处理的端序独立于应用代码)
  5. 屏蔽中断:Reset → F=1 + I=1;FIQ → F=1 + I=1;其他 → I=1(IRQ 被禁用,FIQ 保持原样)
  6. 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
2
SUBS  PC, LR, #4      ; LR-4 → PC,同时 SPSR → CPSR(S 后缀是关键)
MOVS PC, R14 ; LR → PC,同时 SPSR → CPSR

注意 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
2
SRSFD  sp!, #Mode_SVC      ; 将 LR_irq 和 SPSR_irq 压入 SVC 模式栈
; 随后切换到 SVC 模式,RFEFD 从 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 CPU 执行一条指令


能识别吗?

┌────┴────┐
│ │
可以 不可以
│ │
执行 Undefined Exception


Undefined Handler

┌───────┼────────┬─────────┐
│ │ │ │
软件模拟 开启VFP 调试断点 真非法指令
│ │ │ │
▼ ▼ ▼ ▼
返回继续 重执行 调试器接管 SIGILL/终止程序

4.3 SVC 处理器

SVC 是 Linux 系统调用的硬件入口。管理员调用(SVC)通常用于允许用户模式代码访问操作系统功能。例如,如果用户代码想要访问系统的特权部分(例如执行文件 I/O 操作),通常会使用管理员调用SVC指令来实现。

参数可以通过寄存器传递给SVC处理程序,或者(较少见的情况下)通过操作码中的注释字段传递。

手册给出了一段 Hello World 的完整示例:

1
2
3
4
5
MOV   R0, #1          @ stdout
ADR R1, msgtext @ 消息地址
MOV R2, #13 @ 长度
MOV R7, #4 @ sys_write 系统调用号
SVC #0 @ 触发 SVC 异常

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
2
3
4
5
6
// devicemaps_init() → arch/arm/mm/mmu.c: 分配 4KB 向量页,映射到 0xFFFF0000

// trap_init() → arch/arm/kernel/traps.c: 拷贝三段内容到向量页
memcpy((void *)vectors, __vectors_start, vectors_size);
memcpy((void *)vectors + 0x200, __stubs_start, stubs_size);
memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);

三段内存布局:向量表在页首(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_IRQarch/arm/kernel/irq.c)。循环持续到所有中断处理完毕。返回前检查是否需要调用内核调度器——中断可能唤醒了更高优先级的任务,需要上下文切换。

从 ARM 硬件的六步异常入口到 Linux 的三层 memcpy 向量部署——异常处理的完整链条以以下六条核心结论收束。


6. 学习要点总结

  1. ARM 的异常模型是 mode-based 而非 vector-only。每种异常不仅提供向量地址,还将核心硬切换到专用模式——模式决定了 banked 寄存器和栈,这比 x86 的中断门机制更高效。

  2. Prefetch Abort 不到执行不触发。取指时标记 aborted 的指令,如果流水线被冲刷(分支未命中),abort 永远不会发生——这是处理 speculative execution corner case 的关键。

  3. Data Abort 的 -8 调整来自 ARM 三级流水线的历史遗产——PC 比正在执行的指令超前 8 字节,LR 保存的是 PC 值而非指令地址。理解偏移修正的本质是理解 ARM 流水线行为。

  4. Imprecise Asynchronous Abort 是硬件的底线——一旦发生只能杀进程,无法修复返回。探测外设时需特别小心:对不存在的 Device 区域读可能触发 imprecise abort,即使页表标记为 Strongly-ordered 或 Device 也无法保证精确。

  5. 异常返回的两个操作(SPSR→CPSR + PC跳转)必须原子——S 后缀的数据处理指令、^ 修饰符的 LDM 和 RFE 是三条等效路径,但 16-bit Thumb 指令三者皆不具备。

  6. Linux 将所有异常路由到 SVC 模式——通过异常 stub 模式切换后进入统一的 C 处理框架。devicemaps_init 分配向量页后,trap_init 用三条 memcpy 将向量表、stub 和 kuser helpers 拷贝到位。

  7. FIQ 不用于 Linux——内核架构无关的设计没有多级中断的概念。FIQ 在系统中优先级最高(内核从不屏蔽 FIQ),谨慎的系统设计可以将其用于极低延迟的专属外设。