3. ARM TrustZone 深度解析:安全世界与普通世界的切换、Monitor模式、安全中断处理
TrustZone 这东西,说白了就是给 CPU 装了俩“脑子”。一个管日常应用,叫普通世界(Normal World);另一个管密钥、指纹这些敏感数据,叫安全世界(Secure World)。我最早接触 TrustZone 是在做一款金融支付芯片的方案,那时候客户要求私钥必须硬件隔离,不能有任何软件能直接读到。嗯,TrustZone 正好干这个。
但光有俩世界还不够——怎么切?谁来切?中断来了怎么办?这就要聊到 Monitor 模式和中断处理了。
3.1 两个世界的硬件隔离基础
ARM 从 Cortex-A 系列开始引入 TrustZone 扩展。它在硬件层面把总线、内存、Cache 都分成了安全和非安全两套视图。你想想看,一个物理 CPU 核心,却能跑两套完全隔离的操作系统——一边是 Linux/Android,另一边是可信执行环境(TEE)。
关键硬件机制就这几个:
- NS 位(Non-Secure bit):每个总线事务都带一个 NS 标记,决定访问的是安全内存还是非安全内存。
- TZASC(TrustZone Address Space Controller):内存控制器,把物理内存分区,安全区只有安全世界能读写。
- TZPC(TrustZone Protection Controller):外设保护,控制哪些外设属于安全世界。
核心原则:普通世界永远无法直接访问安全世界的任何资源。这是硬件强制隔离,不是软件能绕过的。
我在一个项目中遇到过,有人想通过 DMA 绕过 TrustZone 读安全内存——结果 TZASC 直接返回全 F,数据根本出不来。硬件层面的隔离,就是这么硬。
3.2 世界切换:从 SMC 指令到 Monitor 模式
两个世界怎么切换?靠一条指令:SMC(Secure Monitor Call)。
执行 SMC 后,CPU 会进入一个特殊的模式——Monitor 模式。这个模式不属于普通世界,也不属于安全世界,它是两个世界之间的“海关”。
切换流程大致是这样:
- 普通世界调用 SMC,传入请求 ID 和参数。
- CPU 硬件自动保存当前世界状态,跳转到 Monitor 向量表。
- Monitor 代码检查请求是否合法,然后切换到安全世界。
- 安全世界处理完,再通过 SMC 或异常返回切回来。
个人习惯:我写 Monitor 代码时,一定会把保存的上下文放在安全内存里,并且加密存储。为什么?因为 Monitor 模式虽然特权高,但代码量少,一旦被攻击,整个隔离就破了。
来看一段简化的 Monitor 切换代码(伪汇编):
// 普通世界调用 SMC 进入 Monitor
SMC #0
// Monitor 向量表入口
monitor_vector:
// 保存普通世界上下文
STR r0, [sp, #0x00]
STR r1, [sp, #0x04]
STR lr, [sp, #0x08]
// 切换到安全世界页表
LDR r0, =secure_page_table
MCR p15, 0, r0, c2, c0, 0
// 跳转到安全世界入口
MOV pc, #secure_entry
这段代码看着简单,但实际项目里要考虑的事情多得多——比如中断嵌套、缓存一致性、TLB 刷新。我曾经因为忘了刷 TLB,导致安全世界读到的还是普通世界的脏数据,排查了两天才找到问题。
3.3 Monitor 模式:两个世界的守门人
Monitor 模式是 ARM 架构里最特殊的模式。它有自己的栈、自己的向量表、自己的页表。普通世界和安全世界都管不到它。
Monitor 模式主要干这几件事:
| 职责 | 说明 |
|---|---|
| 上下文保存与恢复 | 切换时保存所有寄存器、状态、页表 |
| 安全检查 | 验证 SMC 请求是否合法,防止恶意调用 |
| 中断路由 | 决定安全中断还是非安全中断,分别投递 |
| 电源管理 | 处理休眠、唤醒时的世界状态 |
避坑指南:Monitor 代码必须放在安全内存里,并且只读。我曾经见过一个方案把 Monitor 放在非安全内存里——结果普通世界直接修改了 Monitor 代码,整个 TrustZone 形同虚设。
Monitor 模式还有一个关键点:它处理的是 FIQ(快速中断),而不是 IRQ。为什么?因为 FIQ 有独立的寄存器组,不需要压栈,切换更快。安全世界的中断必须走 FIQ,这样普通世界的 IRQ 不会干扰安全操作。
3.4 安全中断处理:FIQ 与 IRQ 的分工
中断处理是 TrustZone 里最容易出问题的地方。你想想看,如果普通世界正在刷网页,突然一个安全中断来了——比如指纹传感器数据到了——CPU 必须立刻切到安全世界去处理,处理完再回来继续刷网页。
ARM 的解决方案是:
- FIQ:分配给安全世界。安全中断来了,直接触发 FIQ,CPU 进入 Monitor 模式,再切到安全世界。
- IRQ:分配给普通世界。普通中断走 IRQ,不影响安全世界。
中断路由的硬件逻辑是这样的:
// 中断控制器(GIC)配置示例
// 设置中断 ID 32 为安全中断(FIQ)
GICD_IGROUPR[0] |= (1 << 0); // Group 0 = Secure
GICD_ISENABLER[0] |= (1 << 0); // Enable
// 设置中断 ID 33 为非安全中断(IRQ)
GICD_IGROUPR[1] &= ~(1 << 1); // Group 1 = Non-Secure
GICD_ISENABLER[1] |= (1 << 1); // Enable
这里有个细节:GIC(通用中断控制器)必须支持 TrustZone 扩展,才能区分安全中断和非安全中断。老版本的 GIC 不行,得用 GICv2 以上。
关键点:安全中断必须比普通中断优先级高。否则普通世界关中断时,安全中断进不来,指纹数据就丢了。
我在做一款移动支付设备时,遇到过一个问题:安全中断来了,但 Monitor 模式切不过去。查了半天,发现是 GIC 配置错了——安全中断被配成了 IRQ,结果被普通世界的 Linux 内核给屏蔽了。嗯,从那以后我每次配中断都要反复确认 Group 号。
3.5 完整切换流程与状态机
把上面这些串起来,一次完整的世界切换流程是这样的:
- 普通世界执行 SMC 指令。
- CPU 进入 Monitor 模式,保存普通世界上下文。
- Monitor 检查 SMC 功能号,确认合法。
- 切换到安全世界页表,刷新 TLB。
- 设置 CPU 为安全状态,跳转到安全世界入口。
- 安全世界处理请求,可能访问密钥、做加解密。
- 安全世界执行 SMC 返回,或触发安全中断返回。
- Monitor 恢复普通世界上下文,切回非安全状态。
下面这张图把整个流程和状态机画清楚了:
3.6 实战中的几个关键决策点
做 TrustZone 开发,有几个地方我建议你特别留意:
- Monitor 代码大小:越小越好。我一般控制在 2KB 以内,只做切换和检查,复杂逻辑放安全世界。
- 中断延迟:安全中断处理必须在几十微秒内完成,否则普通世界会卡死。我在项目中用 FIQ 处理指纹数据,从中断到处理完成不超过 10 微秒。
- 调试难度:TrustZone 调试很痛苦。普通世界的 JTAG 看不到安全世界。我建议在开发阶段用硬件仿真器,支持 TrustZone 调试的那种。
个人经验:如果你刚开始做 TrustZone,先别急着写复杂逻辑。先搭一个最小切换框架——SMC 能切过去、能切回来、FIQ 能正确处理——然后再往上加功能。这个基础打牢了,后面就顺了。
TrustZone 的深度解析就到这里。记住一句话:安全世界和普通世界的切换,本质上是硬件强制的一次上下文切换,Monitor 模式是那个永远可信的守门人。理解了这一点,后面的密钥隔离架构就好办了。