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

参考:

《ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition》

《ARM Cortex™-A Series Version: 4.0 Programmer's Guide》

ARMv7-A 系列其他文章:

深入理解 ARMv7-A|处理器模式与寄存器

1、ARMv7-A 异常概述

异常(Exception) 是指任何需要核心暂停当前正常执行流、转而运行一段专用特权代码(异常处理程序,Exception Handler)的条件或系统事件。处理完成后,特权软件负责准备核心恢复到异常发生前的状态继续执行。

在很多资料里,exceptioninterrupttrap 这几个词经常混着用,但在 ARM 语境里最好区分开:

  • Exception:所有打断正常执行流的事件的统称
  • Interrupt:特指 IRQ 和 FIQ 这两种异步硬件中断
  • Trap:非 ARM 官方术语,在虚拟化语境下有时指 Hyp Trap

从程序员视角看,理解异常机制,本质上就是回答三个问题:

  1. 从哪来 ------ 什么事件触发了异常?
  2. 到哪去 ------ 处理器跳到哪个地址执行处理程序?
  3. 怎么回 ------ 处理完毕后如何恢复被中断的程序?

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。
  • 上表中的偏移值,例如 0x180x1C,都只是"表内偏移",实际入口地址还要结合向量表基址一起看。

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 = 1CPSR.I = 1,也就是在处理 FIQ 期间同时屏蔽 FIQIRQ

在 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.ICPSR.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 不变

两个关键推论:

  1. FIQ 可以打断 IRQ 和 Abort 处理程序 。如果 Data Abort 和 FIQ 同时发生,Data Abort(更高优先级)先被处理。因为 Data Abort 不屏蔽 FIQ(F 位不变),核心随后立即进入 FIQ 处理程序。FIQ 处理完毕后再返回继续 Data Abort。
  2. 未定义指令和 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.VVBAR 共同决定:

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

从异常处理返回需要原子地完成两个操作:

  1. SPSR_<mode> 恢复到 CPSR(恢复处理器模式、中断屏蔽位、指令集状态)
  2. 将 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 支持或地址范围限制,本质上仍然是在做"第一跳"。

相关推荐
koo3647 小时前
周报5.24
笔记
wxytxdy8 小时前
通过猜数字游戏学习Shell脚本的分支、循环编写
linux·学习
我想我不够好。8 小时前
观察对方打野的动向,预判下一次gank的时机
学习
java小吕布8 小时前
Hermes Agent:自带学习闭环的开源 AI 智能体,一键部署全平台可用
人工智能·学习·开源
玄米乌龙茶1238 小时前
LLM成长笔记(十一):模型部署与工程化
笔记
会编程的土豆8 小时前
结构体标签与数据流向 笔记
笔记
东风破1379 小时前
达梦DEM和DFM的介绍、搭建学习记录
数据库·学习·dm达梦数据库
玄米乌龙茶1239 小时前
LLM成长笔记(十):多模态应用开发
人工智能·笔记·语音识别
希冀1239 小时前
【CSS学习第十二篇】
css·学习·tensorflow