异常模型与路由:EL0-EL3、同步异步异常、路由机制

异常模型,说白了就是ARMv8/v9架构的“中断与特权级切换”规则。你想想看,一个系统里同时跑着Linux、TrustZone、Hypervisor,谁该响应中断?谁该处理缺页?谁该接管安全监控?答案全在异常等级和路由机制里。

我个人习惯把异常模型看作“电梯系统”——EL0是地下室,EL3是顶层。异常就是按电梯按钮,路由机制决定哪一层停。嗯,咱们一层层拆开看。

异常等级(Exception Levels)

ARMv9定义了4个异常等级,从EL0到EL3。数字越大,特权越高。

等级 典型用途 运行环境
EL0 用户态应用 非特权,无系统寄存器访问
EL1 操作系统内核 特权,管理MMU、中断、调度
EL2 Hypervisor(虚拟机监控器) 虚拟化扩展,管理VM切换
EL3 Secure Monitor(安全监控器) 最高特权,控制安全状态切换

我在项目中遇到过一个问题:某款SoC的EL2代码里直接访问了EL3的寄存器,结果系统直接挂死。后来查手册才发现,EL2根本看不到EL3的寄存器空间——这是硬件强制隔离的。

关键点:异常等级是硬件强制隔离的。EL3的代码可以访问所有低等级的内存和寄存器,但EL2看不到EL3,EL1看不到EL2,EL0什么都看不到。这种“单向可见性”是安全的基础。

同步异常 vs 异步异常

异常分两类:同步异常和异步异常。区别很简单——同步异常是“自找的”,异步异常是“被找的”。

同步异常(Synchronous Exceptions)

同步异常由当前指令直接触发。比如你执行了一条未定义指令,或者访问了一个不存在的内存地址。这类异常的特点是:异常返回地址指向触发异常的指令(或者紧挨着它的下一条)。

  • 未定义指令异常:执行了CPU不认识的指令
  • 缺页异常:MMU找不到页表映射
  • 对齐异常:访问未对齐的地址
  • SVC/HVC/SMC指令:软件主动触发异常(系统调用)

我曾经调试过一个bug:某驱动在EL1执行了SMC指令,结果异常路由到了EL2而不是EL3。查了半天才发现,SCR_EL3的NS位没配好——EL2拦截了本该去EL3的SMC。嗯,这种坑踩过一次就记住了。

异步异常(Asynchronous Exceptions)

异步异常来自外部,跟当前执行的指令无关。典型的就是中断和SError。

  • IRQ:普通中断,比如网卡、UART
  • FIQ:快速中断,优先级更高
  • SError:系统错误,比如ECC校验失败、总线错误

异步异常的返回地址不固定——它可能发生在任意指令之间。所以处理异步异常时,你不能假设返回地址指向哪条指令。

个人经验:调试SError是最头疼的。因为它不告诉你哪条指令出错了,只告诉你“总线出问题了”。我一般会在SError处理函数里打印所有系统错误状态寄存器(比如RAS寄存器),然后反推是哪个外设挂了。

异常路由机制

异常路由,就是决定“这个异常该由哪个EL处理”。ARMv9的路由规则由一组系统寄存器控制,核心是SCR_EL3、HCR_EL2和VBAR_ELx。

路由决策流程

当一个异常发生时,硬件按以下顺序判断路由目标:

  1. 检查异常类型:同步还是异步?来自哪个EL?
  2. 检查路由控制寄存器:SCR_EL3决定是否路由到EL3,HCR_EL2决定是否路由到EL2
  3. 查找异常向量表:根据目标EL的VBAR_ELx找到对应的异常处理入口

注意:异常路由是硬件行为,软件无法干预。你只能通过配置SCR_EL3和HCR_EL2来“建议”硬件怎么路由,但最终决定权在硬件。

异常向量表(Vector Table)

每个EL都有自己的异常向量表,基地址由VBAR_ELx寄存器指定。向量表有16个入口,每个入口对应一种异常场景:

偏移 异常类型 说明
0x000 同步异常(当前EL,SP0) 使用SP_EL0时的同步异常
0x080 IRQ(当前EL,SP0) 使用SP_EL0时的IRQ
0x100 FIQ(当前EL,SP0) 使用SP_EL0时的FIQ
0x180 SError(当前EL,SP0) 使用SP_EL0时的SError
0x200 同步异常(当前EL,SPx) 使用SP_ELx时的同步异常
0x280 IRQ(当前EL,SPx) 使用SP_ELx时的IRQ
0x300 FIQ(当前EL,SPx) 使用SP_ELx时的FIQ
0x380 SError(当前EL,SPx) 使用SP_ELx时的SError
0x400 同步异常(低EL,AArch64) 从低EL来的同步异常
0x480 IRQ(低EL,AArch64) 从低EL来的IRQ
0x500 FIQ(低EL,AArch64) 从低EL来的FIQ
0x580 SError(低EL,AArch64) 从低EL来的SError
0x600 同步异常(低EL,AArch32) 从32位低EL来的同步异常
0x680 IRQ(低EL,AArch32) 从32位低EL来的IRQ
0x700 FIQ(低EL,AArch32) 从32位低EL来的FIQ
0x780 SError(低EL,AArch32) 从32位低EL来的SError

每个入口占0x80字节(128字节)。你想想看,16个入口总共2KB。我建议你在写启动代码时,把VBAR_ELx对齐到2KB边界——这是硬件要求,不对齐的话异常直接挂死。

路由控制寄存器

两个核心寄存器控制路由:

  • SCR_EL3:Secure Configuration Register。控制哪些异常路由到EL3。比如SCR_EL3的IRQ位为1时,所有IRQ都路由到EL3。
  • HCR_EL2:Hypervisor Configuration Register。控制哪些异常路由到EL2。比如HCR_EL2的FMO位为1时,FIQ路由到EL2。

我曾经在调试一个虚拟化场景时,发现虚拟机里的中断总是被EL2拦截了。后来发现是HCR_EL2的IMO位被误设为1——本该路由到EL1的IRQ全跑到了EL2。嗯,这种配置错误在开发初期特别容易踩。

路由优先级:EL3的配置优先级最高。如果SCR_EL3把某个异常路由到EL3,那么HCR_EL2的配置就无效了。EL2只能路由那些EL3“放行”的异常。

异常路由流程图

下面这张图展示了异常从发生到处理的完整路由路径:

ARMv9异常路由流程图 ARMv9异常路由流程图 异常发生 同步异常?异步异常? 同步异常 异步异常 路由决策:SCR_EL3 → HCR_EL2 → VBAR_ELx 目标EL:EL0/EL1/EL2/EL3 异常向量表 → 处理函数 路由优先级:EL3 > EL2 > EL1

实际开发中的避坑指南

异常模型看着简单,实际开发中坑不少。我列几个常见的:

  • VBAR对齐问题:VBAR_ELx必须2KB对齐。不对齐的话,异常直接触发“三重故障”——系统死锁。我见过有人把VBAR放在1KB边界上,调试了一周才发现。
  • SP选择问题:异常处理时,硬件自动切换SP。如果你在EL1用SP_EL1,但异常向量表里配了SP_EL0的入口,那栈指针就乱了。嗯,这种bug特别隐蔽。
  • 路由配置冲突:SCR_EL3和HCR_EL2的配置不能冲突。比如SCR_EL3把IRQ路由到EL3,但HCR_EL2也试图拦截IRQ——结果是EL3优先,HCR_EL2的配置被忽略。
  • SError的“幽灵”特性:SError是异步的,可能延迟到异常处理完成后才触发。我建议在每次异常返回前,主动检查SError状态寄存器,避免“幽灵错误”累积。

我的调试习惯:在异常处理函数入口处,先保存所有通用寄存器和系统寄存器(ESR_ELx、FAR_ELx、SPSR_ELx等),然后打印到串口。这样即使系统崩溃,也能从日志里反推异常原因。

总结

异常模型是ARMv9架构的“交通规则”。EL0到EL3定义了4个特权等级,同步异常和异步异常决定了异常来源,路由机制决定了异常去向。你只要记住三点:

  1. 异常等级:EL0最低,EL3最高,硬件强制隔离
  2. 异常类型:同步异常是“自找的”,异步异常是“被找的”
  3. 路由规则:SCR_EL3优先,HCR_EL2次之,VBAR_ELx决定入口