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 等)加载下一阶段引导程序。因此,复位入口的物理位置和映射关系是启动链路的第一环。

复位入口 = 异常向量表基地址(VBAR_ELx)+ 偏移 0x000。复位后处理器从该地址取指,通常位于 BootROM 或片内 SRAM,而非 DRAM。

二、异常向量表: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,哪怕只是指向一个临时的“死循环”处理程序。

AArch64 异常向量表结构(16 表项 × 128 字节) 来源 / 类型 同步异常 IRQ FIQ SError 当前 EL,SP_EL0 偏移 0x000 0x000 0x080 0x100 0x180 当前 EL,SP_ELx 偏移 0x200 0x200 0x280 0x300 0x380 较低 EL,AArch64 偏移 0x400 0x400 0x480 0x500 0x580 较低 EL,AArch32 偏移 0x600 0x600 0x680 0x700 0x780 复位入口:偏移 0x000(当前 EL,SP_EL0 的同步异常表项),由 VBAR_EL3/EL2/EL1 指向基地址。 每个表项 128 字节(0x80),共 16 项,总计 2KB,基地址需 2KB 对齐。

三、同步异常与异步异常的本质区别

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 位屏蔽

四、启动阶段的异常处理策略

启动早期,系统资源极度受限:栈可能尚未建立、向量表可能未设置、串口可能未初始化。此时若发生异常,最危险的情况是处理器跳转到未初始化地址,导致“跑飞”且无任何日志输出。因此,启动阶段的异常处理策略应遵循“先兜底、再细化”的原则。

  1. 尽早设置 VBAR_ELx:在启动代码的最初阶段,将 VBAR_ELx 指向一个临时的向量表,表中每个表项放置一个死循环或跳转到统一异常处理入口的指令。这样即使发生异常,处理器也会停在可预期的位置,便于调试器捕获。
  2. 屏蔽异步异常:通过设置 DAIF 寄存器,屏蔽 IRQ、FIQ 和 SError。启动阶段通常不需要响应中断,屏蔽可以避免中断控制器未初始化时误触发。待 GIC 等中断控制器初始化完成后,再按需放开。
  3. 建立最小栈:在进入 C 语言代码之前,必须为当前异常级别设置好栈指针(SP_ELx)。异常处理程序通常需要保存上下文,没有栈就无法保存寄存器,也无法调用 C 函数。
  4. 统一异常入口与日志:在向量表建立后,将同步异常和异步异常统一跳转到 C 语言异常处理函数。该函数读取 ESR_ELx、FAR_ELx、ELR_ELx 等寄存器,打印异常类型和地址,然后进入死循环或复位。
  5. 逐步放开异步异常:当外设和中断控制器初始化完成后,清除 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      // 同步异常统一入口
// 其余表项可填充为死循环或跳转到同一处理函数
启动阶段最常见的误区是:认为“异常不会发生”而不设置向量表。实际上,未对齐访问、未定义指令、甚至调试断点都可能触发同步异常。没有向量表兜底,处理器会跳转到随机地址,导致难以复现的挂死。另一个误区是过早放开 IRQ,此时 GIC 未初始化,中断可能被错误路由或反复触发。

五、案例与复盘:一次启动挂死的定位

某 ARM64 平台在启动时偶发挂死,无串口输出。通过 JTAG 调试器暂停处理器,发现 PC 指向一个未映射的地址,且 VBAR_EL3 仍为复位默认值 0。进一步排查发现,启动代码在设置 VBAR_EL3 之前执行了一条未对齐的 LDR 指令,触发了对齐异常。由于向量表未设置,处理器跳转到地址 0 附近的随机数据区,最终跑飞。

复盘结论:启动代码应遵循“先设向量表、再屏蔽异步异常、再建栈、最后执行复杂操作”的顺序。任何可能触发异常的操作(如未对齐访问、除零、未定义指令)都应在向量表就绪之后执行。此外,临时向量表中的处理程序应尽量简单,避免在异常上下文中调用未初始化的外设。

调试启动阶段异常时,可借助 JTAG/SWD 暂停处理器,读取 ESR_ELx(异常综合征寄存器)、ELR_ELx(异常返回地址)、FAR_ELx(错误地址寄存器)来快速定位异常原因。若 VBAR_ELx 为 0 且 PC 跑飞,优先检查向量表是否在异常发生前已正确设置。

总结来说,复位入口是启动链路的第一条指令,异常向量表是异常响应的“路由表”,同步异常与异步异常在触发机制和返回精度上有本质区别。启动阶段的异常处理策略应以“兜底优先、逐步放开”为原则,尽早设置向量表、屏蔽异步异常、建立最小栈,并在资源就绪后逐步细化处理逻辑。掌握这些要点,才能在早期启动中快速定位和规避异常导致的挂死问题。