4. 复位与异常向量
在 ARM64 架构中,处理器上电或复位后并不会立刻进入操作系统,而是从固定的复位入口开始执行。理解复位入口、异常向量表以及同步/异步异常的差异,是掌握早期启动流程的关键。启动阶段的异常处理策略与运行阶段截然不同:此时内存、栈、异常向量表可能都尚未就绪,任何一次异常都可能让系统“静默挂死”。本节将围绕复位入口的定位、异常向量表的结构、两类异常的本质区别,以及启动阶段应采取的异常处理策略展开。
一、复位入口:处理器执行的第一条指令
ARM64 定义了复位异常向量,处理器在复位后从该向量处取指执行。对于 AArch64 状态,复位向量位于异常向量表的第一个表项,即偏移 0x000 处。异常向量表的基地址由 VBAR_EL3、VBAR_EL2 或 VBAR_EL1 寄存器决定,具体使用哪个取决于当前异常级别(EL)。复位时通常处于最高异常级别 EL3(若实现了 EL3),因此复位入口由 VBAR_EL3 指向的向量表决定。
在典型的 SoC 启动流程中,复位入口往往不是直接位于 DRAM,而是映射到片上 ROM(BootROM)或片内 SRAM。这是因为复位瞬间 DRAM 尚未初始化,无法直接访问。BootROM 中的复位代码负责最基础的初始化,然后根据启动模式(如从 eMMC、SPI Flash、UART 等)加载下一阶段引导程序。因此,复位入口的物理位置和映射关系是启动链路的第一环。
二、异常向量表:16 个表项的结构
AArch64 的异常向量表由 16 个表项组成,每个表项占 128 字节(0x80),总计 2KB。这 16 个表项按照“异常类型 × 来源异常级别”两个维度组织:4 种异常类型(同步异常、IRQ、FIQ、SError)分别对应 4 个来源(当前 EL 且使用 SP_EL0、当前 EL 且使用 SP_ELx、较低 EL 使用 AArch64、较低 EL 使用 AArch32)。每个表项内部通常放置若干条指令,用于保存上下文、跳转到具体的处理函数。
异常向量表的基地址必须按 2KB 对齐,因为 VBAR_ELx 的低 11 位被硬件忽略。在启动早期,向量表可能尚未建立,此时若发生异常,处理器会跳转到未初始化的地址,导致不可预期的行为。因此,启动代码通常会在初始化早期就设置好 VBAR_ELx,哪怕只是指向一个临时的“死循环”处理程序。
三、同步异常与异步异常的本质区别
ARM64 将异常分为同步异常和异步异常两大类。同步异常由当前执行的指令直接引起,异常返回地址精确指向触发异常的指令或下一条指令,典型代表包括:指令未定义异常、数据/指令访问权限异常、对齐异常、系统调用(SVC/HVC/SMC)、断点异常(BRK)等。同步异常的关键特征是“可重现、可定位”,处理器能够准确知道是哪条指令导致了异常。
异步异常则与当前指令流无直接因果关系,它在任意时刻“插入”到处理器执行流中,典型代表是 IRQ、FIQ 和 SError。异步异常的返回地址不保证与触发源精确对应,可能指向已经执行过的某条指令。IRQ 和 FIQ 通常来自外设中断控制器,SError 则用于报告内存系统错误、总线错误等严重异步事件。在启动阶段,中断控制器往往尚未初始化,因此 IRQ/FIQ 通常被屏蔽,而 SError 的屏蔽与否则取决于 DAIF 寄存器中的 A 位。
| 对比维度 | 同步异常 | 异步异常 |
|---|---|---|
| 触发原因 | 当前指令直接引起 | 外部事件或系统错误异步插入 |
| 返回地址精度 | 精确指向触发指令或下一条 | 不精确,可能指向已执行指令 |
| 典型代表 | SVC、BRK、数据中止、未定义指令 | IRQ、FIQ、SError |
| 启动阶段处理 | 需立即定位并修复或死循环 | 通常先屏蔽,待初始化后再放开 |
| 屏蔽方式 | 无法屏蔽,只能处理 | 通过 DAIF 的 I/F/A 位屏蔽 |
四、启动阶段的异常处理策略
启动早期,系统资源极度受限:栈可能尚未建立、向量表可能未设置、串口可能未初始化。此时若发生异常,最危险的情况是处理器跳转到未初始化地址,导致“跑飞”且无任何日志输出。因此,启动阶段的异常处理策略应遵循“先兜底、再细化”的原则。
- 尽早设置 VBAR_ELx:在启动代码的最初阶段,将
VBAR_ELx指向一个临时的向量表,表中每个表项放置一个死循环或跳转到统一异常处理入口的指令。这样即使发生异常,处理器也会停在可预期的位置,便于调试器捕获。 - 屏蔽异步异常:通过设置
DAIF寄存器,屏蔽 IRQ、FIQ 和 SError。启动阶段通常不需要响应中断,屏蔽可以避免中断控制器未初始化时误触发。待 GIC 等中断控制器初始化完成后,再按需放开。 - 建立最小栈:在进入 C 语言代码之前,必须为当前异常级别设置好栈指针(SP_ELx)。异常处理程序通常需要保存上下文,没有栈就无法保存寄存器,也无法调用 C 函数。
- 统一异常入口与日志:在向量表建立后,将同步异常和异步异常统一跳转到 C 语言异常处理函数。该函数读取
ESR_ELx、FAR_ELx、ELR_ELx等寄存器,打印异常类型和地址,然后进入死循环或复位。 - 逐步放开异步异常:当外设和中断控制器初始化完成后,清除
DAIF中的I位和F位,允许 IRQ/FIQ 响应。SError 的放开需谨慎,通常在有完整错误处理机制后再开启。
// 启动早期设置临时异常向量表并屏蔽异步异常
msr vbar_el3, x0 // x0 指向临时向量表基地址
msr daifset, #0xf // 屏蔽 D/A/I/F,即调试、SError、IRQ、FIQ
isb // 确保向量表设置生效
// 临时向量表表项示例(每个表项 128 字节)
// 复位入口偏移 0x000
b el3_sync_handler // 同步异常统一入口
// 其余表项可填充为死循环或跳转到同一处理函数
五、案例与复盘:一次启动挂死的定位
某 ARM64 平台在启动时偶发挂死,无串口输出。通过 JTAG 调试器暂停处理器,发现 PC 指向一个未映射的地址,且 VBAR_EL3 仍为复位默认值 0。进一步排查发现,启动代码在设置 VBAR_EL3 之前执行了一条未对齐的 LDR 指令,触发了对齐异常。由于向量表未设置,处理器跳转到地址 0 附近的随机数据区,最终跑飞。
复盘结论:启动代码应遵循“先设向量表、再屏蔽异步异常、再建栈、最后执行复杂操作”的顺序。任何可能触发异常的操作(如未对齐访问、除零、未定义指令)都应在向量表就绪之后执行。此外,临时向量表中的处理程序应尽量简单,避免在异常上下文中调用未初始化的外设。
ESR_ELx(异常综合征寄存器)、ELR_ELx(异常返回地址)、FAR_ELx(错误地址寄存器)来快速定位异常原因。若 VBAR_ELx 为 0 且 PC 跑飞,优先检查向量表是否在异常发生前已正确设置。
总结来说,复位入口是启动链路的第一条指令,异常向量表是异常响应的“路由表”,同步异常与异步异常在触发机制和返回精度上有本质区别。启动阶段的异常处理策略应以“兜底优先、逐步放开”为原则,尽早设置向量表、屏蔽异步异常、建立最小栈,并在资源就绪后逐步细化处理逻辑。掌握这些要点,才能在早期启动中快速定位和规避异常导致的挂死问题。