2、ARMv7异常等级模型:异常等级(EL0-EL3)介绍,安全世界与非安全世界,MMU与异常等级的关系
好,咱们正式开始聊ARMv7的异常等级模型。说实话,我刚接触ARMv7的时候,也被EL0到EL3这几个东西搞得有点晕。你想想看,一个CPU为什么要分这么多等级?直接像单片机那样跑裸机不香吗?
嗯,等你真正做过几个项目,踩过几次坑,就会明白——这套等级模型,其实是ARM为了兼顾安全和性能搞出来的精妙设计。今天我就把这块掰开揉碎了讲给你听。
2.1 异常等级(EL0-EL3)概述
ARMv7架构定义了四个异常等级,从EL0到EL3。数字越大,特权越高。我习惯把它们比作公司里的四个层级:
| 异常等级 | 特权级别 | 典型用途 | 我给的比喻 |
|---|---|---|---|
| EL0 | 最低(无特权) | 用户应用程序 | 普通员工,只能用自己的工位 |
| EL1 | 中等 | 操作系统内核 | 部门经理,能管整个部门的资源 |
| EL2 | 高 | Hypervisor(虚拟机监控器) | 总监,能管多个部门 |
| EL3 | 最高 | 安全监控器(Secure Monitor) | CEO,啥都能管 |
每个等级能访问的寄存器、能执行的指令都不一样。低等级想干高等级的事?门都没有,得通过异常(Exception)来“请求”高等级帮你做。
关键点:异常等级不是“运行模式”,而是“权限层级”。每个等级都有自己的栈指针(SP)、自己的页表基地址(TTBR)、自己的控制寄存器。切换等级时,硬件自动保存和恢复上下文。
2.2 安全世界与非安全世界
ARMv7还有一个更狠的设计——TrustZone。它把整个系统切成了两个世界:安全世界(Secure World)和非安全世界(Non-Secure World)。
我刚开始做TEE(可信执行环境)的时候,就被这个“两个世界”的概念绕进去过。说白了,你可以把CPU想象成有两套独立的硬件资源:
- 非安全世界:跑你的Android、Linux、RTOS。这部分代码永远不能访问安全世界的资源。
- 安全世界:跑指纹识别、支付、DRM等敏感应用。这部分可以访问非安全世界的内存(但通常不这么做)。
两个世界通过监控模式(Monitor Mode)切换,这个模式运行在EL3。切换指令是SMC(Secure Monitor Call)。
我曾经踩过的坑:在非安全世界的EL1里直接写安全世界的物理地址,以为MMU关了就能随便访问。结果CPU直接挂死。后来才明白,TrustZone的隔离是在总线层面做的,跟MMU开不开没关系。非安全世界的访问请求,在总线上就被过滤掉了。
2.3 异常等级与安全世界的组合矩阵
把异常等级和安全世界组合起来,就形成了一个4×2的矩阵。每个组合都有自己的页表、自己的异常向量表:
| 异常等级 | 非安全世界 | 安全世界 |
|---|---|---|
| EL0 | 普通App | 安全App(Trusted App) |
| EL1 | Linux/RTOS内核 | Trusted OS(如OP-TEE) |
| EL2 | Hypervisor | 安全Hypervisor(很少用) |
| EL3 | 安全监控器(唯一共享的等级) | |
你注意看,EL3是唯一一个“不分安全/非安全”的等级。为什么?因为EL3本身就是安全世界的最高权限,它负责在两个世界之间切换。
2.4 MMU与异常等级的关系
好,现在聊重点——MMU跟这些等级到底什么关系?
每个异常等级都有自己的页表基地址寄存器(TTBR0/TTBR1)。也就是说,EL0、EL1、EL2、EL3各自维护一套独立的页表。切换等级时,MMU自动切换到对应的页表。
我个人习惯把MMU的配置分成三层来看:
- 页表本身:每个等级各自维护,互不干扰。
- 地址翻译规则:由系统控制寄存器(SCTLR)的M位控制。每个等级可以独立开关MMU。
- 内存属性:包括Cache策略、共享属性、执行权限等。这些属性在页表条目里定义,不同等级可以不同。
一个小技巧:我在调试MMU问题时,经常先检查当前在哪个异常等级。用MRS x0, CurrentEL读一下CurrentEL寄存器,就能知道当前特权级别。如果发现页表没生效,八成是配错了等级的页表。
2.5 异常等级切换时的MMU行为
异常等级切换时,MMU的行为很关键。我画了一张图帮你理解:
切换等级时,硬件会做这几件事:
- 保存当前等级的TTBR0/TTBR1到系统寄存器
- 加载目标等级的页表基地址
- 根据异常类型决定是否刷TLB(Translation Lookaside Buffer)
注意:从EL1切到EL0时,MMU不会刷TLB。因为EL0的页表是EL1页表的一部分(EL0使用TTBR0,EL1使用TTBR1)。但如果你改了EL0的页表,记得手动刷TLB,否则旧映射还在。
2.6 实战中的配置建议
说了这么多理论,来点实际的。我在做嵌入式系统时,通常这样配置MMU和异常等级:
| 场景 | EL配置 | MMU配置 | 我的建议 |
|---|---|---|---|
| 裸机系统 | EL1或EL3 | 简单页表,1:1映射 | 用EL1就够了,EL3留给安全监控器 |
| Linux + TEE | EL1(非安全) + EL3(安全) | Linux用2级页表,TEE用1级页表 | 注意安全世界和非安全世界的页表不要重叠 |
| 虚拟化场景 | EL2 + EL1 + EL0 | EL2管理物理地址,EL1管理虚拟地址 | EL2的页表要小心配置,错了整个系统崩 |
我的个人习惯:调试阶段,我会把所有等级的MMU都打开,但把页表设成1:1映射(虚拟地址=物理地址)。这样既能享受MMU的权限保护,又不用处理复杂的地址翻译问题。等系统跑稳了,再优化页表。