2、核心概念拆解:安全世界(Secure World)与普通世界(Normal World)的边界在哪里?
好,咱们正式开始聊 TrustZone 最核心的两个世界。
很多初学者第一次接触 TrustZone,都会问同一个问题:“安全世界和普通世界,到底是怎么隔开的?”
有人觉得是硬件加了个开关,有人以为是软件层面的权限控制。其实都不完全对。我当年刚入行时也迷糊过一阵子,直到亲手调过一个安全中断的坑,才真正理解这条“边界”到底长什么样。
今天咱们就把这条边界彻底拆开,看看它到底画在哪儿。
2.1 两个世界,一个物理核
先记住一句话:TrustZone 不是两个独立的 CPU,而是一个 CPU 的两种状态。
ARM 在 Cortex-A 系列处理器里引入了一个新的处理器模式——Monitor 模式,以及一个硬件信号——NS(Non-Secure)位。这个 NS 位,就是两个世界的分界线。
- 当 NS = 0,处理器处于 安全世界(Secure World)
- 当 NS = 1,处理器处于 普通世界(Normal World)
说白了,就是一根信号线的电平高低,决定了整个系统的“阵营”。
核心要点:安全世界和普通世界共享同一个物理 CPU 核心,但拥有各自独立的虚拟地址空间、系统寄存器视图、中断控制器配置和内存访问权限。
你想想看,这意味着什么?
意味着你在普通世界里跑 Linux,内核看到的“全部内存”其实只是安全世界允许你看到的那一部分。安全世界自己还有一块“隐形”的内存区域,普通世界根本不知道它的存在。
2.2 边界到底画在哪里?—— 四层隔离
这条边界不是一条线,而是四层隔离叠加的结果。我习惯把它画成一张图:
这张图我建议你存下来。每次调试 TrustZone 问题时,先问自己:“我现在卡在哪一层?”
2.3 第一层:硬件层 —— NS 位是“总开关”
ARM 在 AMBA 总线协议里增加了一个信号:AxPROT[1],也就是我们常说的 NS 位。
这个信号会随着每一次总线访问(读/写)一起发送给内存控制器和外设总线桥。接收端根据这个信号决定:“这个访问我该不该响应?”
个人经验:我在调试某款 SoC 时,遇到过安全世界写寄存器写不进去的情况。查了两天才发现,是 AXI 总线桥的 NS 位配置反了。硬件设计时把安全外设的地址段配成了 Non-Secure,结果安全世界访问时总线直接返回 DECERR。嗯,从那以后我每次看 datasheet 都会先确认外设的“安全属性”配置。
2.4 第二层:系统软件层 —— Monitor 是“守门人”
两个世界之间不能直接跳转。必须通过 Monitor 模式。
触发方式有两种:
- SMC 指令:软件主动发起世界切换
- 安全中断(FIQ):硬件事件触发切换
Monitor 模式下的代码(通常是 ATF/OP-TEE 的底层部分)负责:
- 保存当前世界的上下文(寄存器、状态)
- 切换 NS 位
- 恢复目标世界的上下文
- 跳转到目标世界的入口
// 伪代码:Monitor 模式下的世界切换
void monitor_smc_handler(uint32_t smc_fid) {
// 1. 保存当前世界上下文
save_context(current_world);
// 2. 切换 NS 位
if (current_world == SECURE) {
set_ns_bit(1); // 切到普通世界
} else {
set_ns_bit(0); // 切到安全世界
}
// 3. 恢复目标世界上下文
restore_context(target_world);
// 4. 返回(自动进入目标世界)
eret();
}
注意:Monitor 模式本身是“安全”的。也就是说,普通世界无法直接修改 Monitor 模式的代码或数据。这是硬件强制保证的——Monitor 模式下的 MMU 配置和异常向量表,普通世界根本看不见。
2.5 第三层:内存隔离层 —— 物理地址空间的“分界线”
这是最容易被误解的一层。
很多人以为安全世界和普通世界用的是不同的物理内存。其实不是。它们共享同一片 DRAM,只是通过 TZASC(TrustZone Address Space Controller) 把物理地址空间划分成了不同的区域。
| 内存区域 | 安全世界访问 | 普通世界访问 | 典型用途 |
|---|---|---|---|
| 安全内存 | 可读可写 | 访问被硬件拒绝 | 安全 OS、密钥、TEE 堆栈 |
| 非安全内存 | 可读可写 | 可读可写 | Linux、Android、普通应用 |
| 共享内存 | 可读可写 | 可读可写 | 世界间通信(SMC 数据) |
关键点在于:安全世界可以访问非安全内存,但普通世界绝对不能访问安全内存。
这个限制不是软件加的,是 TZASC 在硬件层面直接拒绝的。普通世界发出一个指向安全内存的读请求,总线直接返回错误,连 CPU 都收不到数据。
避坑指南:我曾经遇到一个 case,安全 OS 启动时把共享内存的地址配错了,写到了安全内存区域。结果普通世界通过 SMC 请求数据时,安全 OS 返回的指针指向了安全内存,普通世界一读就挂。调试了整整一个下午才发现是地址配置问题。所以,共享内存的物理地址必须在 TZASC 里配成“安全可读+非安全可读”,缺一不可。
2.6 第四层:外设隔离层 —— 每个外设都有自己的“安全属性”
内存隔离完了,外设呢?
ARM 的做法是:每个外设(UART、GPIO、I2C、SPI 等)在系统设计时就被分配了一个安全属性。
这个属性通过 AXI-to-APB 桥 或者 TZPC(TrustZone Protection Controller) 来配置。
- 安全外设:只有安全世界能访问。普通世界访问会触发总线错误。
- 非安全外设:两个世界都能访问。
- 可配置外设:通过 TZPC 动态切换安全属性。
举个例子:
我在一个项目中,把 UART0 配成了安全外设,用于安全 OS 的日志输出。UART1 配成了非安全外设,给 Linux 用。两个世界各自用各自的串口,互不干扰。
但要注意:如果一个外设被配成安全外设,普通世界的中断控制器(GIC)也收不到它的中断信号。 因为 GIC 本身也受 NS 位控制。
2.7 边界不是“墙”,而是“门”
讲到这里,你应该明白了:
安全世界和普通世界的边界,不是一堵密不透风的墙。它更像是一道有门禁的门。
- 门禁系统 = 硬件 NS 位 + TZASC + TZPC
- 守门人 = Monitor 模式代码
- 通行证 = SMC 指令 + 安全中断
普通世界想进入安全世界,必须走 SMC 这道门。安全世界想访问普通世界的数据,也得通过共享内存这个“窗口”。
我个人的习惯是:在设计阶段就把“门”的位置画清楚。 哪些内存是共享的?哪些外设是安全的?SMC 调用号怎么分配?这些在写第一行代码之前就应该定下来。
否则,等到联调时发现安全 OS 和 Linux 互相踩内存,那可就真得加班了。