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 的向量表布局,我建议你直接记这张图:
这张图我建议你保存下来。每个入口 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 向量表配置实战要点
配置向量表时,有几个细节我建议你特别注意:
- 对齐检查:VBAR_ELx 必须 32 字节对齐,但向量表本身建议 2KB 对齐,方便后续扩展
- ISB 同步:修改 VBAR 后必须执行 ISB,否则流水线可能还在用旧表
- 入口代码精简:每个入口只放跳转指令,不要写复杂逻辑
- 未使用入口:建议填默认处理函数,至少打印错误信息,不要留空
- 多核系统:每个核可以有自己的向量表,通过 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 同步、入口代码精简——这三条做到位,向量表这块基本不会出问题。