3. 系统总线与内存隔离:TZASC 控制器、TZMA 控制器、安全内存区域划分
好,我们进入第三章。前面两章我们把 TrustZone 的宏观架构和异常模型聊透了。这一章,咱们得落地到具体硬件——系统总线和内存隔离。
说白了,TrustZone 的「隔离」不能只靠 CPU 自己喊口号。你得让总线、内存控制器都知道:现在是非安全世界在访问,还是安全世界在访问。否则,一个非安全世界的恶意程序,直接 DMA 读你的安全内存,那还隔离个啥?
所以,ARM 设计了两个关键的硬件模块:TZASC 和 TZMA。我当年第一次接触它们时,也花了些时间才理清各自的分工。今天咱们一次讲透。
3.1 为什么需要总线级隔离?
先想一个问题:CPU 执行在安全世界,访问安全内存,这没问题。但系统里不只有 CPU 啊——DMA 控制器、GPU、USB 主机控制器,这些总线上的「主设备」也能发起内存访问。
如果它们不受 TrustZone 约束,那安全内存就形同虚设。我曾在项目中见过一个案例:某个 DMA 通道配置错误,直接把安全内存里的密钥 dump 到了非安全缓冲区。嗯,从那以后,我对总线隔离的敬畏心就拉满了。
总线级隔离的核心目标就两个:
- 控制谁(主设备)能访问哪块内存
- 控制访问的权限(读/写/安全/非安全)
TZASC 和 TZMA 就是干这个的。它们俩分工不同,咱们一个一个说。
3.2 TZASC 控制器:细粒度的内存防火墙
TZASC,全称 TrustZone Address Space Controller。你可以把它想象成一个「内存防火墙」——它挂在系统总线上,监控所有对 DDR 的访问请求。
我个人习惯把 TZASC 比作一个带安检的收费站。每个访问请求过来,TZASC 先查它的 NS(Non-Secure)位:
- 如果 NS=0(安全访问),放行,随便过。
- 如果 NS=1(非安全访问),那就得看它要访问的地址落在哪个区域了。
TZASC 把 DDR 划分成若干个区域(Region),每个区域可以独立配置:
- 起始地址和大小:区域边界
- 安全属性:安全、非安全、或「非安全可读但不可写」
- 子区域权限:更细粒度的控制
来看一个典型的配置示例:
// 伪代码:TZASC 区域配置
TZASC_REGION_ATTRIBUTES(0) = {
.base = 0x80000000, // 起始地址
.size = 0x01000000, // 16MB
.sec_attr = SECURE, // 安全区域
.subregion_disable = 0 // 所有子区域使能
};
TZASC_REGION_ATTRIBUTES(1) = {
.base = 0x81000000,
.size = 0x02000000, // 32MB
.sec_attr = NON_SECURE, // 非安全区域
.subregion_disable = 0
};
TZASC_REGION_ATTRIBUTES(2) = {
.base = 0x83000000,
.size = 0x00100000, // 1MB
.sec_attr = SECURE_READ_ONLY, // 安全只读
.subregion_disable = 0
};
关键点:TZASC 的配置必须在安全世界进行,且一旦锁定(Lock),直到下次复位才能修改。这是为了防止非安全世界篡改隔离策略。
TZASC 的粒度可以做到很细。以 ARM 的 TZC-400 为例,它支持最多 9 个区域,每个区域可以小到 4KB。你想想看,这对于需要精细隔离的场景——比如把安全密钥区、安全堆栈区、非安全应用区都放在同一个 DDR 里——非常有用。
3.3 TZMA 控制器:简单粗暴的静态隔离
TZMA,全称 TrustZone Memory Adapter。它和 TZASC 最大的区别在于:TZMA 管的是片上 SRAM,不是 DDR。
而且 TZMA 的隔离方式更「粗暴」——它只支持把 SRAM 分成两个连续的区域:安全区和非安全区。中间就一个分界线(Split Point),没有 TZASC 那么多花哨的区域配置。
为什么需要 TZMA?因为很多 SoC 内部有紧耦合的 SRAM(比如 Cortex-M 系列的 TCM,或者 A 系列内部的 L2 缓存后面的 SRAM)。这些 SRAM 访问延迟低,适合放关键数据——比如安全监控的栈、中断向量表。
我建议你在设计安全启动流程时,把早期启动代码和关键安全状态放在 TZMA 保护的安全 SRAM 里。这样即使 DDR 还没初始化,安全世界也能有个「安全窝」。
来看 TZMA 的配置方式:
// 伪代码:TZMA 分界线配置
// 假设 SRAM 总大小 256KB,起始地址 0x2E000000
TZMA_SPLIT = 0x2E010000; // 分界线设在 64KB 处
// 0x2E000000 - 0x2E00FFFF : 安全区域 (64KB)
// 0x2E010000 - 0x2E03FFFF : 非安全区域 (192KB)
个人经验:TZMA 的分界线一旦设定,通常只能在安全启动阶段修改。我习惯把安全 SRAM 区域留得稍微大一点——比如 256KB 的 SRAM,我分 96KB 给安全区。因为后面调试时,你永远不知道安全监控需要多少临时缓冲区。
3.4 TZASC vs TZMA:对比与选型
这两个控制器经常被放在一起比较。我整理了一张表,方便你对照:
| 特性 | TZASC | TZMA |
|---|---|---|
| 管控对象 | DDR / 外部内存 | 片上 SRAM |
| 区域数量 | 多个(通常 8-9 个) | 2 个(安全/非安全) |
| 粒度 | 可小到 4KB | 取决于分界线位置 |
| 配置灵活性 | 高(每个区域独立属性) | 低(仅分界线) |
| 典型用途 | 安全内存池、密钥存储、安全缓冲区 | 安全栈、中断向量、启动代码 |
| 硬件复杂度 | 较高(需要区域比较逻辑) | 低(仅地址比较) |
实际项目中,两者通常是配合使用的。TZMA 保护最核心的片上数据,TZASC 保护 DDR 中的安全世界内存。缺一不可。
3.5 安全内存区域划分实战
理论讲完了,咱们来点实际的。假设你正在设计一个基于 ARM Cortex-A72 的 SoC,DDR 地址范围 0x80000000 - 0x8FFFFFFF(256MB),片上 SRAM 地址范围 0x2E000000 - 0x2E03FFFF(256KB)。
你会怎么划分安全内存区域?
我个人习惯按这个思路来:
- 先定安全世界需要什么:安全监控代码、安全堆栈、密钥存储、安全缓冲区(用于加解密)。
- 再定非安全世界需要什么:普通应用、Linux 内核、用户数据。
- 最后定共享区域:安全世界和非安全世界需要交换数据的地方。
一个典型的划分方案如下:
DDR 内存布局 (256MB):
+---------------------------+ 0x8FFFFFFF
| 非安全区域 (192MB) | 非安全世界:Linux + 应用
+---------------------------+ 0x8C000000
| 共享缓冲区 (4MB) | 安全/非安全共享:消息队列
+---------------------------+ 0x8BC00000
| 安全缓冲区 (8MB) | 安全世界:加解密临时数据
+---------------------------+ 0x8B400000
| 密钥存储区 (1MB) | 安全世界:密钥、证书
+---------------------------+ 0x8B300000
| 安全监控代码 (3MB) | 安全世界:OP-TEE / Trusted Firmware
+---------------------------+ 0x8B000000
| 保留区域 (11MB) | 启动固件、ATF
+---------------------------+ 0x80000000
片上 SRAM 布局 (256KB):
+---------------------------+ 0x2E03FFFF
| 非安全 SRAM (160KB) | 非安全世界:快速数据
+---------------------------+ 0x2E018000
| 安全 SRAM (96KB) | 安全世界:栈、中断向量
+---------------------------+ 0x2E000000
注意:共享缓冲区必须同时配置 TZASC 区域属性为「安全可读写,非安全可读写」。但你要小心——非安全世界只能通过安全世界定义的接口访问共享缓冲区,不能直接裸写。否则共享缓冲区就成了攻击入口。
3.6 系统总线隔离的整体视图
为了让你更直观地理解整个隔离体系,我画了一张结构图。它展示了 CPU、总线、TZASC、TZMA 和内存之间的交互关系。
从这张图你能看到:CPU、DMA、GPU 等所有主设备发出的访问请求,都经过系统总线,然后被 TZASC 或 TZMA 拦截检查。只有符合安全策略的请求才能到达 DDR 或 SRAM。
嗯,这里要注意:TZASC 和 TZMA 是串在总线上的,不是旁路监听。这意味着它们能真正阻止非法访问,而不是事后告警。
3.7 避坑指南:我踩过的几个坑
最后,分享几个我在实际项目中遇到的教训:
- 坑一:TZASC 区域重叠。如果你配置的两个区域有地址重叠,TZASC 的行为是未定义的。我见过一个项目,因为区域 0 和区域 1 的边界算错了 1 个字节,导致安全内存被非安全世界读走。从那以后,我每次配 TZASC 都会画一张完整的内存地图,逐字节核对。
- 坑二:忘记配置 TZMA 分界线。有些 SoC 的 TZMA 默认分界线在 0(全部安全),但如果你不显式配置,非安全世界可能无法访问 SRAM。我曾经调试一个启动问题,花了半天才发现是 TZMA 没配。
- 坑三:共享缓冲区的权限过松。共享缓冲区如果配成「非安全可读写」,非安全世界的 bug 可能直接破坏共享数据。我建议至少配成「非安全只读」,或者通过安全监控做消息代理。