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)。

你会怎么划分安全内存区域?

我个人习惯按这个思路来:

  1. 先定安全世界需要什么:安全监控代码、安全堆栈、密钥存储、安全缓冲区(用于加解密)。
  2. 再定非安全世界需要什么:普通应用、Linux 内核、用户数据。
  3. 最后定共享区域:安全世界和非安全世界需要交换数据的地方。

一个典型的划分方案如下:

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 Core 安全/非安全 系统总线 AXI / AHB DMA 控制器 GPU TZASC DDR 防火墙 TZMA SRAM 隔离 DDR 安全区域 非安全区域 片上 SRAM 安全/非安全 说明 所有总线主设备 的访问都经过 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 可能直接破坏共享数据。我建议至少配成「非安全只读」,或者通过安全监控做消息代理。