ARMv7-A小系统实践(六):异常入口、返回地址与独立中断栈

本章记录什么

这一章解释异常入口与返回,并建立任务私有的 IRQ 异常帧。它要解决的不是“进入了哪个 C 函数”,而是任意指令执行到一半时,怎样保存足够的信息,使处理结束后能够像中断从未发生一样继续执行;如果调度器选择了另一个任务,又怎样改为恢复另一个任务的现场。

在这一章之前做了什么

上一章的协同切换发生在函数调用边界,因此可借助 AAPCS 只保存 callee-saved 状态。IRQ 没有这个便利:r0-r3r12、条件标志和返回地址都可能正处于有效计算中。硬件只完成模式切换、CPSR 到 SPSR 的保存以及返回地址写入 banked lr,其余现场必须由入口汇编建立。

本章先分清硬件动作和软件动作,再说明为什么返回 PC 需要修正、为什么异常帧应归属于被打断任务、为什么 C handler 可以使用独立中断栈但恢复现场不能长期停留在共享栈上。

系列目录 · 上一篇:协同任务与普通上下文切换

本章的阅读顺序

先读基础概念,确认每个术语解决什么问题;再看设计推导,理解实现为什么采用这种保存集合、初始化顺序或状态机;随后逐段阅读代码和执行轨迹;最后用验证方法与错误案例检查理解。文中保留寄存器名、指令名和标准缩写,叙述部分统一使用简体中文。

动手前的基础:异常入口、现场与返回

日期:2026-07-28
状态:本专题已完成并通过;只代表异常入口与返回完成,不代表整个第四目标完成
专题范围:CPU 看见异常以后发生什么
后续专题:GIC、设备中断号、定时器、acknowledge/EOI

一、本节学完要形成的完整链

1
2
3
4
5
6
7
8
9
10
11
原程序正在运行
→ 异常发生
→ CPSR 保存到目标模式 SPSR
→ 返回信息保存到目标模式 LR
→ CPU 切换模式和 banked SP/LR
→ PC 进入异常向量
→ 入口汇编保存软件现场
→ 调用 C handler
→ 恢复软件现场
→ 恢复 PC 与 CPSR
→ 原程序继续

本节最重要的不是背指令,而是分清三方责任:

责任方 负责什么
CPU 硬件 切模式、保存旧 CPSR 到 SPSR、生成 LR、进入向量
异常入口汇编 修正返回地址、保存通用寄存器、构造帧、满足 AAPCS
C handler / OS 判断原因、处理事件、记录故障、决定是否调度或终止

二、“中断”和“异常”是什么关系

在 ARMv7-A 里,异常是更大的概念:

1
2
3
4
5
6
7
8
异常 Exception
├── Reset
├── Undefined Instruction
├── SVC
├── Prefetch Abort
├── Data Abort
├── IRQ
└── FIQ
  • Undefined、SVC、Abort 通常与当前执行指令直接相关。
  • IRQ/FIQ 通常来自外部设备或中断控制器,可以异步打断当前代码。
  • GIC 负责汇总和仲裁许多设备中断,再向 Cortex-A9 发出 IRQ/FIQ 信号。
  • CPU 收到 IRQ 信号后,才执行本节学习的“IRQ 异常进入”。

因此:

1
设备 → GIC → CPU 的 IRQ 异常入口

本节先掌握最后一段;下一轮再补设备和 GIC。


三、异常向量表

ARMv7-A 的普通异常向量有 8 个槽,每槽 4 字节:

向量偏移 异常 目标模式
0x00 Reset SVC
0x04 Undefined Instruction UND
0x08 SVC SVC
0x0C Prefetch Abort ABT
0x10 Data Abort ABT
0x14 Reserved
0x18 IRQ IRQ
0x1C FIQ FIQ

向量表不是 8 个完整 handler,只是 8 个入口槽。常见写法:

1
2
3
4
5
6
7
8
9
vector_table:
ldr pc, reset_handler_addr
ldr pc, undef_handler_addr
ldr pc, svc_handler_addr
ldr pc, prefetch_abort_handler_addr
ldr pc, data_abort_handler_addr
udf
ldr pc, irq_handler_addr
ldr pc, fiq_handler_addr

后面再放真正地址:

1
2
irq_handler_addr:
.word irq_entry

为什么不能直接把整个 handler 写在向量槽中:相邻槽只相差 4 字节,一个槽只容纳一条 32 位 ARM 指令,所以通常先跳转。

Cortex-A9 可以通过 VBAR 设置普通向量表基址。RTEMS 启动代码中可定点观察:

1
RTEMS-6.2/bsps/arm/shared/start/start.S

它使用 8 条 ldr pc, ... 组成向量表,并在启动阶段设置向量基址。


四、异常发生时,硬件自动做什么

以原程序处于 System 模式、IRQ 到来为例。

异常前:

1
2
3
4
CPSR      = 原程序状态
当前 SP = SP_sys
当前 LR = LR_sys
PC = 原程序执行位置

CPU 接受 IRQ 后,概念上完成:

1
2
3
4
5
6
7
SPSR_irq = 旧 CPSR
LR_irq = 架构规定的 IRQ 返回信息
CPSR.M = IRQ 模式
CPSR.I = 1,屏蔽普通 IRQ
当前 SP = SP_irq
当前 LR = LR_irq
PC = vector_base + 0x18

这里再次体现第一主题的 banked registers:

  • 原来的 SP_sys/LR_sys 没有被复制或覆盖;
  • 切到 IRQ 模式后,同名 sp/lr 映射成 SP_irq/LR_irq
  • SPSR_irq 保存的是异常前完整 CPSR,里面包含旧模式、T 位、I/F 位和条件标志等。

硬件没有自动做的事

CPU 不会自动:

  • r0-r12 压入栈;
  • 生成操作系统定义的 ExceptionFrame 结构;
  • 调用 C 函数;
  • 查询 GIC 中断号;
  • 清除设备中断源;
  • 决定是否切换任务。

这些都属于软件。


五、为什么必须同时保存返回 PC 和旧 CPSR

只恢复 PC 不够。

假设 IRQ 前:

1
2
3
4
模式 = System
T = 0,ARM state
I = 0,允许 IRQ
条件标志 = 某个计算结果

IRQ 期间:

1
2
3
模式 = IRQ 或 SVC
I = 1
条件标志可能被 handler 改写

返回时必须恢复:

1
2
PC   = 原程序继续位置
CPSR = SPSR_irq 中保存的旧状态

恢复 CPSR 后:

  • 模式回到 System;
  • 当前可见 SP/LR bank 回到 SP_sys/LR_sys
  • ARM/Thumb 状态恢复;
  • IRQ 屏蔽状态恢复;
  • 条件标志恢复。

这正是第一主题“模式 bank”和本主题“异常返回”的闭环。


六、LR 为什么还要修正

异常进入时的 LR 是架构规定的“返回信息”,不保证对所有异常都等于最终继续 PC。不同异常与流水线语义不同,常见 ARM-state 返回方式如下:

异常 常见返回/重试形式 意义
SVC movs pc, lr 从 SVC 后一条继续
Undefined movs pc, lr 跳过未定义指令
IRQ/FIQ subs pc, lr, #4 回到被打断程序的继续位置
Prefetch Abort subs pc, lr, #4 修复原因后重试取指
Data Abort subs pc, lr, #8 修复原因后重试数据访问

这张表用于建立差异意识,不要求现在死背。实际返回还取决于:

  • 希望跳过故障指令还是重试;
  • 异常前是 ARM 还是 Thumb;
  • 使用 SUBS/MOVS PC,LRRFE,还是先构造统一异常帧;
  • 系统是否把 LR 预先修正成最终 PC。

RTEMS 的 IRQ 入口先做:

1
sub lr, lr, #4

LR_irq 预先变成最终继续 PC,然后使用 SRS/RFE 形式统一保存和返回。


七、异常入口为什么和普通上下文不同

普通任务切换:

1
2
3
发生在函数调用边界
→ AAPCS 已允许 r0-r3、r12 被破坏
→ 通常保存 r4-r11、SP、LR

IRQ:

1
2
3
可能发生在任意指令之后
→ r0-r3、r12 中可能有尚未使用的中间结果
→ 入口汇编调用 C 前必须保护它们

例如:

1
2
3
add r0, r1, r2
// IRQ 恰好发生
str r0, [r3]

如果 IRQ 的 C handler 改写 r0,回来后 str 就会写入错误数据。

但这不意味着每个 IRQ 入口一定机械保存所有 r0-r15:

  • 入口汇编自身和被调用 C 函数会破坏的状态必须保存;
  • r4-r11 若完全交给符合 AAPCS 的 C handler,C handler 自己必须保持它们;
  • 如果入口汇编把某些 r4-r11 当临时寄存器,就必须额外保存;
  • PC/CPSR 通过 LR/SPSR 或异常帧专门保存;
  • VFP/NEON 是否保存取决于系统策略。

正确问题不是“是不是全保存”,而是:

从异常点返回时,原程序仍然需要的每一项状态,是否都能得到原值?


八、一条简化的 IRQ 入口路径

下面是教学用骨架,不包含 SMP、嵌套中断、独立中断栈和 VFP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
irq_entry:
// 当前处于 IRQ 模式,当前 lr 是 LR_irq,SPSR 是 SPSR_irq
sub lr, lr, #4

// 把修正后的返回 PC 与 SPSR_irq 保存到 SVC 栈,
// 存储格式与后面的 RFE 匹配
srsdb sp!, #0x13

// 进入 SVC 模式,开始使用 SP_svc
cps #0x13

// 保存 C handler 可破坏的易失寄存器和当前 SVC LR
push {r0-r3, r12, lr}

// SP 必须在公共调用边界满足 8 字节对齐
bl irq_dispatch

pop {r0-r3, r12, lr}

// 从 SVC 栈恢复 PC 与 CPSR
rfeia sp!

逐步解释:

  1. sub lr,lr,#4:将 IRQ 架构 LR 修正为最终继续地址。
  2. SRS:保存 Return State,也就是返回 PC 与旧 CPSR。
  3. CPS #0x13:切换到 SVC 模式,使用 SVC 栈运行公共处理逻辑。
  4. PUSH:保护任意异常点上可能活跃、又会被 C 调用破坏的寄存器。
  5. BL:进入 C 后重新受 AAPCS32 约束。
  6. POP:恢复被中断程序的易失寄存器。
  7. RFE:同时恢复 PC/CPSR;CPSR.M 恢复后重新看到原模式的 banked SP/LR。

这里 6 个寄存器共 24 字节,SRS 的返回状态占 8 字节;如果入口 SVC SP 原本 8 字节对齐,则公共调用前仍保持 8 字节对齐。

实际 RTEMS 代码位于:

1
RTEMS-6.2/cpukit/score/cpu/arm/arm_exc_interrupt.S

可只观察这些关键点:

1
2
3
4
5
6
7
8
sub lr, lr, #4
srsfd sp!, #ARM_PSR_M_SVC
cps #ARM_PSR_M_SVC
push {...}
对齐 SP
bl bsp_interrupt_dispatch
pop {...}
rfefd sp!

其余嵌套计数、SMP、VFP、临时中断栈和调度逻辑暂时忽略。


九、SRS/RFE SUBS PC,LR` 是什么关系

简单 handler

如果整个 IRQ handler 一直留在 IRQ 模式,并把 LR_irq/SPSR_irq 保持好,可以:

1
subs pc, lr, #4

这里 S 与写 PC 的组合完成异常返回语义:

1
2
PC = LR_irq - 4
CPSR = SPSR_irq

统一异常帧

复杂 OS 往往要:

  • 换到 SVC 栈或独立中断栈;
  • 允许嵌套;
  • 调用 C;
  • 可能在 IRQ 尾部调度;
  • 让不同异常共享一套帧。

因此先把返回 PC 和 CPSR 存进内存,最后使用:

1
rfeia sp!

从内存同时恢复它们。

二者最终目标相同:

1
恢复正确 PC + 恢复异常前 CPSR

只是返回状态保存在寄存器还是内存、处理过程简单还是复杂。


十、异常帧是什么

异常帧是 OS 在内存中定义的数据结构,不是 ARM 硬件统一规定的 C 结构体。

示意:

1
2
3
4
5
6
7
8
9
10
typedef struct {
uint32_t r0;
uint32_t r1;
// ...
uint32_t r12;
uint32_t interrupted_sp;
uint32_t interrupted_lr;
uint32_t return_pc;
uint32_t saved_cpsr;
} ExceptionFrame;

它的作用:

  • 让 C handler 查看异常现场;
  • 让调试器打印寄存器;
  • 修改返回值或返回地址;
  • 支持异常尾部调度;
  • 最终由汇编按相同偏移恢复。

ARM 规定的是异常入口和返回的架构语义;帧字段顺序由 OS 决定。但一旦汇编执行 BL c_handler

1
2
3
4
r0-r3 参数
r4-r11 保持责任
SP 8 字节对齐
返回值位置

仍必须遵守 AAPCS32。


十一、IRQ 的完整分层

先建立全图,下一轮再展开中间部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
设备产生事件

设备设置自己的 pending/status 位

GIC Distributor 判断使能、优先级、目标 CPU

GIC CPU Interface 向 Cortex-A9 发 IRQ

CPU 执行 IRQ 异常硬件入口

vector_base + 0x18

IRQ 汇编保存现场

读取 GIC acknowledge 寄存器,得到 interrupt ID

调用对应 C handler

清设备中断源

写 GIC EOI

恢复现场并异常返回

“CPU 进入 IRQ 模式”并不能告诉软件到底是 UART、定时器还是网卡触发。真正的中断号来自 GIC acknowledge,这就是下一轮的核心。


十二、常见错误与 Debug 现象

1. 没有初始化 SP_irq

现象:第一次 IRQ 一压栈就访问错误地址,常表现为 Data Abort 或直接死机。

2. 忘记修正 IRQ LR

现象:返回后跳过或重复一条指令,可能持续出现难以解释的逻辑错误。

3. 没保存 r0-r3/r12 就调用 C

现象:中断返回后普通代码变量随机改变。

4. SP 未按 8 字节对齐就 BL

现象:简单 handler 似乎可运行,稍微优化、增加 64 位访问或换编译器后崩溃。

5. 只恢复 PC,没恢复 CPSR

现象:仍处于异常模式、IRQ 一直被屏蔽、栈 bank 错误或 ARM/Thumb 状态错误。

6. 没清设备 pending 或没写 EOI

现象:返回后立刻再次进入 IRQ,形成中断风暴。此项将在下一轮展开。


十三、自检题与解析

完成这一章后,可以不看正文回答下面六个问题。

  1. 硬件进入 IRQ 时自动保存什么?

    硬件把旧 CPSR 保存到 SPSR_irq,按异常类型生成 LR_irq,切换到 IRQ 模式并跳到 VBAR+0x18。它不会自动保存 r0-r12,也不会替操作系统建立 C 结构体形式的异常帧。

  2. 为什么只恢复 PC 不够?

    异常前的模式、I/F/T 位和条件标志都在旧 CPSR 中。正确返回必须同时恢复 PC 与 CPSR,否则即使地址正确,处理器也可能留在错误模式或使用错误指令状态。

  3. 为什么 IRQ 入口要保护 r0-r3r12

    IRQ 可发生在任意指令边界,这些寄存器可能含有尚未消费的中间值;入口又要调用遵循 AAPCS 的 C 函数,而 C 函数有权改写 caller-saved 寄存器。

  4. SRSRFE 分别做什么?

    SRS 把当前异常模式的返回地址和 SPSR 放入目标模式栈;RFE 从匹配布局中同时装回 PC 与 CPSR。它们是一组适合构造统一异常帧的指令,但仍需软件保存通用寄存器。

  5. 独立 IRQ 栈和任务私有 frame 是否矛盾?

    不矛盾。任务私有 frame 保存将来恢复任务所需的状态;独立 IRQ 栈承载 C handler 的调用深度。前者解决现场所有权,后者隔离中断处理过程的栈消耗。

  6. 怎样口述一次完整 IRQ?

    设备提出请求,GIC 交付 IRQ,处理器保存 CPSR/LR 并进入向量,汇编修正返回地址并建立 frame,C 分发器读取 IAR、处理设备、清源并写 EOIR,IRQ 尾部选择最终 frame,最后恢复通用寄存器并以 RFE 或等价序列恢复 PC/CPSR。

十四、自检结论

如果能够独立解释硬件与软件的责任边界、返回地址修正、异常帧所有权和 PC/CPSR 成对恢复,就具备继续连接 GIC 与设备中断的基础。

架构手册规则怎样落到 IRQ frame

第一部分:ARM 架构异常入口与返回

3. 异常与中断不是完全同义

ARM 异常包括同步和异步来源:

异常 典型原因 同步性
Reset 复位 特殊
Undefined Instruction 指令不支持/编码非法 同步
SVC 软件执行 SVC 同步
Prefetch Abort 取指失败 同步
Data Abort 数据访问失败 同步
IRQ 普通外部中断请求 异步
FIQ 快速外部中断请求 异步

因此:

1
2
IRQ/FIQ 是异常类型
但异常不只有中断

GIC 只负责外部中断分发,不负责修复 Data Abort,也不决定 SVC 的语义。


4. 向量表

普通 AArch32 异常向量偏移:

偏移 向量
0x00 Reset
0x04 Undefined Instruction
0x08 SVC
0x0C Prefetch Abort
0x10 Data Abort
0x14 Reserved
0x18 IRQ
0x1C FIQ

向量表每槽只有 4 字节,通常放一条跳转或装载 PC 指令:

1
2
3
4
5
6
7
8
9
vectors:
b reset_entry
b undef_entry
b svc_entry
b prefetch_abort_entry
b data_abort_entry
b reserved_entry
b irq_entry
b fiq_entry

实际可使用 VBAR 指向对齐的向量基址,也可能受高向量配置影响。移植时必须确认:

  • 向量内容位于 CPU 可执行地址;
  • VBAR 设置的是运行地址,不是错误的链接/加载地址;
  • 写 VBAR 后按架构要求使用同步措施;
  • 每个异常入口符号和指令状态正确;
  • IRQ 栈在开放 IRQ 前已经初始化。

向量与 GIC ID 再次区分

所有 GIC 普通 IRQ 都进入 +0x18。向量表没有 1024 个槽。设备 ID 是在 IRQ 软件入口读取 GICC_IAR 后才得到。


5. 异常入口:硬件做什么

以普通 IRQ 进入 IRQ 模式为主线,架构效果可理解为:

1
2
3
4
5
6
7
1. 保存异常前 CPSR 到 SPSR_irq
2. 把架构定义的首选返回信息写入 LR_irq
3. 修改 CPSR.M,进入 IRQ 模式
4. 修改相应屏蔽位,IRQ 入口至少设置 I
5. 按架构配置确定 ARM/Thumb 状态和端序
6. PC 跳到 IRQ 向量
7. 当前可见 SP/LR 切为 SP_irq/LR_irq

硬件通常没有替软件完成:

1
2
3
4
5
6
保存 r0-r12
分配 C 结构体异常帧
读取 GIC ID
清设备中断状态
写 GIC EOI
调用调度器

所以“异常发生时 CPU 自动保存全部寄存器”是错误认识。

为什么要尽快保存 LR_irq 与 SPSR_irq

它们是当前异常的关键返回状态。同模式再次嵌套或入口代码误改可能覆盖它们。成熟入口会尽快将其搬到受控栈帧。


6. 返回地址为什么不能统一写死

不同异常的 LR_mode 语义不同,处理目标也不同:

1
2
3
4
IRQ/FIQ:通常要回到被打断后应继续的位置
Prefetch Abort:可能修复后重试取指
Data Abort:可能修复后重试数据访问,也可能终止任务
SVC/Undefined:通常回到触发指令之后

常见 A32 形式:

1
2
3
subs pc, lr, #4      @ 常见 IRQ/FIQ 返回
subs pc, lr, #8 @ 常见 Data Abort 重试
movs pc, lr @ 常见 SVC/Undefined 返回

但实际返回必须结合:

  • 异常类型;
  • ARM/Thumb 状态;
  • 是重试故障指令还是跳过;
  • 入口是否已经提前修正 LR;
  • 使用 RFE 还是经典带 S 返回。

最稳妥的方法是依据 ARM ARM B1.8.3 的 Table B1-6/B1-7,而不是背一句“所有异常 LR 都减 4”。


7. 软件入口怎样建立异常帧

一种可读的逻辑布局:

1
2
3
4
5
6
7
8
9
typedef struct {
uint32_t r0;
uint32_t r1;
...
uint32_t r12;
uint32_t lr_irq_raw;
uint32_t return_pc;
uint32_t return_cpsr;
} arm_irq_frame;

入口的职责:

  1. 修正或保存原始 LR_irq
  2. 保存 SPSR_irq
  3. 保存会被入口和 C handler 改写的通用寄存器。
  4. 保证 frame 布局与 C 定义一致。
  5. 保证调用 C 时 SP 8 字节对齐。
  6. 处理 IRQ 栈、SVC 栈或 per-CPU interrupt stack 的切换策略。
  7. 记录中断嵌套状态。

SRS/RFE 路线

支持相应 ARMv7 指令时,可用:

1
2
3
4
5
sub   lr, lr, #4
srsfd sp!, #svc_mode
cps #svc_mode
...
rfeia sp!

SRS 把异常返回状态保存到指定模式栈;RFE 从内存同时恢复 PC 与 CPSR。具体寻址后缀必须与栈布局匹配。

经典路线

也可以:

1
2
3
4
5
6
7
sub   lr, lr, #4
stmdb sp!, {r0-r12, lr}
mrs r0, spsr
...
msr spsr_cxsf, saved_state
ldmia sp!, {r0-r12, lr}
subs pc, lr, #0

这只是示意。实际代码若把状态搬到 SVC 栈、支持嵌套或调度,布局会更复杂。

为什么 handler 不能直接普通 return

C 的 return 只遵循 AAPCS 返回到调用它的汇编入口,并不会自动:

  • SPSR_irq 恢复到 CPSR;
  • 从 IRQ 模式回到原模式;
  • 选择异常返回语义;
  • 重新装载完整异常现场。

真正异常返回仍由汇编出口完成。


8. 异常返回的本质

无论使用哪种指令形式,最终必须原子地实现逻辑:

1
2
PC   ← 保存的返回地址
CPSR ← 异常前程序状态

恢复 CPSR 带来的效果包括:

  • 原模式恢复;
  • I/F/A 屏蔽状态恢复;
  • 原 ARM/Thumb 状态恢复;
  • 原条件标志恢复;
  • 当前可见 SP/LR bank 回到原模式。

如果只恢复 PC、不恢复 CPSR,代码可能从正确地址以错误模式或错误指令状态执行,通常很快崩溃。


从普通 context 走到任务私有 IRQ frame

实现步骤四:建立真正的 IRQ 异常帧

1
69aac20 irq: add the ARM exception frame and return path

为什么要和普通上下文分开

普通切换发生在函数调用边界,受 AAPCS 保护;IRQ 可以发生在任意指令之间,因此
r0-r3/r12 也可能保存着仍然有效的中间值,不能丢。

先看什么

1
2
3
4
5
异常入口自检清单
GIC 状态机自检清单
rtems-6.2/bsps/arm/shared/start/start.S
rtems-6.2/cpukit/score/cpu/arm/armv4-exception-default.S
rtems-6.2/cpukit/score/cpu/arm/armv4-exception-resume.S

官方资料重点:

  • IRQ 自动执行 CPSR → SPSR_irq
  • LR_irq 的返回地址语义;
  • IRQ 模式有 banked SP/LR;
  • subs pc, lr, #4 或 RFE 类返回如何恢复 CPSR。

必须先回答的问题

  1. 硬件自动保存了哪些内容,哪些必须由软件保存?
  2. 为什么 IRQ 入口要保存 r0-r12
  3. 为什么调用 C dispatcher 前 SP 必须仍为 8 字节对齐?
  4. 为什么保存的是 SPSR_irq,而不是只保存 handler 当前 CPSR?
  5. 为什么普通 bx lr 不能完成异常返回?

本实现的 64 字节异常帧

1
2
3
4
5
6
offset 0   SPSR_irq
offset 4 对齐填充
offset 8 r0
...
offset 56 r12
offset 60 LR_irq

r0-r12,lr 共 56 字节,再为 SPSR 和 padding 留 8 字节,总计 64 字节,C 调用前仍满足
8 字节对齐。

返回顺序

1
2
3
4
恢复SPSR_irq
→ 丢弃padding
→ 恢复r0-r12和LR_irq
→ subs pc, lr, #4

这一步仍不主动打开 IRQ。目的是把“异常保存/返回合同”与“中断控制器配置”拆开。


从规则走到实际实现

下面回到本章对应的小系统实现。这里不再假定读者已经打开源码,而是把关键地址、数据结构、指令序列和返回路径直接放在文章中解释。

异常是总称,IRQ 只是其中一种

在这里采用的经典 ARM 向量布局中:

偏移 异常 常见原因
0x00 Reset 复位入口
0x04 Undefined 不能执行的指令或相应扩展不可用
0x08 SVC 软件执行 SVC 指令
0x0c Prefetch Abort 取指访问失败
0x10 Data Abort 数据访问失败
0x14 Reserved 保留
0x18 IRQ 普通中断请求
0x1c FIQ 快速中断请求

SVC、Undefined、精确 Abort 与当前指令有明确关系;IRQ/FIQ 来自异步请求。Data Abort 还要区分精确与非精确情形,不能把所有异常都看成“当前 PC 的一条指令出错”。

CPU 的 IRQ 向量只有一个。UART 和 Timer 的 IRQ 并不会分别跳到向量表中第 UART 号、第 Timer 号的位置;具体设备号要进入后读取 GIC。

硬件自动做了什么,没做什么

对本工程从 SVC 进入 IRQ 的路径,硬件完成的核心动作是:

1
2
3
4
旧 CPSR → SPSR_irq
返回相关地址 → LR_irq
切换到 IRQ 模式,并更新异常入口相关控制状态
PC → IRQ 向量

它不会自动把 r0-r12、任务 LR 和所有局部变量压进内存。也不会创建 C 栈帧、调用 irq_dispatch,或查找哪个任务最该运行。这些都是入口软件的责任。

IRQ 模式有自己的 SP/LR,可以让入口先取得一组独立寄存器,但普通通用寄存器仍然可能包含被打断代码的活跃数据。在保存前拿 r0 当临时变量,就可能破坏任务。

LR_irq 为什么减 4

异常 LR 是架构定义的返回信息,不能当成普通 BL 留下的 LR。对当前 IRQ 处理方式,入口执行:

1
sub lr, lr, #4

得到适合恢复的返回 PC,再把它放入异常帧。后面的 RFE 已使用修正后的值,不应再次减 4。

在本系列 ARM 状态的常见处理约定中,可以这样理解:

异常 常见返回位置处理 限定条件
IRQ/FIQ LR 减 4 后回到被打断执行流 采用经典 PL1 返回语义
SVC 通常使用异常 LR,继续下一条 不重复执行 SVC
Undefined 要看模拟、跳过还是终止 不能统一套一个修正常量
Prefetch Abort 若修复后重试,常用 LR 减 4 先确认异常与执行状态
精确 Data Abort 若修复后重试,常用 LR 减 8 不是对所有 Data Abort 都可恢复

Thumb 指令宽度、异常种类和期望重试行为都会影响推理。系统故障诊断应保存原始 LR/SPSR,并据异常类型解释,不能把 IRQ 的减 4 抄遍所有 handler。

基础版把完整现场留在任务自己的栈上

基础版 src/start.S 的 IRQ 主体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
irq_entry:
sub lr, lr, #4
srsdb sp!, #0x13
cps #0x13

stmdb sp!, {r0-r12, lr}
mov r0, sp

mrc p15, 0, r1, c0, c0, 5
and r1, r1, #0x3
cmp r1, #0
ldreq sp, =__stack_irq_cpu0_top
ldrne sp, =__stack_irq_cpu1_top
bl irq_dispatch

mov sp, r0
ldmia sp!, {r0-r12, lr}
rfefd sp!

不要一口气读完。它其实有四个阶段。

把 IRQ bank 中的返回信息转移到 SVC 栈

SRSDB sp!, #0x13 使用当前异常模式的 LR/SPSR,把它们存到指定目标模式 SVC 的栈,并更新那个目标 SP。此时 CPU 尚在 IRQ 模式。

接着 cps #0x13 切到 SVC,名字 SP 便能看到刚刚更新后的 SVC 栈位置,名字 LR 看到的是原任务的 LR_svc。随后保存 r0-r12 和 LR_svc。

这里严格依赖“被打断任务使用 SVC 栈”的设计。如果以后任务在 User 模式,SVC 栈不再自动等于该用户任务自己的栈,必须另外安排用户 SP/LR 和内核栈的归属。

得到 64 字节的任务私有 frame

1
2
3
4
5
6
7
8
9
低地址 ← 当前任务 SP
+0 r0
+4 r1
...
+48 r12
+52 lr_svc
+56 return_pc
+60 cpsr(原 SPSR_irq)
高地址 ← 被打断前的任务 SP

总计 16 个字,64 字节。r13 没作为字段保存,因为 frame 地址加上帧大小就是这段恢复序列应还原的 SP;PC 与 CPSR 来自额外的两个返回字。

对应 C 定义是 r[13]、svc_lr、return_pc、cpsr。工程用 offsetof 和 sizeof 静态断言核对 52/56/60/64 这些数字,避免 C 布局与汇编脱节。

把 C handler 移到独立栈

frame 指针放在 r0,作为 irq_dispatch 的参数。随后入口按 MPIDR 选本核中断栈顶,再执行 BL。

此时 CPU 处于 SVC 模式。当前 SP 是 SVC bank 的寄存器,只是它的值落在名为 irq_stack 的内存区。因此“中断里读 SP 就一定是 sp_irq”在这个实现里不成立。

两种“IRQ 栈”概念必须分开:

对象 含义
banked sp_irq IRQ 模式下名为 SP 的硬件寄存器
独立软件中断栈 专门留给 handler 调用链使用的内存
任务私有异常帧 被打断任务未来恢复所需的内存状态

一块内存可以在启动时作为 sp_irq 的初始值,之后又被 SVC 模式用作 C 处理栈;这不改变 banked 寄存器的架构定义。

从分发器选择的现场返回

irq_dispatch 返回一个 frame 指针。它可以是原任务 A 的 frame,也可以是任务 B 的 frame。

1
2
3
mov sp, r0
ldmia sp!, {r0-r12, lr}
rfefd sp!

先恢复普通寄存器和任务 LR,再从剩下的两个字同时恢复 PC/CPSR。RFE 不是普通 BX:它携带异常状态恢复语义,使任务模式、中断屏蔽与条件标志一起回到保存时的状态。

为什么不能把两个任务的现场一直留在同一 IRQ 栈

假定 A 被打断,现场保存在共享中断栈;IRQ 尾部切到 B。下一次 B 被打断又从同一个中断栈顶压入现场,A 尚未恢复的数据就可能被覆盖。

于是出现一种典型假象:第一次切换成功,第二次返回就跳飞。寄存器保存集合可能完全正确,错误来自内存所有权。

本实现的分工是:

1
2
3
4
A 栈:A 的调用链 + A 的异步 frame
B 栈:B 的调用链 + B 的异步 frame
CPU0 IRQ 栈:当前 handler 的临时调用链
CPU1 IRQ 栈:另一个核的 handler 临时调用链

任务可恢复状态随任务保留;中断栈只承担当前中断处理的临时工作。

不支持嵌套,是当前实现的重要约束

每次入口都把 handler SP 直接设到同一个本核 IRQ 栈顶。如果处理中再次允许 IRQ,新入口会覆盖第一层 handler 的调用链。因此当前代码保持 IRQ 屏蔽,不支持重入式嵌套。

真要加嵌套,至少要处理:

  • 异常 LR/SPSR 已经转移到安全内存;
  • 仅最外层切到中断栈,内层沿当前中断栈继续压栈;
  • 嵌套深度和 dispatch-disable 状态;
  • GIC 优先级与 EOIR 的对应顺序;
  • 只在允许的最外层返回点调度。

RTEMS 参考路径中多出的这些计数和检查都有具体用途,并不是可以随手删除的冗余代码。

栈对齐与栈容量要分别验证

被打断时任务 SP 可能只按 4 字节对齐,64 字节保存并不会改变它的模 8 余数。当前入口在调用 C 前换到 8 字节对齐的中断栈,从而满足公共接口要求。

中断栈底有四个哨兵字,handler 还记录见到的最小 SP。哨兵能发现部分越界,最小 SP 能观察到采样点处的使用量,但不能证明任意更深调用从未达到更低地址。完整高水位分析需要填充整个栈并扫描,或在更多位置记录。

小系统没有为每个任务提供完善的 guard page。扩大 printf 调用、局部数组或递归以后,仍然要重新计算栈预算。

Abort 的记录比盲目返回更重要

当前 fatal 路径输出 vector、异常 LR、SPSR,以及 DFSR/DFAR/IFSR/IFAR,然后停止。它没有实现缺页修复和用户异常信号。

诊断顺序应是:先用 vector 判断异常类别,再解码对应 FSR,结合有效 FAR、SPSR 和反汇编定位。对于某些非精确异常,FAR 或 LR 不足以精确定位出错的那条访存,不能机械使用“打印地址减常量”。

异常规则可查 ARM ARM 的 B1.8,尤其是 Exception return addresses 与 Exception return。结合源码画出上面的 frame,比只记住一个 SUBS 返回公式更可靠。

下一篇:GIC、Timer 与一条完整 IRQ 链路

重点回看

IRQ 可发生在任意指令边界,硬件只保存少量控制状态,软件必须建立完整可恢复 frame。返回 PC 需要按异常类型修正,C handler 可以运行在独立 IRQ 栈上,但任务的可恢复现场必须有稳定所有者。恢复 PC 和 CPSR 是一个不可拆开的动作。

本章总结

IRQ 可发生在任意指令边界,硬件只保存少量控制状态,软件必须建立完整可恢复 frame。返回 PC 需要按异常类型修正,C handler 可以运行在独立 IRQ 栈上,但任务的可恢复现场必须有稳定所有者。恢复 PC 和 CPSR 是一个不可拆开的动作。

下一章把安全的 IRQ 入口接到 GIC 与 Private Timer,建立可重复中断的状态闭环。

ARMv7-A小系统实践(六):异常入口、返回地址与独立中断栈

https://goko-son626.github.io/post/armv7a-05-exception-frames.html

作者

GoKo Mell

发布于

2026-06-21

更新于

2026-06-21

许可协议

评论

:D 一言句子获取中...