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        ; 快速中断
注意:每个向量表条目只占4字节。你放不下完整的中断处理函数,只能放一条跳转指令。我见过新手直接在这里写函数体,结果后面的向量全被覆盖了——那画面太美不敢看。

3.2 异常优先级——谁先谁后?

ARM处理器在同一时刻只能处理一个异常。如果多个异常同时发生,谁先谁后?看优先级:

优先级 异常类型 说明
1(最高) 复位 优先级最高,谁来了都得让路
2 数据中止 内存访问出问题,必须立即处理
3 FIQ 快速中断,优先级高于IRQ
4 IRQ 普通中断
5 预取指中止 指令预取失败
6 SVC / 未定义指令 软件触发或非法指令

你想想看,复位优先级最高,这很好理解——系统都重启了,其他异常自然没意义。但有个细节很多人忽略:FIQ的优先级高于IRQ,这意味着FIQ处理期间,IRQ会被屏蔽。

经验之谈:我在做高速数据采集卡时,把时间敏感的数据传输放在FIQ里,把按键扫描、LED闪烁这类不紧急的任务放在IRQ里。这样既保证了实时性,又不会让CPU被琐事拖死。

3.3 中断处理流程——从硬件到软件

一个完整的中断处理流程,我习惯把它拆成5步:

  1. 硬件自动完成:CPU检测到中断信号,保存当前状态(CPSR到SPSR,PC到LR),切换到中断模式
  2. 向量表跳转:CPU根据中断类型,从向量表取出目标地址并跳转
  3. 现场保护:软件保存被中断任务的寄存器(R0-R12等)
  4. 中断服务:执行实际的中断处理逻辑
  5. 现场恢复:恢复寄存器,执行返回指令

这里有个坑: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}^; 恢复现场并返回
关键点:IRQ返回地址需要减4,FIQ不需要。为什么?因为IRQ是在指令执行完成后才被采样,而FIQ是在指令执行前就被采样。这个细节,面试时经常被问到。

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调用的核心流程:

  1. 用户程序执行 SVC #imm 指令,其中 #imm 是系统调用号
  2. CPU切换到SVC模式,保存返回地址和CPSR
  3. 跳转到SVC向量表入口
  4. SVC处理函数根据系统调用号,分发到对应的内核函数
  5. 执行完成后,返回用户态
; 用户程序发起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}^
避坑指南:我曾经在移植Linux时,发现SVC调用偶尔会返回错误。查了半天,原来是用户态和内核态的栈指针(SP)没有正确切换。记住:SVC模式下必须使用内核栈,否则用户栈溢出会直接导致内核崩溃。

3.6 知识体系总览

下面这张图,把本章的核心知识点串在了一起。我建议你把它保存下来,复习时对照着看:

ARM异常处理知识体系 ARM异常处理知识体系 异常向量表 异常优先级 中断处理流程 向量表布局 • 低端:0x00000000 • 高端:0xFFFF0000 • 每个条目4字节跳转指令 • 共8个异常类型 优先级排序 • 复位 > 数据中止 > FIQ • FIQ > IRQ > 预取指中止 • SVC / 未定义指令最低 • 高优先级可打断低优先级 5步处理流程 ① 硬件保存状态 ② 向量表跳转 ③ 软件保存现场 ④ 执行ISR → ⑤恢复返回 FIQ vs IRQ 核心对比 FIQ:高优先级 | 私有R8-R12 | 向量在末尾 | 不可嵌套 IRQ:低优先级 | 共享寄存器 | 向量在0x18 | 可被FIQ打断 适用场景:FIQ→高速数据流 | IRQ→普通外设中断 SVC调用(系统调用入口) 用户态 → SVC指令 → 陷入内核 → 系统调用分发 → 返回用户态 关键:栈切换、系统调用号传递、返回地址修正

3.7 避坑总结

最后,把我这些年踩过的坑总结一下,希望能帮你少走弯路:

  • 向量表对齐:向量表必须4字节对齐,否则CPU跳转会出错。我见过有人用 __attribute__((aligned(4))) 都没用对,结果程序跑飞了
  • 中断嵌套:FIQ处理期间不要开IRQ,否则优先级反转会让你怀疑人生
  • 栈空间:每种异常模式都要有独立的栈。我习惯给FIQ分配至少1KB,IRQ分配512B,SVC模式分配4KB
  • 返回地址:IRQ返回要减4,FIQ不用。这个记不住的话,调试时打印LR值看看就明白了
调试技巧:如果系统莫名其妙地死机,先检查异常向量表有没有被意外修改。我写了一个小工具,在启动时计算向量表的CRC并保存,运行时定期校验——一旦发现被篡改,立刻报警。这个方法帮我抓到了好几次内存越界导致的bug。