深入理解 ARMv8-A|异常/中断处理

参考:

《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 系列其他文章:

深入理解 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)

异常触发时,硬件严格按以下顺序执行三步操作:

  1. PSTATE → SPSR_ELn:将当前处理器的完整状态(CurrentEL、DAIF、NZCV 等关键字段)压缩写入目标 EL 对应的 SPSR_ELn,形成一份"状态快照"。

  2. PSTATE 更新:反映新的处理器状态。此时:

    • Exception level 可能提升(例如 EL0→EL1),也可能保持不变(EL1→EL1)
    • 对应中断类型的掩码位自动置位(例如 IRQ 触发后,PSTATE.I=1,屏蔽嵌套 IRQ)
    • 执行状态可能从 AArch32 切换到 AArch64(见 3.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_EL3VBAR_EL2VBAR_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. 第一个参数 101 = Current EL(来自 EL1 自身),0 = Lower EL(来自 EL0)
  2. EL1t / EL1ht = Current EL with SP_EL0(使用 EL0 栈指针),h = Current EL with SP_ELx(使用 EL1 自己的栈指针)
  3. fiq_invalid:Linux 内核默认不使用 FIQ,因此标记为 invalid------如果误触发 FIQ,会进入统一的异常处理流程并报错
  4. CONFIG_COMPAT :当内核支持 32 位兼容模式时,来自 32 位 EL0 的异常走 _compat 路径;不支持时直接标记为 _invalid

这与 ARMv8-A 向量表的 16 条目布局完全对应,非常直观。

相关推荐
吃好睡好便好1 小时前
矩阵的求逆运算
人工智能·学习·线性代数·matlab·矩阵
東隅已逝,桑榆非晚1 小时前
新手入门指南:认识 C 语言文件操作(下)
c语言·笔记
库奇噜啦呼1 小时前
【iOS】源码学习-方法交换
学习·ios·cocoa
飞翔中文网1 小时前
Java学习笔记之泛型
java·笔记·学习
济6171 小时前
ROS开发专栏---基于 NAV2 实现仿真环境自主导航实验--适配Ubuntu 22.04
嵌入式硬件·嵌入式·ros2·机器人方向
li星野1 小时前
RAG优化系列:自适应检索(Adaptive Retrieval)——让系统智能选择是否检索
人工智能·python·学习
济6172 小时前
ROS开发专栏---基于开源导航插件 wp_map_tools 多航点巡航导航实验--适配Ubuntu 22.04
ubuntu·嵌入式·ros2·机器人开发·机器人方向
AOwhisky2 小时前
Ceph系列第四期:Ceph块存储(RBD)精讲
linux·运维·笔记·ceph·云计算·rbd
longxiangam10 小时前
esp-idf 中 mipi dsi 使用的笔记
笔记