异常等级模型: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中断来了:
- 如果HCR_EL2.IMO == 1,IRQ被路由到EL2的Hypervisor处理
- 如果HCR_EL2.IMO == 0,IRQ被路由到EL1的OS处理
- 如果SCR_EL3.IRQ == 1,IRQ被路由到EL3的Secure Monitor处理
你看,路由优先级是EL3 > EL2 > EL1。硬件会先检查高等级的路由配置,如果高等级不拦截,才往下走。
避坑指南:我曾经在配置虚拟化环境时,把HCR_EL2的TGE位设成了1,结果所有EL0的异常都直接路由到了EL2,EL1的OS完全收不到任何异常。系统看起来在跑,但所有系统调用都超时。排查了两天才发现是TGE位的问题。
异常路由的完整流程
下面这张图展示了异常从产生到处理的完整路径:
异常等级切换的硬件自动行为
当异常从一个等级路由到另一个等级时,硬件会自动做几件事:
- 保存返回地址到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指令。这条指令会:
- 从ELR_ELx恢复PC
- 从SPSR_ELx恢复PSTATE
- 自动降低异常等级(回到产生异常时的等级)
注意: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的。