3. TEE安全架构:安全世界与普通世界、Monitor模式、SMC指令、安全中断处理流程
这一章,我们来聊聊TEE最核心的骨架——安全世界与普通世界的划分。说白了,就是一块芯片上怎么同时跑两个“世界”,还能保证它们互不干扰。
我刚开始接触TEE时,总觉得这东西很玄乎。后来亲手调过几次Monitor模式的切换流程,才真正理解它的精妙之处。嗯,咱们一步步来。
3.1 两个世界:普通世界与安全世界
ARM从Cortex-A系列开始,引入了“安全扩展”(TrustZone)。硬件上,它把CPU的执行状态分成了两个世界:
- 普通世界(Normal World):跑你的Android、Linux、RTOS。所有非安全应用都在这里。
- 安全世界(Secure World):跑TEE OS(如OP-TEE、Trusty)和安全应用。这里的数据和代码,普通世界碰不到。
你想想看,这两个世界共享同一个物理CPU核心,但硬件上做了隔离。怎么做到的?核心秘密在于——总线上的“安全位”。
核心概念:ARM TrustZone在系统总线上增加了一个额外的控制信号——NS位(Non-Secure)。当NS=0时,表示当前访问来自安全世界;NS=1时,来自普通世界。内存控制器、中断控制器、DMA等外设都会检查这个位,决定是否允许访问。
我在项目中遇到过一个问题:某个外设的寄存器在普通世界也能读写,导致安全数据泄露。查了半天,原来是硬件设计时忘了给这个外设加上NS位过滤。所以,硬件隔离是根基,软件只是辅助。
3.2 Monitor模式:两个世界的“守门人”
两个世界不能直接切换,必须经过一个特殊的模式——Monitor模式。它就像两个国家之间的海关,所有出入境都得从这里过。
Monitor模式是ARM架构中特权最高的模式,比SVC(超级用户模式)还高。它运行在安全世界中,但拥有独立的栈和异常向量表。
| 模式 | 特权级别 | 所属世界 | 主要用途 |
|---|---|---|---|
| Monitor | 最高(PL3) | 安全世界 | 世界切换、安全中断处理 |
| 安全世界内核 | 高(PL1) | 安全世界 | TEE OS运行 |
| 普通世界内核 | 高(PL1) | 普通世界 | Rich OS运行 |
| 用户态 | 低(PL0) | 各自世界 | 普通应用/安全应用 |
这张表我建议你记牢。很多安全漏洞,本质上就是“不该进Monitor模式的时候进去了”,或者“该进的时候没进去”。
3.3 SMC指令:世界切换的“开关”
普通世界想调用安全世界的服务,怎么办?不能直接跳转,必须通过一条特殊的指令——SMC(Secure Monitor Call)。
SMC指令的执行流程:
- 普通世界执行
SMC #0指令。 - CPU硬件自动切换到Monitor模式。
- Monitor模式的异常向量表被触发,执行对应的处理函数。
- 处理函数解析SMC参数,决定是切换到安全世界,还是直接返回。
- 完成后,通过
ERET指令返回普通世界。
代码层面,SMC调用通常长这样:
// 普通世界(Linux内核中调用TEE驱动)
struct smc_param {
unsigned long a0; // 功能ID
unsigned long a1; // 参数1
unsigned long a2; // 参数2
unsigned long a3; // 参数3
};
// 执行SMC调用
static inline void tee_smc_call(struct smc_param *param)
{
asm volatile(
"ldr x0, %[a0]\n"
"ldr x1, %[a1]\n"
"ldr x2, %[a2]\n"
"ldr x3, %[a3]\n"
"smc #0\n" // 触发世界切换
"str x0, %[a0]\n" // 保存返回值
: [a0] "=m" (param->a0)
: [a1] "m" (param->a1),
[a2] "m" (param->a2),
[a3] "m" (param->a3)
: "x0", "x1", "x2", "x3", "memory"
);
}
个人经验:我曾经在调试一个SMC调用时,发现普通世界传进来的参数在Monitor模式里读出来全是乱的。排查了半天,原来是普通世界的缓存(Cache)没有刷干净。记住:世界切换前,一定要做Cache和TLB的清理操作,否则你会看到“幽灵数据”。
3.4 安全中断处理流程
中断处理在TEE里是个大坑。普通世界的中断和安全世界的中断,处理方式完全不同。
ARM GIC(通用中断控制器)支持两种中断配置:
- NS中断(普通中断):只能由普通世界处理。
- S中断(安全中断):只能由安全世界处理。
当一个安全中断到来时,如果CPU当前在普通世界运行,会发生什么?
- GIC检测到S中断,直接触发IRQ或FIQ(取决于配置)。
- CPU硬件自动切换到Monitor模式。
- Monitor模式的FIQ/IRQ处理函数判断:这是安全中断,不能交给普通世界。
- Monitor模式保存普通世界上下文,切换到安全世界。
- 安全世界处理完中断后,通过Monitor模式返回普通世界。
这个流程里,最容易被忽视的是——中断嵌套。安全世界在处理中断时,普通世界的中断又来了,怎么办?
避坑指南:我曾经在一个项目里,安全世界的中断处理函数里调用了SMC返回普通世界,结果普通世界的中断又触发了Monitor模式,形成了递归调用,直接栈溢出。所以,安全中断处理函数里,绝对不要主动发起世界切换。如果需要,请使用异步通知机制。
3.5 整体架构图
下面这张图,是我自己画的一个简化版TEE安全架构。它把普通世界、安全世界、Monitor模式、SMC指令和中断处理串在了一起。
这张图里,我特意把安全中断的路径画成了红色虚线。为什么?因为这是最容易出问题的地方。普通世界正在跑一个关键任务,突然被安全中断抢走CPU,如果Monitor模式没有妥善保存上下文,普通世界回来时就会崩溃。
3.6 实践中的几个关键点
最后,我总结几个在实际项目中容易踩的坑:
- Cache一致性:世界切换前,必须刷Cache。否则安全世界读到的是脏数据。
- 中断优先级:安全中断的优先级必须高于普通中断。否则普通世界可以“饿死”安全世界。
- 栈隔离:Monitor模式要有独立的栈,不能和普通世界或安全世界共用。否则栈溢出会直接导致安全崩溃。
- SMC参数校验:Monitor模式收到SMC调用后,必须校验参数合法性。我曾经见过一个漏洞,普通世界传了一个非法地址,导致Monitor模式直接写坏了安全世界的内存。
我的习惯:每次写完Monitor模式的代码,我都会做一次“世界切换压力测试”——让普通世界以最高频率发起SMC调用,同时注入安全中断,看系统能不能稳定运行24小时。很多隐藏的竞态条件,都是这么测出来的。
好了,这一章的内容就到这里。TEE的安全架构,说白了就是“两个世界 + 一个守门人”。理解了这个模型,后面的接口适配和安全部署,你就能看得更透彻。