3. ARM异常处理:异常向量表、异常优先级、中断处理流程、FIQ与IRQ的区别、SVC调用
异常处理,说白了就是CPU在遇到突发事件时的应急响应机制。我刚开始接触ARM时,总觉得这玩意儿就是查表跳转,没啥技术含量。直到有一次调试一个随机死机的bug,折腾了整整三天,最后发现是异常向量表被意外修改了……嗯,从那以后我再也不敢小看这章内容了。
3.1 异常向量表——CPU的紧急通讯录
ARM的异常向量表,你可以把它想象成一张“紧急事件处理通讯录”。CPU遇到异常时,会直接去这张表里找对应的处理函数入口。
这张表通常放在内存的固定位置。ARM有两种模式:
- 低端向量表:起始地址
0x00000000 - 高端向量表:起始地址
0xFFFF0000
我个人习惯用高端向量表,尤其是在做Linux内核移植时。为什么?因为低端地址往往被Bootloader或ROM占用,你写进去也改不了。
来看一个典型的向量表布局:
; ARM异常向量表(以低端为例)
0x00000000: LDR PC, Reset_Addr ; 复位
0x00000004: LDR PC, Undef_Addr ; 未定义指令
0x00000008: LDR PC, SWI_Addr ; 软件中断(SVC)
0x0000000C: LDR PC, Prefetch_Addr ; 预取指中止
0x00000010: LDR PC, Data_Addr ; 数据中止
0x00000014: LDR PC, IRQ_Addr ; 普通中断
0x00000018: LDR PC, FIQ_Addr ; 快速中断
3.2 异常优先级——谁先谁后?
ARM处理器在同一时刻只能处理一个异常。如果多个异常同时发生,谁先谁后?看优先级:
| 优先级 | 异常类型 | 说明 |
|---|---|---|
| 1(最高) | 复位 | 优先级最高,谁来了都得让路 |
| 2 | 数据中止 | 内存访问出问题,必须立即处理 |
| 3 | FIQ | 快速中断,优先级高于IRQ |
| 4 | IRQ | 普通中断 |
| 5 | 预取指中止 | 指令预取失败 |
| 6 | SVC / 未定义指令 | 软件触发或非法指令 |
你想想看,复位优先级最高,这很好理解——系统都重启了,其他异常自然没意义。但有个细节很多人忽略:FIQ的优先级高于IRQ,这意味着FIQ处理期间,IRQ会被屏蔽。
3.3 中断处理流程——从硬件到软件
一个完整的中断处理流程,我习惯把它拆成5步:
- 硬件自动完成:CPU检测到中断信号,保存当前状态(CPSR到SPSR,PC到LR),切换到中断模式
- 向量表跳转:CPU根据中断类型,从向量表取出目标地址并跳转
- 现场保护:软件保存被中断任务的寄存器(R0-R12等)
- 中断服务:执行实际的中断处理逻辑
- 现场恢复:恢复寄存器,执行返回指令
这里有个坑:ARM的LR在中断模式下会被修改。如果你在中断里调用了子函数,LR会被覆盖,返回地址就丢了。我曾经因为这个bug,查了整整一个下午……
正确的做法是:进入中断后立即保存LR,或者用 SUBS PC, LR, #4 这种技巧来返回。
; IRQ中断处理入口示例
IRQ_Handler:
SUB LR, LR, #4 ; 修正返回地址
STMFD SP!, {R0-R12, LR} ; 保存现场
BL C_IRQ_Handler ; 调用C语言处理函数
LDMFD SP!, {R0-R12, PC}^; 恢复现场并返回
3.4 FIQ与IRQ的区别——快与稳的博弈
FIQ和IRQ,说白了就是“快车道”和“普通车道”的区别。ARM设计FIQ的初衷,就是为了处理那些对时间极度敏感的任务。
它们的核心区别:
| 特性 | IRQ | FIQ |
|---|---|---|
| 优先级 | 较低 | 较高(高于IRQ) |
| 向量位置 | 0x00000018 | 0x0000001C(最后一个) |
| 私有寄存器 | 无 | 有R8-R12专用副本 |
| 中断嵌套 | 可被FIQ打断 | 不可被打断 |
| 典型用途 | 普通外设中断 | 高速数据传输、实时控制 |
FIQ为什么快?两个原因:
- 寄存器私有化:FIQ模式有自己的R8-R12,省去了保存和恢复这些寄存器的时间
- 向量位置优势:FIQ向量在最后,可以直接在向量表位置写代码,省去一次跳转
我记得有一次做音频处理项目,要求中断响应时间在1微秒以内。IRQ死活达不到,换成FIQ后,响应时间直接降到了0.3微秒。这就是硬件设计带来的红利。
3.5 SVC调用——用户态到内核态的桥梁
SVC(Supervisor Call),以前叫SWI(Software Interrupt),是用户程序主动触发异常的唯一合法途径。你想想看,用户程序不能直接操作硬件,那它想读写文件、创建进程怎么办?通过SVC陷入内核。
SVC调用的核心流程:
- 用户程序执行
SVC #imm指令,其中#imm是系统调用号 - CPU切换到SVC模式,保存返回地址和CPSR
- 跳转到SVC向量表入口
- SVC处理函数根据系统调用号,分发到对应的内核函数
- 执行完成后,返回用户态
; 用户程序发起SVC调用
MOV R0, #1 ; 参数1
MOV R1, #100 ; 参数2
SVC #0x05 ; 系统调用号5(假设是write)
; SVC处理函数(内核态)
SVC_Handler:
STMFD SP!, {R0-R12, LR}
LDR R0, [SP] ; 获取系统调用号
CMP R0, #0x05
BLEQ sys_write ; 调用对应的内核函数
LDMFD SP!, {R0-R12, PC}^
3.6 知识体系总览
下面这张图,把本章的核心知识点串在了一起。我建议你把它保存下来,复习时对照着看:
3.7 避坑总结
最后,把我这些年踩过的坑总结一下,希望能帮你少走弯路:
- 向量表对齐:向量表必须4字节对齐,否则CPU跳转会出错。我见过有人用
__attribute__((aligned(4)))都没用对,结果程序跑飞了 - 中断嵌套:FIQ处理期间不要开IRQ,否则优先级反转会让你怀疑人生
- 栈空间:每种异常模式都要有独立的栈。我习惯给FIQ分配至少1KB,IRQ分配512B,SVC模式分配4KB
- 返回地址:IRQ返回要减4,FIQ不用。这个记不住的话,调试时打印LR值看看就明白了