异常等级模型:EL0-EL3的划分、安全世界与非安全世界、异常路由机制

异常等级模型,说白了就是ARMv8/v9给软件划分的四个特权层级。我刚开始接触这个模型时,总觉得它跟操作系统的用户态/内核态差不多。后来踩了几个坑才明白——它远比想象中复杂,尤其是安全世界的引入,让整个模型变成了一个三维结构。

EL0-EL3:四个特权等级

ARMv9定义了四个异常等级,从EL0到EL3,数字越大特权越高。每个等级都有独立的虚拟内存配置、独立的异常向量表、独立的系统寄存器视图。

异常等级 典型用途 页表基数寄存器 SP选择
EL0 用户态应用程序 TTBR0_EL0 SP_EL0
EL1 操作系统内核 TTBR0_EL1 / TTBR1_EL1 SP_EL1
EL2 Hypervisor / 虚拟机监视器 TTBR0_EL2 SP_EL2
EL3 Secure Monitor / 安全固件 TTBR0_EL3 SP_EL3

这里有个关键点:EL0不能直接访问系统寄存器。你想想看,如果用户程序能随便改TTBR0,那内存隔离就形同虚设了。EL0只能通过SVC指令(系统调用)陷入EL1,或者通过其他异常机制进入更高等级。

核心原则:每个异常等级只能看到自己及更低等级的寄存器。EL2看不到EL3的SCR_EL3,EL1看不到EL2的HCR_EL2。这种隔离是硬件强制保证的。

安全世界与非安全世界:两个平行的宇宙

ARMv9把整个系统切成了两个世界——安全世界(Secure World)和非安全世界(Non-secure World)。每个世界都有自己的EL0-EL3,但EL3是共享的。

我做过一个TEE(可信执行环境)的项目,当时被问得最多的问题就是:「安全世界和非安全世界到底怎么切换?」答案是通过SMC指令(Secure Monitor Call)。

非安全世界的EL1或EL2执行SMC,会直接陷入EL3。EL3的Secure Monitor根据调用号决定是处理请求,还是切换到安全世界的EL1去执行。整个过程对非安全世界是透明的——它只知道SMC出去了,不知道里面发生了什么。

我的经验:调试安全世界的问题特别痛苦。非安全世界的JTAG通常被锁死,安全世界的调试接口需要芯片厂商提供。我曾经为了追一个安全内存的DMA问题,硬是看了三天的汇编代码。嗯,从那以后我写安全代码都格外小心。

异常路由机制:谁来处理这个异常?

异常来了,到底由哪个异常等级处理?ARMv9给了你一套灵活的路由规则。核心控制寄存器是SCR_EL3(Secure Configuration Register)和HCR_EL2(Hypervisor Configuration Register)。

举个例子,非安全世界的IRQ中断来了:

  1. 如果HCR_EL2.IMO == 1,IRQ被路由到EL2的Hypervisor处理
  2. 如果HCR_EL2.IMO == 0,IRQ被路由到EL1的OS处理
  3. 如果SCR_EL3.IRQ == 1,IRQ被路由到EL3的Secure Monitor处理

你看,路由优先级是EL3 > EL2 > EL1。硬件会先检查高等级的路由配置,如果高等级不拦截,才往下走。

避坑指南:我曾经在配置虚拟化环境时,把HCR_EL2的TGE位设成了1,结果所有EL0的异常都直接路由到了EL2,EL1的OS完全收不到任何异常。系统看起来在跑,但所有系统调用都超时。排查了两天才发现是TGE位的问题。

异常路由的完整流程

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

异常路由机制流程图 异常产生 当前异常等级 < 目标等级? 是 路由到目标异常等级 查找VBAR_ELx异常向量表 保存上下文 / 切换SP 执行异常处理程序 路由决策依据 • SCR_EL3 控制EL3路由 • HCR_EL2 控制EL2路由 • 异常类型(IRQ/FIQ/SError) 异常向量偏移 • 同步异常:0x000 • IRQ:0x080 • FIQ:0x100 / SError:0x180

异常等级切换的硬件自动行为

当异常从一个等级路由到另一个等级时,硬件会自动做几件事:

  • 保存返回地址到ELR_ELx(Exception Link Register)
  • 保存当前PSTATE到SPSR_ELx(Saved Program Status Register)
  • 切换栈指针到目标等级的SP_ELx
  • 禁用中断(根据异常类型和配置,DAIF标志位会被设置)

这里有个容易忽略的细节:ELR_ELx保存的是发生异常的指令地址,而不是下一条指令。对于同步异常(比如SVC),你需要从ELR_ELx拿到SVC指令,解析出立即数,然后手动把ELR_ELx加4才能正确返回。我见过有人直接拿ELR_ELx返回,结果同一个SVC被执行了两次。

安全世界与非安全世界的异常交互

两个世界之间的异常交互,是ARMv9虚拟化最复杂的部分之一。我整理了一个表格,方便你对照:

场景 触发方式 目标等级 典型用途
非安全EL1 → 安全EL1 SMC #0 EL3 → 安全EL1 TEE服务调用(如密钥管理)
非安全EL2 → 安全EL1 SMC #1 EL3 → 安全EL1 虚拟化环境中的安全服务
安全EL1 → 非安全EL1 ERET(异常返回) EL3 → 非安全EL1 TEE处理完成返回
非安全中断 → 安全世界 物理IRQ EL3(如果SCR_EL3.IRQ=1) 安全世界处理非安全中断

个人建议:如果你刚开始做安全世界开发,先别碰FIQ路由。FIQ在ARMv9里通常被用作安全中断,配置错了会导致安全世界和非安全世界的中断互相干扰。我一般先把IRQ调通,再考虑FIQ的优化。

异常返回:ERET指令的细节

异常处理完了怎么回去?用ERET指令。这条指令会:

  1. 从ELR_ELx恢复PC
  2. 从SPSR_ELx恢复PSTATE
  3. 自动降低异常等级(回到产生异常时的等级)

注意:ERET不能跨等级提升。你不能在EL1执行ERET然后跳到EL2——那是非法的。等级提升只能通过异常(SVC、HVC、SMC)或者系统寄存器写入来实现。

我曾经踩过的坑:在调试安全启动时,我在EL3用ERET想直接返回EL1,但忘了检查SPSR_EL3里的M[3:0]字段。结果M字段残留了EL3的值,ERET之后系统又回到了EL3,形成了一个死循环。排查时看PC一直在跳,但就是进不了OS。后来用JTAG单步才发现是SPSR没设对。

小结

异常等级模型是ARMv9虚拟化和安全的基础。EL0-EL3的划分提供了特权隔离,安全世界和非安全世界提供了数据隔离,异常路由机制则把这两者串联起来。理解这三个概念,你才能看懂后面的页表切换、虚拟机切换、以及安全内存管理。

嗯,下一块内容会更深入——我们会用实际的代码来配置异常向量表,看看EL3的Secure Monitor到底是怎么处理SMC的。