ARMv8-A 异常模型:EL0-EL3 与异常路由

异常模型,说白了就是 CPU 在遇到「意外情况」时怎么切换工作模式。ARMv8-A 的异常模型设计得相当精巧,我当年刚接触时也被绕晕过。但搞懂它,你才能真正理解 ATF 为什么能作为安全世界的「看门人」。

异常级别 EL0-EL3:四个特权世界

ARMv8-A 把 CPU 的执行状态分成了四个等级,叫 Exception Level,简称 EL。数字越大,权限越高。

异常级别 典型用途 运行环境
EL0 用户态应用程序 非安全/安全
EL1 操作系统内核 非安全/安全
EL2 虚拟机监视器 (Hypervisor) 非安全
EL3 安全监视器 (Secure Monitor) 安全

EL0 是最底层,跑你的 App。EL1 跑操作系统,比如 Linux 内核。EL2 是虚拟化层,跑 KVM 或 Xen。EL3 是最高权限层,ATF 就待在这里。

关键点:EL3 是唯一一个永远处于安全世界的异常级别。它负责管理非安全世界(EL1/EL0)和安全世界(S-EL1/S-EL0)之间的切换。说白了,EL3 就是两个世界的「海关」。我在项目中见过有人把安全配置搞错,结果 EL3 的代码被非安全世界篡改——那场面,直接重启都救不回来。

同步异常与异步异常

ARMv8-A 把异常分成两大类:同步异常和异步异常。区别很简单——同步异常是 CPU 执行指令时自己搞出来的,异步异常是外部事件触发的。

同步异常

同步异常和当前执行的指令直接相关。比如你访问了一个不存在的内存地址,或者执行了一条未定义的指令。CPU 会立即停下来,跳转到异常向量表去处理。

  • SVC (Supervisor Call):用户态程序主动调用内核服务。比如 Linux 里的系统调用。
  • HVC (Hypervisor Call):虚拟机里的 Guest OS 调用 Hypervisor。
  • SMC (Secure Monitor Call):非安全世界调用安全世界。ATF 就是靠这个接口被调用的。
  • Data Abort / Prefetch Abort:内存访问失败或指令预取失败。
  • Undefined Instruction:CPU 不认识这条指令。

我个人的习惯:调试同步异常时,先看 ESR (Exception Syndrome Register)。它里面记录了异常的具体原因,比如是 Alignment fault 还是 Translation fault。省得你瞎猜。

异步异常

异步异常和当前指令没有直接关系。它们是外部事件,比如中断或系统错误。

  • IRQ (Interrupt Request):普通中断。外设发来的。
  • FIQ (Fast Interrupt Request):快速中断。优先级更高,延迟更低。
  • SError (System Error):系统错误。比如内存 ECC 校验失败、总线超时。

你想想看,为什么 ARM 要区分 IRQ 和 FIQ?我当年做安全启动时,就把 FIQ 留给了安全世界的定时器,确保安全监控任务不会被普通中断打断。嗯,这个设计在攻击场景下特别有用——攻击者想用大量 IRQ 淹没 CPU,但 FIQ 依然能准时响应。

异常路由机制:谁来决定异常去哪?

异常发生后,CPU 怎么知道该跳到哪个 EL 去处理?这就靠异常路由机制了。ARMv8-A 提供了一套寄存器来控制路由策略。

核心寄存器

  • SCR_EL3 (Secure Configuration Register):EL3 的配置寄存器。它控制着 IRQ、FIQ、SError 和 SMC 是否路由到 EL3。
  • HCR_EL2 (Hypervisor Configuration Register):EL2 的配置寄存器。控制非安全世界的异常路由。
  • RVBAR_EL3 (Reset Vector Base Address Register):复位向量基地址。CPU 上电后第一条指令从这取。

举个例子,假设你希望所有 FIQ 都直接交给 EL3 处理,而不是让 EL1 或 EL2 先碰。你就在 SCR_EL3 里设置 FIQ 位为 1。这样,任何 EL 下产生的 FIQ 都会直接路由到 EL3。

我曾经踩过的坑:在调试 Secure Monitor 时,忘了配置 SCR_EL3 的 SMC 路由位。结果非安全世界发 SMC 调用,CPU 直接 Undefined Exception 了。查了半天才发现是路由没开。记住:SMC 默认不会路由到 EL3,你得手动配。

异常路由的典型场景

  1. 非安全世界的 IRQ:通常路由到 EL1(Linux 内核)或 EL2(Hypervisor)。
  2. 安全世界的 IRQ:路由到 S-EL1(安全 OS)或 EL3(ATF)。
  3. FIQ:我建议全部路由到 EL3,由 ATF 统一分发。这样可以避免安全中断被非安全世界干扰。
  4. SMC:必须路由到 EL3。这是非安全世界调用安全世界的唯一通道。
  5. SError:通常路由到 EL3,由 ATF 记录错误并决定是否恢复。

异常向量表:CPU 的跳转地图

每个异常级别都有自己的异常向量表。向量表是一个 2KB 对齐的内存区域,里面放了 16 个入口,每个入口对应一种异常类型。

ARMv8-A 的向量表结构如下:

// 异常向量表布局(每个入口 128 字节)
// 偏移量 0x000: 同步异常 (Current EL with SP0)
// 偏移量 0x080: IRQ (Current EL with SP0)
// 偏移量 0x100: FIQ (Current EL with SP0)
// 偏移量 0x180: SError (Current EL with SP0)
// 偏移量 0x200: 同步异常 (Current EL with SPx)
// 偏移量 0x280: IRQ (Current EL with SPx)
// 偏移量 0x300: FIQ (Current EL with SPx)
// 偏移量 0x380: SError (Current EL with SPx)
// 偏移量 0x400: 同步异常 (Lower EL using AArch64)
// 偏移量 0x480: IRQ (Lower EL using AArch64)
// 偏移量 0x500: FIQ (Lower EL using AArch64)
// 偏移量 0x580: SError (Lower EL using AArch64)
// 偏移量 0x600: 同步异常 (Lower EL using AArch32)
// 偏移量 0x680: IRQ (Lower EL using AArch32)
// 偏移量 0x700: FIQ (Lower EL using AArch32)
// 偏移量 0x780: SError (Lower EL using AArch32)

ATF 在启动时会为 EL3 设置向量表,地址放在 VBAR_EL3 寄存器里。每个入口只有 128 字节,通常放一个跳转指令,跳到真正的处理函数。

避坑指南:向量表必须 2KB 对齐。我曾经因为链接脚本没对齐,导致异常跳到了错误的地方。调试器一看 PC 值,完全不在向量表范围内。嗯,那次排查花了我一整天。

异常处理流程:从触发到返回

一个完整的异常处理流程是这样的:

  1. 异常触发:CPU 检测到异常条件(比如 SMC 指令执行)。
  2. 保存上下文:CPU 自动保存 PSTATE 到 SPSR_ELx,保存返回地址到 ELR_ELx。
  3. 切换异常级别:CPU 根据路由配置,切换到目标 EL。
  4. 跳转向量表:CPU 从 VBAR_ELx + 偏移量 取出入口地址,开始执行处理代码。
  5. 处理异常:软件保存更多寄存器(比如通用寄存器),然后执行具体处理逻辑。
  6. 异常返回:执行 ERET 指令,CPU 从 SPSR_ELx 恢复 PSTATE,从 ELR_ELx 恢复返回地址,继续执行。

在 ATF 里,SMC 异常的处理流程特别重要。非安全世界发 SMC,CPU 自动跳到 EL3 的同步异常入口。ATF 解析 SMC 的功能 ID,然后调用对应的处理函数。处理完后再 ERET 回去。

核心要点:异常处理过程中,CPU 会自动屏蔽同级别或更低级别的中断。比如在 EL3 处理 SMC 时,除非你手动开中断,否则 IRQ 和 FIQ 都不会被响应。这是为了防止处理过程中被干扰。

SVG:ARMv8-A 异常模型全景图

下面这张图展示了异常级别、异常类型和路由关系。我建议你把它存下来,调试时对照着看。

ARMv8-A 异常模型全景图 ARMv8-A 异常模型全景图 EL3 - Secure Monitor (ATF) 处理 SMC、FIQ、SError;管理安全世界与非安全世界切换 VBAR_EL3 → 异常向量表 | SCR_EL3 → 路由控制 EL2 - Hypervisor (非安全) 处理 HVC、虚拟化中断;管理虚拟机 HCR_EL2 → 路由控制 | VTTBR_EL2 → 第二阶段地址翻译 EL1 - 非安全 OS (Linux) 处理 SVC、IRQ、Data Abort VBAR_EL1 → 向量表 | SPSR_EL1 → 状态保存 S-EL1 - 安全 OS (Trusted OS) 处理安全中断、安全服务 VBAR_EL1 (安全) | SCR_EL3 控制访问 EL0 - 非安全 App 用户态程序,无特权 通过 SVC 调用 OS 服务 S-EL0 - 安全 App 安全世界用户态,受限访问 通过 SMC 调用 ATF 服务 SMC → EL3 FIQ → EL3 (可配置) IRQ → EL1 说明 实线箭头表示默认路由路径,虚线箭头表示可配置路由路径。EL3 是异常路由的最终决策者。

实际项目中的异常配置建议

基于我多年的嵌入式安全经验,给你几条配置建议:

  • FIQ 全部路由到 EL3:让 ATF 统一管理所有快速中断。安全世界的定时器、加密引擎完成中断都走 FIQ。
  • SMC 必须路由到 EL3:这是安全调用的唯一入口。别让其他 EL 拦截 SMC。
  • SError 路由到 EL3:系统错误通常不可恢复,让 ATF 记录日志并决定是否重启。
  • IRQ 按需分配:非安全中断给 EL1/EL2,安全中断给 S-EL1 或 EL3。

重要提醒:异常路由配置必须在 EL3 完成。一旦配置错误,非安全世界可能劫持安全中断。我见过一个案例,攻击者利用配置错误的 FIQ 路由,伪造安全中断来触发 EL3 的漏洞。嗯,从那以后我每次做安全启动都会 triple-check SCR_EL3 的值。

ARMv8-A 的异常模型,说白了就是一套「谁有权处理什么事」的规则。EL3 是最高裁判,EL2 是虚拟化裁判,EL1 是操作系统裁判,EL0 只能乖乖听话。搞懂这套规则,你才能理解 ATF 为什么能成为安全世界的基石。