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 四层隔离边界 第1层:硬件层 NS 位 / TZASC / TZMA 总线信号 + 内存控制器 第2层:系统软件层 Monitor / SMC 指令 安全监控调用 第3层:内存隔离层 安全内存 / 非安全内存 物理地址空间划分 第4层:外设隔离层 安全外设 / 非安全外设 AXI-to-APB 桥 + 安全位 四层缺一不可 任何一层被绕过,安全边界即失效

这张图我建议你存下来。每次调试 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 的底层部分)负责:

  1. 保存当前世界的上下文(寄存器、状态)
  2. 切换 NS 位
  3. 恢复目标世界的上下文
  4. 跳转到目标世界的入口
// 伪代码: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 互相踩内存,那可就真得加班了。