2. 异常向量表:系统异常的第一道门槛

异常向量表,说白了就是一张「异常处理函数的跳转表」。CPU 遇到异常时,硬件会自动跳转到这张表里对应的位置,然后执行你写好的处理代码。我刚开始接触 ARMv8 时,总觉得这玩意儿就是个简单的地址映射,后来踩过坑才明白——向量表的布局、基址寄存器的配置、同步异步的分类,每一样都藏着细节。

2.1 向量表基址寄存器(VBAR_ELx)

每个异常等级都有自己的向量表基址寄存器:VBAR_EL1、VBAR_EL2、VBAR_EL3。它们分别指向对应异常等级的向量表起始地址。

关键点:VBAR_ELx 必须 32 字节对齐。低 5 位硬件直接忽略,写进去也会被清零。

配置方法很简单,一条指令搞定:

/* 设置 EL1 的向量表基址 */
ldr x0, =vector_table_el1
msr VBAR_EL1, x0
isb   /* 确保生效 */

我在项目中遇到过一个问题:系统启动后偶尔会莫名其妙地跑飞。查了两天才发现,是 bootloader 里配置 VBAR_EL2 时忘了加 isb。你想想看,指令流水线还在用旧的向量表,异常一来直接跳到了错误的地方。嗯,从那以后我写 VBAR 配置,后面必跟一条 isb。

寄存器 所属异常等级 对齐要求 复位值
VBAR_EL1 EL1(操作系统) 32 字节 IMPLEMENTATION DEFINED
VBAR_EL2 EL2(虚拟化) 32 字节 IMPLEMENTATION DEFINED
VBAR_EL3 EL3(安全监控) 32 字节 IMPLEMENTATION DEFINED

个人习惯:我会在链接脚本里把向量表单独放在一个段里,强制 32 字节对齐。这样既保证对齐,又方便调试时定位。

2.2 向量表偏移与布局

ARMv8 的向量表布局,我建议你直接记这张图:

ARMv8 异常向量表布局(每个异常等级) 偏移量 异常类型 触发条件 0x000 同步异常(Synchronous) 数据/指令中止 SVC/HVC/SMC 0x080 IRQ(异步) 普通中断请求 0x100 FIQ(异步) 快速中断请求 0x180 SError(异步) 系统错误 每个异常等级包含 4 组 × 4 种异常类型 = 16 个入口 组 0 当前EL,使用SP0 组 1 当前EL,使用SPx 组 2 低EL,AArch64 组 3 低EL,AArch32 ⚠ 注意: 每个入口占用 0x80 字节(128 字节),共 16 个入口。 向量表总大小 = 16 × 0x80 = 0x800 字节(2KB)。

这张图我建议你保存下来。每个入口 0x80 字节,一共 16 个入口,正好 2KB。为什么是 0x80?因为 ARM 希望你在入口处直接放一小段跳转代码,而不是把整个处理函数塞进去。我见过有人把几百行的处理逻辑直接写在向量表里——结果代码越界,覆盖了下一个入口,调试时简直噩梦。

我曾经踩过的坑:在 EL3 下配置了 VBAR_EL3,但忘记配置 SCR_EL3 的 EA 位。结果 SError 来了,CPU 根本不走向量表,直接挂死。查了两天,最后发现是安全配置位没设对。

2.3 同步异常与异步异常分类

异常分两大类:同步异常和异步异常。区分它们其实很简单——看异常和当前指令的关系。

同步异常

同步异常是由当前正在执行的指令直接触发的。换句话说,CPU 执行到某条指令时,发现不对劲,当场就报异常。这类异常的特点是:精确的,你能准确知道是哪条指令出了问题。

  • 数据/指令中止:访问内存时 MMU 报错,比如缺页、权限错误
  • 未定义指令:CPU 不认识这条指令编码
  • SVC/HVC/SMC:系统调用指令,主动触发异常进入更高异常等级
  • 对齐错误:比如在要求 4 字节对齐的地址上做 4 字节访问
  • 断点/观察点:调试时用的硬件断点

异步异常

异步异常和当前执行的指令没有直接关系。它可能来自外部设备、总线错误、或者系统内部的其他事件。这类异常是不精确的——你只知道异常发生了,但不知道具体是哪条指令导致的。

  • IRQ:普通中断,来自外设
  • FIQ:快速中断,优先级更高
  • SError:系统错误,比如总线访问超时、ECC 校验错误
特性 同步异常 异步异常
触发源 当前指令 外部事件/系统错误
精确性 精确,可定位到指令 不精确,无法定位
返回地址 触发异常的指令地址 下一条或上一条指令
典型例子 缺页、未定义指令、SVC IRQ、FIQ、SError
调试难度 相对容易 较难,需要分析上下文

调试技巧:遇到 SError 时,先检查 ESR_ELx 寄存器。它的低 6 位是异常类型编码,能告诉你到底是同步还是异步,以及具体是哪一类。我调试时第一件事就是打印 ESR,省掉很多猜谜时间。

2.4 向量表配置实战要点

配置向量表时,有几个细节我建议你特别注意:

  1. 对齐检查:VBAR_ELx 必须 32 字节对齐,但向量表本身建议 2KB 对齐,方便后续扩展
  2. ISB 同步:修改 VBAR 后必须执行 ISB,否则流水线可能还在用旧表
  3. 入口代码精简:每个入口只放跳转指令,不要写复杂逻辑
  4. 未使用入口:建议填默认处理函数,至少打印错误信息,不要留空
  5. 多核系统:每个核可以有自己的向量表,通过 VBAR_ELx 独立配置
/* 向量表示例(汇编) */
.align 11    /* 2KB 对齐 */
vector_table_el1:
  /* 组 0:当前EL,SP0 */
  .align 7    /* 每个入口 0x80 对齐 */
  b   sync_curr_sp0      /* 0x000:同步异常 */
  .align 7
  b   irq_curr_sp0       /* 0x080:IRQ */
  .align 7
  b   fiq_curr_sp0       /* 0x100:FIQ */
  .align 7
  b   serror_curr_sp0    /* 0x180:SError */

  /* 组 1:当前EL,SPx */
  .align 7
  b   sync_curr_spx      /* 0x200 */
  .align 7
  b   irq_curr_spx       /* 0x280 */
  .align 7
  b   fiq_curr_spx       /* 0x300 */
  .align 7
  b   serror_curr_spx    /* 0x380 */

  /* 组 2:低EL,AArch64 */
  .align 7
  b   sync_low_a64      /* 0x400 */
  .align 7
  b   irq_low_a64       /* 0x480 */
  .align 7
  b   fiq_low_a64       /* 0x500 */
  .align 7
  b   serror_low_a64    /* 0x580 */

  /* 组 3:低EL,AArch32 */
  .align 7
  b   sync_low_a32      /* 0x600 */
  .align 7
  b   irq_low_a32       /* 0x680 */
  .align 7
  b   fiq_low_a32       /* 0x700 */
  .align 7
  b   serror_low_a32    /* 0x780 */

核心总结:向量表是异常处理的入口,配置错了系统连异常都处理不了。VBAR_ELx 对齐、ISB 同步、入口代码精简——这三条做到位,向量表这块基本不会出问题。