参考:
《ARM Cortex-A Series Version: 1.0 Programmer's Guide for ARMv8-A》
《Arm Architecture Reference Manual Armv8, for Armv8-A architecture profile》
ARMv8-A 系列其他文章:
1. 概念澄清:异常 vs 中断
在日常交流中我们常说"中断",但在 ARM 的术语体系中,打断软件执行流的所有事件统称为异常(exception),中断只是异常的一个子集。更精确地讲:
- 异常:需要特权软件(exception handler)介入才能保证系统正常运行的条件或系统事件。每种异常类型都有对应的 handler。handler 执行完毕后,特权软件负责恢复 core 在异常发生前的执行上下文,使其继续运行。
- 中断 :特指 IRQ 和 FIQ 这两种异步异常,由 core 外部的硬件信号触发,与 core 当前执行的指令流没有直接关联。
理清这个关系后,下文将统一使用 ARM 的标准术语------异常。
2. 异常分类速览
ARMv8-A 将异常分为两大类:同步异常 和异步异常。
2.1 同步异常(Synchronous)
同步异常由指令流的执行或尝试执行直接触发,返回地址能够精确指明导致异常的指令。主要来源包括:
- MMU 产生的 abort :
- Instruction Abort(如从标记为 Execute Never 的内存区域取指)
- Data Abort(如权限失败、对齐检查失败、Access flag fault)
- SP 和 PC 对齐检查
- 同步外部 abort(如读取 translation table 时出错)
- 未定义指令(Unallocated instructions)
- Debug 异常
- Service Call:SVC、HVC、SMC------用于向更高 Exception level 请求服务
同步异常可能是 OS 正常运作的一部分。例如 Linux 中,当任务申请新内存页时,正是通过 MMU Data Abort 机制触发缺页处理。
ARMv7-A 中 prefetch abort、Data Abort、undef 是各自独立的异常向量。AArch64 将它们统一为 Synchronous abort,handler 通过 ESR_ELn(下详)区分具体原因。
2.2 异步异常(Asynchronous)
异步异常不由指令执行直接产生,返回地址通常无法精确指示异常根源。包括三类:
- IRQ(普通优先级中断)
- FIQ(快速中断,优先级高于 IRQ)
- SError(System Error)
IRQ 和 FIQ 本质上是对应 core 输入引脚的物理信号。实际系统中,各类中断源通过 中断控制器 汇聚------最典型的是 Generic Interrupt Controller(GIC)。GIC 负责仲裁和优先级排序,最终将串行化后的信号接入 core 的 IRQ/FIQ 引脚。当引脚有效且未被屏蔽时,core 在当前指令执行完毕后响应异常。
SError 最常见的原因是异步 Data Abort:当 dirty cache line 被写回外部内存时,若内存系统(DDR、总线、互连、外设等)返回错误,由于最初发起写入的指令早已执行完毕,处理器无法将错误归因到某条具体指令,只能以 SError 形式异步报告。
2.3 异常优先级
当多个异常同时挂起时,处理器按固定优先级裁决:
| 优先级 | 异常类型 | 说明 |
|---|---|---|
| 最高 | Reset | 不可屏蔽,上电后立即响应 |
| ↓ | Synchronous | 指令执行触发的同步异常 |
| ↓ | SError | 系统错误 |
| ↓ | FIQ | 快速中断 |
| ↓ | IRQ | 普通中断 |
| 最低 | Debug | 调试异常(优先级最低,可被其他异常抢占) |
需要注意的是,异常不会被路由到 EL0。异常处理总是在当前或更高的 Exception level 中进行(详见第 5 章)。
3. 硬件自动行为:异常入口与返回
在深入寄存器之前,先理解硬件在异常发生时自动完成了哪些动作,有助于建立全局认知。
3.1 异常入口(Taking an Exception)
异常触发时,硬件严格按以下顺序执行三步操作:
-
PSTATE → SPSR_ELn:将当前处理器的完整状态(CurrentEL、DAIF、NZCV 等关键字段)压缩写入目标 EL 对应的 SPSR_ELn,形成一份"状态快照"。
-
PSTATE 更新:反映新的处理器状态。此时:
- Exception level 可能提升(例如 EL0→EL1),也可能保持不变(EL1→EL1)
- 对应中断类型的掩码位自动置位(例如 IRQ 触发后,PSTATE.I=1,屏蔽嵌套 IRQ)
- 执行状态可能从 AArch32 切换到 AArch64(见 3.3)
-
返回地址 → ELR_ELn:将首选返回地址写入 ELR_ELn,具体地址取决于异常类型(详见 4.2)。
然后处理器跳转到向量表中对应异常的入口(详见第 6 章),开始执行 handler。
走读示例 :一个 EL0 应用程序执行 SVC 0x42 发起系统调用:
1. 硬件检测到 SVC 指令 → 同步异常,路由到 EL1
2. PSTATE 保存到 SPSR_EL1(记录:当前 EL0、AArch64、DAIF 原始值等)
3. PSTATE 更新:CurrentEL = EL1,PSTATE.I = 1(屏蔽 IRQ)
4. ELR_EL1 = SVC 的下一条指令地址(handler 返回后从调用点之后继续)
5. 跳转到 VBAR_EL1 + 对应偏移(来自 EL0 AArch64 的同步异常向量)
6. Handler 读取 ESR_EL1 识别出是 SVC,且调用号为 0x42
7. 执行对应服务逻辑...
8. 执行 ERET → SPSR_EL1 恢复 PSTATE(回到 EL0),ELR_EL1 恢复 PC
9. 应用程序在 SVC 之后继续执行,仿佛什么都没发生
3.2 异常返回(ERET)
异常返回通过 ERET 指令完成,硬件反向执行入口时的保存操作:
- SPSR_ELn → PSTATE:恢复异常发生前的处理器状态(包括 Exception level、DAIF、NZCV 等)
- ELR_ELn → PC:从首选返回地址继续执行
这就是为什么异常返回不能使用普通的 RET 指令------RET 仅恢复 PC(来自 X30),而异常返回还需要同时恢复整个 PSTATE(来自 SPSR_ELn)。两套机制的对比见下表:
| 函数调用返回 | 异常返回 | |
|---|---|---|
| 寄存器 | X30(LR) | ELR_ELn |
| 触发方式 | BL / BLR 指令 | 硬件自动(异常入口) |
| 返回指令 | RET | ERET |
| 返回后 PC 来源 | X30 | ELR_ELn |
| 是否恢复 PSTATE | 否 | 是(从 SPSR_ELn) |
ARMv8-A 的异常返回模型显著简化于 ARMv7-A。ARMv7-A 出于向后兼容原因,从某些异常返回时需要手动对 LR 值减 4 或减 8;而 ARMv8-A 的 ELR 直接指向正确地址,handler 无需做任何算术修正。
3.3 AArch32 ↔ AArch64 状态切换
处理器只能在异常入口和异常返回时切换执行状态:
- 从低 EL 到高 EL:可保持 AArch32,也可切换到 AArch64
- 从高 EL 到低 EL:可保持 AArch64,也可切换到 AArch32
例如,一个 AArch32 的 EL0 应用触发异常进入 AArch64 的 EL1 内核后,handler 可以在 ERET 时通过 SPSR_EL1.M 字段将执行状态切回 AArch32。
有一个边界情况:若异常从 AArch32 的 EL 进入 AArch64 的 EL,且写入了目标 EL 对应的 FAR_ELn,则 FAR_ELn 的高 32 位全部置零(详见 4.4)。
4. 核心寄存器详解
4.1 SPSR_ELn --- 处理器状态快照
当异常被触发时,硬件将 PSTATE 保存到 SPSR_ELn(Saved Program Status Register)。SPSR_EL3、SPSR_EL2、SPSR_EL1 分别对应 EL3、EL2、EL1。
可以这样理解:PSTATE 是"当前状态",SPSR 是异常发生那一刻的"状态快照"。ERET 时,处理器从 SPSR 恢复 PSTATE,被中断的代码流完全感知不到异常的发生。

M 字段(bit 4)--- 执行状态
记录异常发生时的执行状态:0 = AArch64,1 = AArch32。这是 ERET 时恢复正确执行模式的关键依据。

DAIF --- 异常掩码位
用于屏蔽各类异常事件。对应位被置位(=1)时,异常不会被响应:
| 位 | 含义 |
|---|---|
| D | Debug 异常掩码 |
| A | SError 掩码(A 取 Abort 之意) |
| I | IRQ 掩码 |
| F | FIQ 掩码 |
一个常见的误解是把 DAIF 等同于"关中断"------实际上它不仅控制 IRQ/FIQ,还管 Debug 和 SError。命名也直接体现了这一点:D ebug、SError(A bort)、I RQ、FIQ。
SPSel --- 栈指针选择
控制当前 EL 使用 SP_ELn 还是 SP_EL0。除 EL0 外(EL0 只能使用 SP_EL0),所有 EL 都支持此选择。详见 4.5。
IL --- 非法执行状态标记
置位时,下一条指令的执行会触发异常。典型场景:试图在配置为 AArch32 的 EL2 上以 AArch64 状态执行 ERET 时,处理器检测到非法状态组合,通过此位报错。
SS --- 软件单步
Debug 相关,详见 ARMv8 Debug 章节。调试器置位此位后,处理器每执行一条指令即触发一次 Debug 异常,实现单步调试。
4.2 ELR_ELn --- 首选返回地址
ELR(Exception Link Register)保存的是首选返回地址(preferred return address) 。这个地址取决于异常类型,handler 无需手动修正:
-
异步异常(IRQ / FIQ / SError) :ELR 指向因响应中断而尚未执行或未完整执行的第一条指令。中断返回后从这里继续,对被中断的代码完全透明。
-
SVC / HVC / SMC(系统调用类同步异常) :ELR 指向触发异常的指令的下一条指令。系统调用是一次主动的"服务请求",调用者不希望返回后再执行一次调用指令,而是从调用点之后继续。
-
其他同步异常(Data Abort、Undefined Instruction 等) :ELR 指向产生异常的那条指令本身 。这类异常往往是可恢复的(如缺页、COW),handler 修复条件后需要重新执行同一条指令。
下图清晰展示了三类场景下 ELR 的指向差异:


Handler 可以修改 ELR:ELR 不是只读的。handler 可以通过修改 ELR_ELn 实现一些高级场景,例如同步 abort 后跳过问题指令直接执行下一条、或者实现上下文切换。
4.3 ESR_ELn --- 异常症状寄存器
SPSR 告诉你"异常发生时的状态",ELR 告诉你"返回到哪里",但为什么触发异常 还需要第三个寄存器来回答------这就是 ESR_ELn(Exception Syndrome Register)。


ESR_ELn 仅对同步异常和 SError 更新,对 IRQ/FIQ 不更新------因为中断原因由外部 GIC 的状态寄存器提供,而非 core 内部产生。
ESR_ELn 的位域编码:
| 位域 | 描述 |
|---|---|
| Bits 31:26 | EC(Exception Class):使 handler 能够区分各类异常原因------未定义指令、MCR/MRC 到 CP15 的异常、FP 操作异常、SVC/HVC/SMC 执行、Data Abort、对齐异常等 |
| Bit 25 | IL(Instruction Length):指示被 trap 的指令长度(0 = 16-bit,1 = 32-bit),对某些异常类别也会置位 |
| Bits 24:0 | ISS(Instruction Specific Syndrome) :特定于该异常类型的详细信息。例如执行 SVC 0x123456 时,此字段包含立即数 0x123456;对于 Data Abort,则包含 WnR(读/写)、ISV(是否有有效 syndrome)等子字段 |
ESR 的价值在于让 handler 通过一个寄存器就能完成异常分发,无需反复读取多个状态寄存器来猜测原因。
4.4 FAR_ELn --- 故障地址寄存器
FAR_ELn (Fault Address Register)保存所有同步 Instruction Abort、Data Abort 及对齐故障所对应的 faulting virtual address。简单来说:
- ESR_ELn 告诉你为什么错(缺页?权限?对齐?)
- FAR_ELn 告诉你哪个地址错了
handler 可以结合 FAR_ELn 判断应该修复哪个地址的映射、还是向进程发送 SIGSEGV。
边界情况:若异常从 AArch32 的 EL 进入 AArch64 的 EL,且写入了目标 EL 对应的 FAR_ELn,则 FAR_ELn 的高 32 位全部置零------因为 AArch32 的虚拟地址只有 32 位。
FAR_ELn 并非对所有同步异常都会更新。例如 SVC/HVC/SMC 这类本身不涉及内存访问的系统调用,FAR 内容无意义。handler 应首先检查 ESR_ELn.EC 字段,确认是内存相关的 abort 后再读取 FAR。
4.5 SP_ELn --- 异常级专用栈
每个异常级别都有自己专用的栈指针寄存器:SP_EL0、SP_EL1、SP_EL2、SP_EL3。
处理器通过 SPSR 中的 SPSel 位控制栈指针选择:
c
MSR SPSel, #0 // 使用 SP_EL0
MSR SPSel, #1 // 使用 SP_ELn(当前异常级别的栈指针)
一个典型使用场景:SP_EL1 指向内核保证始终有效的小栈 (用于中断上下文等临界路径),SP_EL0 指向更大的内核任务栈(空间更大但可能溢出)。handler 可以按场景在两个栈之间切换,兼顾安全性与灵活性。
除 EL0 外所有异常级别都支持此机制------EL0 永远只能使用 SP_EL0。
5. 异常路由
确定了异常类型之后,下一个问题是:在哪个 Exception level 处理它?
5.1 核心规则
- 异常永远不会被路由到 EL0
- 同步异常通常在当前或更高的 EL 中被捕获
- 异步异常可以(按需)被路由到更高的 EL,交由 Hypervisor 或 Secure Kernel 处理
- 异常被捕获后,该异常类型在捕获它的 EL 中自动被屏蔽(PSTATE.DAIF 对应位自动置位)
5.2 路由控制寄存器
两个关键寄存器控制异步异常的路由:
- SCR_EL3:指定哪些异常应路由到 EL3(Secure Monitor),包含 IRQ、FIQ、SError 各自的独立控制位
- HCR_EL2:指定哪些异常应路由到 EL2(Hypervisor),同样包含 IRQ、FIQ、SError 各自的独立控制位
这为虚拟化和安全环境提供了灵活的控制粒度。例如 Hypervisor 可以配置 HCR_EL2 将所有物理 IRQ 路由到自己处理,再通过虚拟中断注入机制分发给 Guest OS。
6、异常向量表
6.1 向量表结构
异常触发后,处理器需要跳转到对应 handler。存储 handler 入口的内存位置称为异常向量(exception vector) ,这些向量组织在一张异常向量表中。
每个 EL 都有独立的向量表,基地址由向量基址寄存器 VBAR_EL3、VBAR_EL2 和 VBAR_EL1 指定。

每张表包含 16 个条目 ,每个条目 128 字节(32 条指令) 。这张表实际上由 4 组 × 4 条目构成,具体使用哪个条目取决于异常的来源:

四种异常来源:
| 来源 | 含义 | 典型场景 |
|---|---|---|
| Current EL with SP_EL0 | handler 运行在当前 EL,但栈指针借用 EL0 的 SP | 不常见,用于特殊优化 |
| Current EL with SP_ELx | handler 运行在当前 EL,使用自己级别的 SP_ELx | 最常见:内核(EL1)内部中断 |
| Lower EL using AArch64 | 异常来自更低 EL,且那个 EL 运行在 AArch64 | 64 位 EL0 应用发起 SVC |
| Lower EL using AArch32 | 异常来自更低 EL,且那个 EL 运行在 AArch32 | 32 位 EL0 应用发起 SVC |
当异常被路由到当前 EL 时,处理器根据异常发生前 PSTATE.SP(即 SPSel)的取值,判断当前正使用 SP_EL0 还是 SP_ELx,进而选择 Current EL with SP_EL0 或 Current EL with SP_ELx 对应的向量入口。
举个具体例子:内核代码正在 EL1 执行 SP_EL1,此时一个 IRQ 信号到来。该 IRQ 与 Hypervisor 或 Secure 环境无关,在内核内部处理,且 SPSel 置位(表示使用 SP_EL1)。因此,处理器跳转到 VBAR_EL1 + 0x280 处的向量入口。
6.2 与 ARMv7-A 的对比
ARMv7-A 向量表每个条目只有 4 字节 ,只能容纳一条跳转指令(LDR PC, [PC, #offset]),真正的 handler 代码必须放在别处。AArch64 将每个条目扩展到 128 字节 ,这意味着顶层的 handler 逻辑可以直接内联写在向量表内部,减少了一次跳转延迟。
AArch64 不再支持
LDR PC, [PC, #offset]这类指令。向量间较大的间距还有一个好处:避免未使用的向量条目污染典型大小的 instruction cache line。
复位地址是一个完全独立的地址,由实现定义(IMPLEMENTATION DEFINED),保存在 RVBAR_EL1/2/3 中。
6.3 向量表的实际价值
为每种异常(无论是来自当前 EL 还是更低 EL、AArch64 还是 AArch32)设置独立向量,为 OS/Hypervisor 提供了极大的灵活性。例如:
- 内核可以区分异常来自自身(EL1h)还是来自用户态(EL0)
- 可以区分来自 64 位应用还是 32 位应用,从而采取不同的处理路径
- SP_ELn 用于处理来自更低 EL 的异常,但 handler 可在内部切换到 SP_EL0(如果 SPSel 支持),方便访问线程上下文
7. 实践:Linux 内核的异常向量表
Linux 内核在 arch/arm64/kernel/entry.S 中实现了 EL1 的异常向量表:
c
SYM_CODE_START(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error // Error EL1h
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error // Error 64-bit EL0
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid_compat, 32// FIQ 32-bit EL0
kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
#else
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endif
SYM_CODE_END(vectors)
对照第 6 节的向量表结构来理解这段代码:
- 第一个参数
1或0:1= Current EL(来自 EL1 自身),0= Lower EL(来自 EL0) - EL1t / EL1h :
t= Current EL with SP_EL0(使用 EL0 栈指针),h= Current EL with SP_ELx(使用 EL1 自己的栈指针) fiq_invalid:Linux 内核默认不使用 FIQ,因此标记为 invalid------如果误触发 FIQ,会进入统一的异常处理流程并报错CONFIG_COMPAT:当内核支持 32 位兼容模式时,来自 32 位 EL0 的异常走_compat路径;不支持时直接标记为_invalid
这与 ARMv8-A 向量表的 16 条目布局完全对应,非常直观。