ARM TrustZone

本文从 TrustZone 的设计动机出发,逐层讲清楚两个世界是如何隔离的、世界切换是怎么工作的、这套硬件机制为什么能保证 Normal world 绝对碰不到 Secure 资源,以及作为 Normal world 的程序员——即使你一行安全代码都不写——你写的代码行为会受到哪些影响。

1. 为什么需要 TrustZone

传统操作系统的安全模型建立在软件隔离之上:用虚拟内存隔离进程、用特权级隔离内核与用户态。这套模型的前提是内核本身是可信的

问题在于,现代移动平台的开放性直接挑战了这个前提。用户可以安装任意来源的应用,系统支持加载第三方内核模块,有的甚至允许运行多套虚拟机。一旦恶意代码通过内核漏洞获取了最高特权级,整个系统的软件防线就全部崩塌——所有进程的内存、文件系统的加密密钥、用户的支付密码,一切暴露无遗。

这说明了一个结论:软件级别的隔离无法抵御内核级别的攻击。换句话说,安全边界必须下沉到一个即使内核也无法逾越的位置。TrustZone 的思路是把隔离从操作系统搬到硬件:用电路而不是代码来执行安全策略。


2. TrustZone 到底是什么

2.1 两个世界

TrustZone 的核心设计极简:把一整颗 SoC 的全部硬件和软件资源一分为二。一部分属于 Secure world,专供安全敏感的子系统(密钥存储、加密运算、DRM、指纹识别);其余一切都归 Normal world,跑常规操作系统和应用。

这不是建议,是硬件强制执行:Normal world 的任何代码——包括运行在特权级最高的内核——尝试访问被标记为 Secure 的内存或外设时,总线控制器在物理层面直接拒绝该事务。

1

2.2 一颗物理核心

TrustZone 不需要两颗物理 CPU。一个 Cortex-A 核心以时间分片的方式在两个世界之间交替执行。上一微秒它还在 Normal world 里跑 Linux 的调度器,下一微秒它已经切到 Secure world 里做签名校验。

从 Normal world 的视角看,Secure world 的执行时间是完全不可见的——它只表现为”处理器在某个不可中断的操作中停留了一段时间”。

2.3 硬件隔离不同于虚拟化

第一次接触 TrustZone 的人经常把它和虚拟机混为一谈。两者本质不同:

  • 虚拟机在同一个安全域内做垂直分层:Hypervisor(PL2)管着上面的 Guest OS(PL1),但大家都跑在同一个边(都用 Normal world)。安全边界仍然是软件(Hypervisor),只是比传统内核更高了一层。
  • TrustZone 做的是水平隔离:Secure 和 Normal 共享同一套特权级体系(各自都有自己的 PL0/PL1),但被一道硬件墙完全隔开。两者不是谁管谁的关系,而是互不可见、只通过 Monitor 模式做受控通信。

简单记法:虚拟化是楼上的住户和下层的物业;TrustZone 是同一层楼两套互相封死的单元。


3. 世界如何切换

3.1 Monitor 模式:唯一的通道

两个世界之间的切换必须经过一个特殊模式——Monitor。ARMv7-A 的处理器模式从 Reset、FIQ、SVC 等一路排下来,Monitor 是专门为 TrustZone 新增的一个。它的特殊之处有两点:

第一,Monitor 模式始终运行在 Secure 状态。无论处理器是从 Secure 还是 Normal 世界进入的 Monitor,在 Monitor 的代码执行期间,它看到的都是 Secure 资源的完整视图。这意味着 Monitor 可以访问两套 banked 寄存器——Normal world 的和 Secure world 的——这是执行上下文切换的前提。

第二,Monitor 模式是唯一可以合法切换世界的上下文。Normal 代码没有别的路径能绕过去。

2

3.2 SMC 指令:Normal world 的敲门砖

Normal world 进入 Monitor 模式的标准方式是执行 SMC(Secure Monitor Call)指令。这是一条体系结构定义的指令,唯一的语义就是触发一个到 Monitor 向量表的异常。

触发 Monitor 模式还有另外几条路:如果 SCR 寄存器配置了 FIQ 或外部 Abort 路由到 Monitor,那么这些硬件异常也会绕开当前世界的向量表,直接目标 Monitor。但从软件调用角度,SMC 是唯一的公开入口。

3

3.3 上下文切换的开销为什么低

从 Normal 世界切换到 Secure 世界,Monitor 模式的处理程序大致做这几步:

  1. 保存当前世界的上下文——SPSR(保存返回地址和 CPSR)、R0 到 R12 等通用寄存器、必要时还包括 banked 的 R13/R14。
  2. 恢复目标世界的上下文——从内存中把 Secure 世界的寄存器状态加载回来。
  3. 修改 SCR 寄存器的 NS 位(0 表示 Secure,1 表示 Non-secure),然后执行异常返回。

整个过程本质上就是一次增强版的内核上下文切换。整个往返通常只需要几百个时钟周期。一个关键的原因后面会展开:切换世界不需要刷新缓存。


4. 世界为什么不能互相访问

上一节讲了”怎么切换”,这一节讲”为什么换过去就碰不到了”。

4.1 NS 位:一切的开端

TrustZone 的硬件隔离由一条比特驱动——NS 位。它被附加到每一笔内存系统事务上:CPU core 到缓存的请求、缓存到总线的请求、总线到外设控制器的请求。没有例外。这也包括缓存行标签——每一条缓存条目都带上了自己的 NS 标记。

从效果上看,NS 位就是第 33 根地址线。Secure world 和 Normal world 各自拥有一套完整的 32 位物理地址空间。两套空间互不重叠,即使它们碰巧在软件侧使用相同的物理地址 0x8000_0000——地址相等但 NS 位不同,硬件就把它们路由到完全不同的物理存储体。

4.2 总线级强制

NS 位只是”标签”。只有标签没有执法者,标签就是摆设。总线系统上有一组配套硬件负责执行安全策略:

  • TZASC(TrustZone Address Space Controller):管理 DRAM 的物理分区。它将物理地址空间划分出一块 Secure-only 区域,所有 NS=1 的事务访问该区域都会被当场拒绝。
  • TZPC(TrustZone Protection Controller):管理外设的安全属性。某些外设(如片内加密引擎)可以标记为 Secure-only,禁止 Normal world 读写。
  • TZMA(TrustZone Memory Adapter):针对片内 SRAM 做类似的访问控制。

这就是 ARM Programmer’s Guide 强调”TrustZone 没有内存系统支持就毫无用处”的原因。NS 位只是信息载体,TZASC/TZPC/TZMA 才是执行者。一个只支持 SMC 指令但总线不做安全检查的 SoC,TrustZone 只是一条 CPU 指令,没有安全价值。

4.3 缓存:Secure 行与 Normal 行可以共存

NS 位也被写入了缓存行的标签。这意味着同一个缓存组里可以同时存在一条 NS=0 的缓存行和一条 NS=1 的缓存行,它们互不干扰,因为缓存在查找时会把 NS 位作为匹配条件。

这个设计的工程后果非常关键:世界切换不需要刷缓存。如果两条世界的缓存行无法共存,每次从 Normal 切到 Secure(或反过来)都要 flush 整个缓存,重建缓存热度将消耗数千个周期。那样的话 SMC 调用的延迟会高到不可接受,TrustZone 就只适用于离线密钥存储等高延迟可接受的场景,而不适合 DRM 视频解密这种实时场景。

4.4 页表:两套独立的虚拟地址空间

TrustZone 也给 MMU 做了双层设计。VBAR(异常向量表基址寄存器)、TTBR0/TTBR1(页表基址寄存器)都是 banked 的——Secure 和 Normal 各有一套独立的副本,指向不同的页表集。

页表描述符里也有 NS 位。但在这里,Normal 和 Secure 的硬件行为完全不同:

  • Secure 世界:页表项的 NS 位决定该页映射到 Secure 物理空间还是 Normal 物理空间。Secure 代码可以自由选择访问哪一边。
  • Normal 世界:硬件直接忽略页表描述符的 NS 字段,所有内存访问强制以 NS=1 发出。不管你在页表里写什么,Normal 世界永远碰不到 Secure 物理地址。

有一处实践细节值得一提:ARM Programmer’s Guide 建议不关心 TrustZone 的内核代码在修改页表时始终将 NS 位设为 0。这听起来矛盾——我在 Normal 世界、不关心安全、为什么要置零?理由很简单:硬件反正会强制覆盖成 NS=1,设成 0 有什么坏处?而且同样的页表操作代码如果在两个世界间复用(比如共享的启动代码),置 0 可以避免把 Secure 世界的映射意外指到 Normal 物理空间去。


5. 对程序员有哪些影响

前面四节是 TrustZone 的设计和原理。对于大多数 Normal world 的程序员来说,道理归道理,他们真正关心的只有一个问题:我的代码行为会怎么变?

5.1 FIQ:可能永远到不了你的 handler

4

ARMv7-A 的异常向量表有三套:一套给 Normal world、一套给 Secure world、一套给 Monitor 模式。硬件根据当前安全状态自动选择对应的表。

但 FIQ 和 IRQ 的路由是可以配置的。SCR 寄存器有两个位分别控制是否把 FIQ 和 IRQ 传到 Monitor 模式。最常见的配置是:FIQ 路由到 Monitor,留给 Secure world 专用;IRQ 留给 Normal world

这意味着你在 Normal world 注册的 FIQ handler 可能永远不会被调用。不是因为代码写错了,而是硬件根本没把 FIQ 送到你的向量表。

5.2 CP15 寄存器:不是所有写入都生效

TrustZone 要求硬件在 CP15 层面伪造出”两个独立核心”的假象。实现方式包括三层:

  • 一部分敏感寄存器(如 SCR、SDER)只能在 Secure 世界写。Normal 世界的写入被硬件静默忽略。
  • 一部分寄存器被硬件自动 bank,例如 TTBR0/TTBR1、VBAR。你的写入确实会生效,但只影响 Normal world 自己的副本。切到 Secure world 后它看到的是另一套。
  • 只读识别寄存器(如 MIDR)两个世界共享,谁都一样。

如果你在写一个驱动,尝试通过 CP15 修改某种系统行为却发现无效,第一反应应该不是怀疑硬件坏了,而是检查一下——这个寄存器是不是 TrustZone 的 secure-only 写限制对象。

5.3 共享内存:NS 位不同等于两个地址

Normal world 和 Secure world 之间可以通过共享物理内存传递数据——比如 Normal world 下载了一个固件更新包,请求 Secure world 的签名校验器验证。

但这里有一个非常容易踩的坑。Normal world 访问更新包数据时以 NS=1 发出,数据进入缓存后,该缓存行的标签被标记为 Non-secure。当 Secure world 接手后去读同一块物理地址,它必须用自己的页表建立一个 NS=1 的映射去访问——因为缓存行的 NS 标签是 Non-secure。如果 Secure world 用了 NS=0 的映射,即使物理地址相同,缓存硬件会因为 NS 位不匹配而视为不同的缓存条目(cache miss),读到的是旧数据甚至随机数据。

这不是性能优化,是数据正确性问题。

5.4 Cache Maintenance:Normal 维护不到 Secure 行

前面讲过 Normal 和 Secure 的缓存行可以共存在同一组,因为它们的标签里带了 NS 位。反过来,这也意味着 Normal world 的 cache maintenance 操作(invalidate、clean)默认只作用于 NS=1 的缓存行。你的 cache flush 操作不会误伤 Secure 世界的数据,但反过来也没法帮 Secure 世界做维护。

5.5 性能可见性的丢失

从 Normal world 的视角看,Secure world 的执行是完全不可见的。处理器进入 SMC 调用后,Normal 世界的调度器看到的现象是”当前线程在一条指令上卡了一段时间”——这段不可中断的执行窗口被归因给最后一条 Normal 世界指令。

后果有二:第一,Normal 世界感知到的中断延迟会略微增加(Secure 执行期间 IRQ 被禁用);第二,性能分析工具无法区分”CPU 在做 Secure 操作”和”线程因为某种原因被阻塞”——两者在 trace 里看起来一模一样。


6. 多核与调试

6.1 多核独立切换

在 SMP 多核系统中,每个核心的 TrustZone 行为完全独立。任意数量的核心可以在任意时刻同时处于 Secure world,一个核心在加密、另一个在跑 Linux 用户程序、第三个在 idle——不需要任何同步。各核心之间的世界切换互不感知。

不过工程实践中有一个折中:ARM Programmer’s Guide 建议 Secure world OS 只跑在单个核心上,其他核心的安全请求通过 IPC 路由到这个指定核心。不是因为做不到多核 Secure OS,而是减少攻击面——少一个跑 Secure 代码的核心,就少一套可能被侧信道攻击的执行上下文。

共享硬件组件(SCU 缓存一致性控制器、GIC 中断控制器)也必须感知安全状态——它们并不会给 Normal world 随意配置安全相关寄存器。

5

6.2 调试隔离

TrustZone 对调试的管控是最容易被忽略的防线。JTAG 调试器和 Trace 数据流的控制可以按世界独立配置:你可以选择只允许调试 Normal world、完全屏蔽 Secure world 的寄存器、内存和指令流。而且这不是软件拦截——是硬件开关。即使攻击者物理接入 JTAG 接口,Secure world 内部的信息也不会从调试通道泄漏。


关键要点

  1. TrustZone 是总线级的硬件隔离。NS 位像第 33 根地址线一样附加到所有内存事务上,两个世界各有一套完整的 32 位物理地址空间,互不可见。软件隔离做不到这一点——因为内核有最高特权,它可以绕过一切自己设定的规则。

  2. 没有 TZASC/TZPC/TZMA 配合,TrustZone 只是一条 CPU 指令。NS 位是标签,这些总线级组件才是执法者。只有 SMC 没有总线安全检查的 SoC,TrustZone 形同虚设。

  3. 世界切换不需要刷缓存——NS 位写入了缓存行标签,Secure 和 Normal 的缓存行可以共存于同一组。这使得一次 SMC 调用往返仅需几百个周期,才能用于 DRM 解密等延迟敏感场景。

  4. 你在 Normal world 写的 FIQ handler 可能永远不被调用——SCR 寄存器可以把 FIQ 路由到 Monitor 模式留给 Secure world 专用。这不是 bug,是合理的安全设计。默认配置下 FIQ 属于 Secure,IRQ 属于 Normal。

  5. 页表项的 NS 位在 Normal 世界被硬件强制覆盖为 1——不管你在页表里写什么值。所以实践规则是始终置零,以兼容 Secure 和 Normal 两边的代码复用。

  6. 跨世界共享内存时,Secure 世界必须用 NS=1 的映射去访问,否则读不到 Normal 世界已有的缓存数据——因为缓存标签里带了 NS 位,NS 不同就不是同一条缓存行。这是数据正确性问题,不是性能问题。

  7. Secure 世界执行期间 Normal 世界看不到任何踪迹——调度器和性能分析工具看到的只是一个不可中断的指令窗口,无法区分 Secure 操作和被阻塞的线程。

  8. 多核系统中每个核心独立切换世界,但实践中推荐只让一个核心跑 Secure OS 以缩小攻击面。

  9. JTAG 调试通道也可以按世界做硬件级隔离——即使物理接入 JTAG,Secure 世界的信息不会从调试通道泄漏。