深入理解 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的条件或系统事件。处理完成后特权软件负责准备核心恢复到异常发生前的状态继续执行。在很多资料里exception、interrupt、trap这几个词经常混着用但在 ARM 语境里最好区分开Exception所有打断正常执行流的事件的统称Interrupt特指 IRQ 和 FIQ 这两种异步硬件中断Trap非 ARM 官方术语在虚拟化语境下有时指 Hyp Trap从程序员视角看理解异常机制本质上就是回答三个问题从哪来—— 什么事件触发了异常到哪去—— 处理器跳到哪个地址执行处理程序怎么回—— 处理完毕后如何恢复被中断的程序ARMv7-A 的异常处理有一个很重要的特点它和处理器模式是紧密绑定的。不同类型的异常会让处理器自动进入不同的模式同时硬件还会自动保存现场的一部分关键状态例如把当前CPSR保存到目标模式的SPSR把返回地址写入目标模式的LR根据异常类型设置中断屏蔽位把PC重定向到异常向量表对应入口这也是 ARM 异常机制非常“硬件化”的地方异常入口的大量基础动作硬件已经替软件做掉了。2、异常类型详解ARMv7-A 常见的异常类型如下表所示。表中的“向量偏移”是相对于异常向量表基址而言的偏移而不是绝对地址。异常类型进入模式向量偏移触发方式同步/异步ResetSVC0x00复位信号异步Undefined InstructionUND0x04未定义指令 / 未知协处理器指令同步SVCSVC0x08SVC指令同步HVCHYP0x08(Hyp 向量表)HVC指令虚拟化扩展同步SMCMON0x08(Monitor 向量表)SMC指令安全扩展同步Prefetch AbortABT0x0C指令预取失败同步Data AbortABT0x10数据访问失败同步Hyp TrapHYP0x14Hyp 模式下的异常入口虚拟化—IRQIRQ0x18普通中断信号异步FIQFIQ0x1C快速中断信号异步说明ARMv7-A 在引入安全扩展和虚拟化扩展后可能存在多套向量表例如 Non-secure、Secure、Monitor、Hyp。上表中的偏移值例如0x18、0x1C都只是“表内偏移”实际入口地址还要结合向量表基址一起看。2.1 IRQ 与 FIQARMv7-A 提供两类外部中断请求信号IRQInterrupt Request普通中断系统中绝大多数外设中断都走这条路径。FIQFast Interrupt Request快速中断优先级高于IRQ并且具备硬件层面的速度优势FIQ 模式拥有私有的 R8-R12处理程序无需压栈即可直接使用进一步减少时钟周期开销设计原则FIQ 预留给单一的、需要保证快速响应时间的高优先级中断源并且 FIQ 处理程序被期望不再产生任何其他异常不能有 SVC 调用不能触发缺页等。这是因为 FIQ 不会禁用自身如果再产生新的异常处理将变得极其复杂。一个关键的差异在于中断屏蔽行为进入IRQ模式时硬件自动置位CPSR.I 1也就是屏蔽后续IRQ但CPSR.F不会因此自动置位所以FIQ仍然可以抢占IRQ。进入FIQ模式时硬件会同时置位CPSR.F 1和CPSR.I 1也就是在处理FIQ期间同时屏蔽FIQ和IRQ。在 Linux 世界里FIQ并不是通用主路径。主流 Linux 内核日常主要使用IRQ体系FIQ往往只在一些非常特定的 SoC 方案、调试方案或安全方案里才会被使用。部分 Cortex-A 处理器还支持把FIQ配置成NMFINon-Maskable FIQ。在这种配置下软件不能简单通过修改CPSR.F来长期屏蔽FIQ这通常由硬件配置决定。2.2 Abort中止异常Abort 是最复杂的异常类型由失败的内存访问触发。按来源可分为两个维度1按访问阶段类型向量偏移触发时机LR 调整值Prefetch Abort0x0C指令预取失败。异常在指令执行前触发。LR - 4Data Abort0x10数据读写失败。异常在 load/store 指令尝试执行后触发。LR - 8为什么两者的返回修正不同根本原因在于ARM 流水线导致异常发生时的PC已经不是“当前那条指令的地址”。对Prefetch Abort来说处理器是在取指过程中发现问题因此返回时通常用LR - 4回到故障指令。对Data Abort来说错误是在数据访问阶段暴露出来的此时流水线更靠后因此通常要用LR - 8才能回到出问题的那条指令。这也是为什么异常返回代码里经常会看到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其中DFSRData Fault Status Register记录 Data Abort 的原因DFARData Fault Address Register记录 Data Abort 的相关地址IFSR/IFAR则对应取指相关故障把这些寄存器信息和修正后的返回地址结合起来异常处理程序才能判断这是缺页可以修页表后重试这是权限错误应当向用户态抛异常这是总线错误或严重硬件故障应当终止当前任务甚至停机2.3 ResetReset是最高优先级异常。它不属于“程序运行过程中某条指令触发的异常”而是整个处理器执行环境被重新拉回初始状态的入口。复位后处理器通常进入SVC模式并从复位向量开始执行。常见的初始化工作包括建立异常向量表初始化栈初始化时钟、内存控制器、串口等基础硬件初始化 MMU/Cache初始化 VFP/NEON为多核系统唤醒其他 CPU最后跳入main()或更上层的启动代码如果是多核系统通常并不是每个核都完整跑一遍同样的启动流程。更常见的做法是主核负责系统初始化从核先等待后续再被主核唤醒。2.4 指令触发的异常除了外部事件和硬件错误ARM 里还有一类异常是软件主动触发的。它们本质上是在请求更高特权级提供服务。指令进入模式用途扩展要求SVCSVC (PL1)User 模式请求操作系统服务系统调用无HVCHYP (PL2)Guest OS 请求 Hypervisor 服务虚拟化扩展SMCMON (PL1, Secure)Normal World 请求 Secure World 服务安全扩展此外任何核心无法识别的指令包括不存在或不使能的协处理器指令会触发UNDEFINED异常。这个机制的重要应用之一是指令模拟——比如硬件 VFP 未实现或不使能时通过 UNDEFINED 异常处理程序来用软件模拟浮点运算。在 Thumb 状态执行SVC时取 SVC 立即数的方法和 ARM 状态不同。异常处理程序通常需要结合SPSR.T判断调用方来自 ARM 还是 Thumb再决定如何解析原始指令编码。3、异常优先级当多个异常同时发生时硬件按照固定的优先级顺序处理。每种异常进入时对CPSR.I和CPSR.F屏蔽位的行为也不同优先级异常进入模式CPSR.ICPSR.F最高ResetSVC11Data AbortABT1不变FIQFIQ11IRQIRQ1不变Prefetch AbortABT1不变最低Undefined / SVCUND / SVC1不变两个关键推论FIQ 可以打断 IRQ 和 Abort 处理程序。如果 Data Abort 和 FIQ 同时发生Data Abort更高优先级先被处理。因为 Data Abort 不屏蔽 FIQF 位不变核心随后立即进入 FIQ 处理程序。FIQ 处理完毕后再返回继续 Data Abort。未定义指令和 SVC 是互斥的——它们都由执行指令触发一条指令不可能既是 SVC 又是未定义指令。同理Prefetch Abort 标记了无效指令也不可能与 Undef / SVC 同时发生。注意ARM 架构并未定义异步异常FIQ、IRQ、异步 Abort的确切采样时机。因此异步异常相对于同步异常的优先级实际上是implementation defined。4、异常向量表异常向量表可以理解成处理器遇到某类异常后第一跳会去查的一张固定入口表。在经典 ARM 状态下向量表由一组固定偏移的入口组成每个入口只有 4 字节空间因此通常放不下完整处理逻辑而只是放一条跳转指令。4.1 向量表布局引入安全扩展和虚拟化扩展之后ARMv7-A 可能维护多套向量表分别用于不同执行环境向量偏移Normal 模式Secure 模式Hyp 模式Monitor 模式0x00ResetReset——0x04UndefinedUndefinedUndefined (from Hyp)—0x08SVCSVC—SMC0x0CPrefetch AbortPrefetch AbortPrefetch Abort (from Hyp)Prefetch Abort0x10Data AbortData AbortData Abort (from Hyp)Data Abort0x14——Hyp Trap Entry—0x18IRQIRQIRQIRQ0x1CFIQFIQFIQFIQ其中0x14这个偏移在传统 ARMv7-A 里是保留的有了虚拟化扩展之后它被 Hyp 模式用作专门的 trap 入口。4.2 向量表基址配置异常入口的“偏移”固定但整张表放在哪里是由基址决定的。在 ARMv7-A 中向量表基址主要由SCTLR.V和VBAR共同决定SCTLR.V向量表基址来源说明0VBAR向量表可放在任意物理/虚拟地址由CP15 c12, c0, 0指定10xFFFF0000高向量HIVECS模式向后兼容旧 ARM 架构这里要注意两个容易混淆的点VBAR并不是“任意值都行”它本身有对齐要求。当启用高向量时异常入口不再从VBAR offset取而是改为固定从高地址区域取。如果处理器支持安全扩展或虚拟化扩展还会额外有MVBARMonitor 向量表基址HVBARHyp 向量表基址4.3 CP15 异常配置关键寄存器寄存器CP15 编码用途VBARc12, c0, 0Non-secure / Secure PL1 向量表基址BankedMVBARc12, c0, 1Monitor 模式向量表基址HVBARc12, c4, 0Hyp 模式向量表基址SCTLR.Vc1, c0, 0bit 13向量表基址选择0 VBAR, 1 0xFFFF0000SCTLR.TEc1, c0, 0bit 30异常处理指令集0 ARM, 1 ThumbSCTLR.EEc1, c0, 0bit 25异常处理字节序0 小端, 1 大端Monitor 模式向量表基址寄存器支持 Hypervisor 扩展时的 Hyp 向量表基址寄存器4.4 向量表入口的两种跳转模式每个异常入口只有 4 字节空间因此向量表中几乎总是放置以下两种跳转指令之一1PC 相对分支B handler_label优点是快、简单缺点是跳转范围有限。ARM 状态下B指令可覆盖大约±32MB的范围。2间接加载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从异常处理返回需要原子地完成两个操作将SPSR_mode恢复到 CPSR恢复处理器模式、中断屏蔽位、指令集状态将 PC 设置为修正后的返回地址ARM 的三级流水线意味着保存到LR_mode里的值往往不是你想直接返回的最终地址移。更关键的是ARM 架构要求Prefetch Abort 和 Undefined 异常必须能够重新执行导致异常的那条指令例如缺页处理程序修复页表后要重新执行触发 abort 的 load/store而非直接跳到下一条。这也意味着不同类型的异常需要不同的 LR 修正值异常类型LR 调整典型返回指令返回目标SVC / UndefLR 0MOVS PC, LRSVC: 下一条指令 / Undef: 重执行当前指令Prefetch AbortLR - 4SUBS PC, LR, #4重新执行导致 abort 的指令Data AbortLR - 8SUBS PC, LR, #8重新执行导致 abort 的指令IRQ / FIQLR - 4SUBS PC, LR, #4被中断的下一条指令注意SUBS中的S后缀当目标寄存器是 PC 时S后缀表示同时将 SPSR 恢复到 CPSR。在 ARM 架构中这一条指令同时完成 PC 跳转和 CPSR 恢复是整个异常返回机制的精髓。这也是MOVS PC, LR被特意设计为异常返回指令的原因——普通的MOV PC, LR不会恢复 SPSR。5.3 三种返回方式方式指令示例适用场景数据处理指令 SSUBS PC, LR, #4最简单的返回方式处理程序未使用栈保存上下文多寄存器加载 ^LDMFD sp!, {R0-R12, PC}^处理程序入口将各寄存器保存在栈上返回时统一恢复。^后缀表示同时恢复 SPSRRFE 指令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.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 支持或地址范围限制本质上仍然是在做“第一跳”。