参考:
《ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition》
《ARM Cortex™-A Series Version: 4.0 Programmer's Guide》
ARMv7-A 系列其他文章:
1、ARMv7-A 异常概述
异常(Exception) 是指任何需要核心暂停当前正常执行流、转而运行一段专用特权代码(异常处理程序,Exception Handler)的条件或系统事件。处理完成后,特权软件负责准备核心恢复到异常发生前的状态继续执行。
在很多资料里,exception、interrupt、trap 这几个词经常混着用,但在 ARM 语境里最好区分开:
- Exception:所有打断正常执行流的事件的统称
- Interrupt:特指 IRQ 和 FIQ 这两种异步硬件中断
- Trap:非 ARM 官方术语,在虚拟化语境下有时指 Hyp Trap
从程序员视角看,理解异常机制,本质上就是回答三个问题:
- 从哪来 ------ 什么事件触发了异常?
- 到哪去 ------ 处理器跳到哪个地址执行处理程序?
- 怎么回 ------ 处理完毕后如何恢复被中断的程序?
ARMv7-A 的异常处理有一个很重要的特点:它和处理器模式是紧密绑定的。不同类型的异常,会让处理器自动进入不同的模式;同时,硬件还会自动保存现场的一部分关键状态,例如:
- 把当前
CPSR保存到目标模式的SPSR - 把返回地址写入目标模式的
LR - 根据异常类型设置中断屏蔽位
- 把
PC重定向到异常向量表对应入口
这也是 ARM 异常机制非常"硬件化"的地方:异常入口的大量基础动作,硬件已经替软件做掉了。

2、异常类型详解
ARMv7-A 常见的异常类型如下表所示。表中的"向量偏移"是相对于异常向量表基址而言的偏移,而不是绝对地址。
| 异常类型 | 进入模式 | 向量偏移 | 触发方式 | 同步/异步 |
|---|---|---|---|---|
| Reset | SVC | 0x00 |
复位信号 | 异步 |
| Undefined Instruction | UND | 0x04 |
未定义指令 / 未知协处理器指令 | 同步 |
| SVC | SVC | 0x08 |
SVC 指令 |
同步 |
| HVC | HYP | 0x08 (Hyp 向量表) |
HVC 指令(虚拟化扩展) |
同步 |
| SMC | MON | 0x08 (Monitor 向量表) |
SMC 指令(安全扩展) |
同步 |
| Prefetch Abort | ABT | 0x0C |
指令预取失败 | 同步 |
| Data Abort | ABT | 0x10 |
数据访问失败 | 同步 |
| Hyp Trap | HYP | 0x14 |
Hyp 模式下的异常入口(虚拟化) | --- |
| IRQ | IRQ | 0x18 |
普通中断信号 | 异步 |
| FIQ | FIQ | 0x1C |
快速中断信号 | 异步 |

说明
- ARMv7-A 在引入安全扩展和虚拟化扩展后,可能存在多套向量表,例如 Non-secure、Secure、Monitor、Hyp。
- 上表中的偏移值,例如
0x18、0x1C,都只是"表内偏移",实际入口地址还要结合向量表基址一起看。
2.1 IRQ 与 FIQ
ARMv7-A 提供两类外部中断请求信号:
- IRQ(Interrupt Request):普通中断,系统中绝大多数外设中断都走这条路径。
- FIQ (Fast Interrupt Request):快速中断,优先级高于 IRQ,并且具备硬件层面的速度优势:FIQ 模式拥有私有的 R8-R12,处理程序无需压栈即可直接使用,进一步减少时钟周期开销
设计原则 :FIQ 预留给单一的、需要保证快速响应时间的高优先级中断源,并且 FIQ 处理程序被期望不再产生任何其他异常(不能有 SVC 调用,不能触发缺页等)。这是因为 FIQ 不会禁用自身,如果再产生新的异常,处理将变得极其复杂。
一个关键的差异在于中断屏蔽行为:
- 进入 IRQ 模式时,硬件自动置位
CPSR.I = 1,也就是屏蔽后续IRQ;但CPSR.F不会因此自动置位,所以FIQ仍然可以抢占IRQ。 - 进入 FIQ 模式时,硬件会同时置位
CPSR.F = 1和CPSR.I = 1,也就是在处理FIQ期间同时屏蔽FIQ和IRQ。
在 Linux 世界里,FIQ 并不是通用主路径。主流 Linux 内核日常主要使用 IRQ 体系;FIQ 往往只在一些非常特定的 SoC 方案、调试方案或安全方案里才会被使用。
部分 Cortex-A 处理器还支持把 FIQ 配置成 NMFI(Non-Maskable FIQ) 。在这种配置下,软件不能简单通过修改 CPSR.F 来长期屏蔽 FIQ,这通常由硬件配置决定。
2.2 Abort(中止异常)
Abort 是最复杂的异常类型,由失败的内存访问触发。按来源可分为两个维度:
(1)按访问阶段:
| 类型 | 向量偏移 | 触发时机 | LR 调整值 |
|---|---|---|---|
| Prefetch Abort | 0x0C |
指令预取失败。异常在指令执行前触发。 | LR - 4 |
| Data Abort | 0x10 |
数据读写失败。异常在 load/store 指令尝试执行后触发。 | LR - 8 |
为什么两者的返回修正不同?根本原因在于 ARM 流水线导致异常发生时的 PC 已经不是"当前那条指令的地址"。
- 对 Prefetch Abort 来说,处理器是在取指过程中发现问题,因此返回时通常用
LR - 4回到故障指令。 - 对 Data Abort 来说,错误是在数据访问阶段暴露出来的,此时流水线更靠后,因此通常要用
LR - 8才能回到出问题的那条指令。
这也是为什么异常返回代码里经常会看到:
asm
SUBS PC, LR, #4 ; Prefetch Abort / IRQ / FIQ 常见形式
SUBS PC, LR, #8 ; Data Abort 常见形式
(2)按同步属性:
| 类型 | 含义 |
|---|---|
| 同步 Abort | 由指令流执行直接触发,返回地址能够精确定位导致 abort 的那条指令。 |
| 精确异步 Abort | 外部内存系统报告的错误,但 abort 处理程序可以确定是哪条指令导致的,且此后再无其他指令执行。 |
| 不精确异步 Abort | 外部内存系统对某次无法识别的内存访问报告了错误。例如缓冲写操作收到错误响应时,之后已有其他指令被执行。处理程序无法定位问题指令,只能终止导致问题的进程。 |
MMU 引起的权限错误、翻译错误,通常都属于同步 Abort。这类异常最适合做缺页处理、权限检查、进程杀死等标准操作系统路径。
不精确异步 Abort 更麻烦。它往往来自总线、写缓冲、外部内存系统错误,而且报错可能"晚到"。等异常真正进入时,处理器可能已经继续执行了后面的多条指令。
这时 CPSR.A 位就很重要了。它控制的是异步 abort 的屏蔽/延迟处理行为。操作系统会结合 barrier 指令,尽量把异步错误和正确的上下文对应起来,避免"前一个进程做错了事,后一个进程来背锅"。
如何定位 Abort 现场?
分析 Abort 时,最关键的通常有三类信息:
- 故障状态 :例如 CP15 的
DFSR/IFSR - 故障地址 :例如 CP15 的
DFAR/IFAR - 返回地址 :也就是进入异常时保存下来的
LR_abt
其中:
DFSR(Data Fault Status Register)记录 Data Abort 的原因DFAR(Data Fault Address Register)记录 Data Abort 的相关地址IFSR/IFAR则对应取指相关故障
把这些寄存器信息和修正后的返回地址结合起来,异常处理程序才能判断:
- 这是缺页,可以修页表后重试
- 这是权限错误,应当向用户态抛异常
- 这是总线错误或严重硬件故障,应当终止当前任务甚至停机
2.3 Reset
Reset 是最高优先级异常。它不属于"程序运行过程中某条指令触发的异常",而是整个处理器执行环境被重新拉回初始状态的入口。
复位后,处理器通常进入 SVC 模式,并从复位向量开始执行。常见的初始化工作包括:
- 建立异常向量表
- 初始化栈
- 初始化时钟、内存控制器、串口等基础硬件
- 初始化 MMU/Cache
- 初始化 VFP/NEON
- 为多核系统唤醒其他 CPU
- 最后跳入
main()或更上层的启动代码
如果是多核系统,通常并不是每个核都完整跑一遍同样的启动流程。更常见的做法是:主核负责系统初始化,从核先等待,后续再被主核唤醒。
2.4 指令触发的异常
除了外部事件和硬件错误,ARM 里还有一类异常是软件主动触发的。它们本质上是在请求更高特权级提供服务。
| 指令 | 进入模式 | 用途 | 扩展要求 |
|---|---|---|---|
SVC |
SVC (PL1) | User 模式请求操作系统服务(系统调用) | 无 |
HVC |
HYP (PL2) | Guest OS 请求 Hypervisor 服务 | 虚拟化扩展 |
SMC |
MON (PL1, Secure) | Normal World 请求 Secure World 服务 | 安全扩展 |
此外,任何核心无法识别的指令(包括不存在或不使能的协处理器指令)会触发 UNDEFINED 异常。这个机制的重要应用之一是指令模拟------比如硬件 VFP 未实现或不使能时,通过 UNDEFINED 异常处理程序来用软件模拟浮点运算。
在 Thumb 状态执行
SVC时,取 SVC 立即数的方法和 ARM 状态不同。异常处理程序通常需要结合SPSR.T判断调用方来自 ARM 还是 Thumb,再决定如何解析原始指令编码。
3、异常优先级
当多个异常同时发生时,硬件按照固定的优先级顺序处理。每种异常进入时对 CPSR.I 和 CPSR.F 屏蔽位的行为也不同:
| 优先级 | 异常 | 进入模式 | CPSR.I | CPSR.F |
|---|---|---|---|---|
| 最高 | Reset | SVC | 1 | 1 |
| Data Abort | ABT | 1 | 不变 | |
| FIQ | FIQ | 1 | 1 | |
| IRQ | IRQ | 1 | 不变 | |
| Prefetch Abort | ABT | 1 | 不变 | |
| 最低 | Undefined / SVC | UND / SVC | 1 | 不变 |
两个关键推论:
- FIQ 可以打断 IRQ 和 Abort 处理程序 。如果 Data Abort 和 FIQ 同时发生,Data Abort(更高优先级)先被处理。因为 Data Abort 不屏蔽 FIQ(F 位不变),核心随后立即进入 FIQ 处理程序。FIQ 处理完毕后再返回继续 Data Abort。
- 未定义指令和 SVC 是互斥的------它们都由执行指令触发,一条指令不可能既是 SVC 又是未定义指令。同理,Prefetch Abort 标记了无效指令,也不可能与 Undef / SVC 同时发生。
注意 :ARM 架构并未定义异步异常(FIQ、IRQ、异步 Abort)的确切采样时机。因此异步异常相对于同步异常的优先级实际上是 implementation defined。
4、异常向量表
异常向量表可以理解成:处理器遇到某类异常后,第一跳会去查的一张固定入口表。
在经典 ARM 状态下,向量表由一组固定偏移的入口组成,每个入口只有 4 字节空间,因此通常放不下完整处理逻辑,而只是放一条跳转指令。
4.1 向量表布局
引入安全扩展和虚拟化扩展之后,ARMv7-A 可能维护多套向量表,分别用于不同执行环境:
| 向量偏移 | Normal 模式 | Secure 模式 | Hyp 模式 | Monitor 模式 |
|---|---|---|---|---|
0x00 |
Reset | Reset | --- | --- |
0x04 |
Undefined | Undefined | Undefined (from Hyp) | --- |
0x08 |
SVC | SVC | --- | SMC |
0x0C |
Prefetch Abort | Prefetch Abort | Prefetch Abort (from Hyp) | Prefetch Abort |
0x10 |
Data Abort | Data Abort | Data Abort (from Hyp) | Data Abort |
0x14 |
--- | --- | Hyp Trap Entry | --- |
0x18 |
IRQ | IRQ | IRQ | IRQ |
0x1C |
FIQ | FIQ | FIQ | FIQ |
其中 0x14 这个偏移在传统 ARMv7-A 里是保留的;有了虚拟化扩展之后,它被 Hyp 模式用作专门的 trap 入口。
4.2 向量表基址配置
异常入口的"偏移"固定,但整张表放在哪里,是由基址决定的。
在 ARMv7-A 中,向量表基址主要由 SCTLR.V 和 VBAR 共同决定:
SCTLR.V |
向量表基址来源 | 说明 |
|---|---|---|
0 |
VBAR | 向量表可放在任意物理/虚拟地址,由 CP15 c12, c0, 0 指定 |
1 |
0xFFFF0000 |
高向量(HIVECS)模式,向后兼容旧 ARM 架构 |
这里要注意两个容易混淆的点:
VBAR并不是"任意值都行",它本身有对齐要求。- 当启用高向量时,异常入口不再从
VBAR + offset取,而是改为固定从高地址区域取。
如果处理器支持安全扩展或虚拟化扩展,还会额外有:
MVBAR:Monitor 向量表基址HVBAR:Hyp 向量表基址


4.3 CP15 异常配置关键寄存器
| 寄存器 | CP15 编码 | 用途 |
|---|---|---|
| VBAR | c12, c0, 0 |
Non-secure / Secure PL1 向量表基址(Banked) |
| MVBAR | c12, c0, 1 |
Monitor 模式向量表基址 |
| HVBAR | c12, c4, 0 |
Hyp 模式向量表基址 |
| SCTLR.V | c1, c0, 0 bit 13 |
向量表基址选择(0 = VBAR, 1 = 0xFFFF0000) |
| SCTLR.TE | c1, c0, 0 bit 30 |
异常处理指令集(0 = ARM, 1 = Thumb) |
| SCTLR.EE | c1, c0, 0 bit 25 |
异常处理字节序(0 = 小端, 1 = 大端) |
Monitor 模式向量表基址寄存器:

支持 Hypervisor 扩展时的 Hyp 向量表基址寄存器:

4.4 向量表入口的两种跳转模式
每个异常入口只有 4 字节空间,因此向量表中几乎总是放置以下两种跳转指令之一:
(1)PC 相对分支:
asm
B <handler_label>
优点是快、简单;缺点是跳转范围有限。ARM 状态下 B 指令可覆盖大约 ±32MB 的范围。
(2)间接加载:
asm
LDR PC, [PC, #offset]
它的思路是:先从附近取出一个绝对地址,再把这个地址装入 PC。这样处理程序可以放在更远的位置,布局更灵活。
这也是很多内核源码里常见的写法。
5、异常进入与返回
5.1 硬件自动行为(异常进入)
当异常发生时,ARM 核心硬件自动 完成以下四步操作------这些是程序员不需要写代码实现的:
Step 1: CPSR → SPSR_<mode> // 将当前状态保存在目标模式的 SPSR 中
Step 2: 返回地址 → LR_<mode> // 将返回地址写入目标模式的 LR(返回地址是异常发生时的 PC 值)
Step 3: 修改 CPSR // 切换模式、设置 T/J/E 位、置中断屏蔽位
Step 4: PC → 向量表对应入口 // 跳转到异常处理程序
第三步中 CPSR 的变化细节值得展开:
CPSR.M[4:0]:设置为异常类型对应的处理器模式编码。CPSR.{A, I, F}:根据异常类型对照表(见第 3 节)自动置位或保持不变。CPSR.T:设置为SCTLR.TE的值------无论被中断的代码处于 ARM 还是 Thumb 状态,异常处理程序可以统一使用一种指令集。CPSR.J:清零。CPSR.E:设置为SCTLR.EE的值,同样统一异常处理的字节序。
5.2 异常返回时,为什么总要修正 LR
从异常处理返回需要原子地完成两个操作:
- 将
SPSR_<mode>恢复到 CPSR(恢复处理器模式、中断屏蔽位、指令集状态) - 将 PC 设置为修正后的返回地址
ARM 的三级流水线意味着保存到 LR_<mode> 里的值,往往不是你想直接返回的最终地址移。更关键的是,ARM 架构要求 Prefetch Abort 和 Undefined 异常必须能够重新执行导致异常的那条指令(例如缺页处理程序修复页表后要重新执行触发 abort 的 load/store),而非直接跳到下一条。这也意味着不同类型的异常需要不同的 LR 修正值:
| 异常类型 | LR 调整 | 典型返回指令 | 返回目标 |
|---|---|---|---|
| SVC / Undef | LR + 0 |
MOVS PC, LR |
SVC: 下一条指令 / Undef: 重执行当前指令 |
| Prefetch Abort | LR - 4 |
SUBS PC, LR, #4 |
重新执行导致 abort 的指令 |
| Data Abort | LR - 8 |
SUBS PC, LR, #8 |
重新执行导致 abort 的指令 |
| IRQ / FIQ | LR - 4 |
SUBS PC, LR, #4 |
被中断的下一条指令 |
注意
SUBS中的S后缀 :当目标寄存器是 PC 时,S后缀表示同时将 SPSR 恢复到 CPSR。在 ARM 架构中,这一条指令同时完成 PC 跳转和 CPSR 恢复,是整个异常返回机制的精髓。这也是MOVS PC, LR被特意设计为异常返回指令的原因------普通的MOV PC, LR不会恢复 SPSR。
5.3 三种返回方式
| 方式 | 指令示例 | 适用场景 |
|---|---|---|
| 数据处理指令 + S | SUBS PC, LR, #4 |
最简单的返回方式,处理程序未使用栈保存上下文 |
| 多寄存器加载 + ^ | LDMFD sp!, {R0-R12, PC}^ |
处理程序入口将各寄存器保存在栈上,返回时统一恢复。^ 后缀表示同时恢复 SPSR |
| RFE 指令 | RFEFD sp! |
将栈上的 LR 和 SPSR 分别恢复到 PC 和 CPSR,一步完成。语义更清晰,推荐在新代码中使用 |
关键约束:不能使用 16 位 Thumb 指令返回异常,因为它们无法操作 CPSR/SPSR。异常返回必须使用 32 位 ARM 指令(或等价的 32 位 Thumb-2 指令)。
6、结合 Linux 源码理解向量表
6.1 Linux 中的异常向量表示例
Linux ARM 代码里,异常向量表通常会放在类似下面的位置:
arch/arm/kernel/entry-armv.S
asm
.section .vectors, "ax", %progbits
W(b) vector_rst
W(b) vector_und
ARM( .reloc ., R_ARM_LDR_PC_G0, .L__vector_swi )
THUMB( .reloc ., R_ARM_THM_PC12, .L__vector_swi )
W(ldr) pc, .
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq
W(b) vector_fiq
这段代码可以这样理解:
W(b) vector_rst:在 Reset 向量入口放一条跳转指令W(b) vector_und:在 Undefined 向量入口放一条跳转指令W(ldr) pc, .:在当前入口放一条ldr pc, ...形式的间接跳转ARM(...)和THUMB(...):分别为 ARM 和 Thumb 场景生成对应的重定位信息
可以把它理解成:向量表本身并不负责做复杂处理,它只负责把异常快速导流到真正的处理函数入口。
这也是异常向量表设计的典型思路:
- 向量表入口尽量短
- 真正复杂的现场保存和异常分发,交给后面的 handler
如果你在读 Linux 源码时发现某些向量入口没有直接写成 b handler,而是用了 ldr pc, ... 或重定位技巧,不要奇怪。那通常只是为了兼顾链接布局、Thumb 支持或地址范围限制,本质上仍然是在做"第一跳"。