TrustZone-M深度解析:Cortex-M核心的安全世界与工程实践
2026/8/26 20:49:52 网站建设 项目流程

如果你跟我一样,接手过一个号称“带 TrustZone-M 的 ARM Cortex-M 核心”的 IoT 项目,大概率会有一个共同困惑:TrustZone-M 到底在哪里?是编译器选项,还是芯片寄存器?是烧录分区,还是某个 SDK 里的隔离开关?

实际上,TrustZone-M 是在 ARMv8-M 架构里嵌入到处理器核心层面的一整套硬件机制。它不是软件沙箱,不是操作系统容器,而是处理器核心本身被重新划分成了两个世界:安全世界(Secure)和非安全世界(Non-secure)。每个 core 的取指、访存、外设访问、中断处理、堆栈操作,都带着一个额外的安全属性标签。理解 TrustZone-M,不是去背 API,而是要习惯站在 core 的角度重新看待整个系统。

我自己是从一个实际的智能门锁项目开始接触 TrustZone-M 的。那会儿需要在项目里把密钥存储、固件校验和蓝牙配对算法隔离在一个信任域里。最初以为只要在 SDK 里勾选“使能 TrustZone”就行,结果踩了大量与 core 相关的坑。这篇文章就以我实际跑的芯片和工程为线索,从核心架构、支持 TrustZone-M 的 Cortex-M 家族、SAU/IDAU 安全属性分配、Secure/Non-secure 调用机制、调试与 RTOS 配合等几个方面,把这个题目里真正要聊的东西一次说清楚。

1. TrustZone-M 到底给“核心”加了什么:安全状态机与两级世界

1.1 不是“沙箱”,而是 core 的位态

很多人第一反应是:TrustZone-M 是不是类似桌面 CPU 的虚拟机扩展,给程序提供一个加密隔间?不是这样。TrustZone-M 在 core 内部增加了一个安全状态位,所有正常程序执行时,CPU 都处于 Non-secure 或 Secure 状态。每个指令周期,处理器都会对当前状态做判定:当前取指地址、数据地址归属于哪个安全域,域属性与当前状态不匹配,就会触发 fault。

你可以把 core 想象成一台同时可以扮演“保安室”和“公共大厅”的处理器——公共大厅(Non-secure)里的应用代码,无论如何都无法直接进入保安室;保安室(Secure)里的固件可以合法访问两个世界。这就是安全/非安全世界的关系。更重要的是,这种状态切换不是靠管理员权限,不是靠操作系统调用,而是靠硬件机制和一组受控的入口。

ARM 在设计 M 系列核心时,把这一整套东西直接放进流水线里。所以在 TrustZone-M 的工程里,你经常会听到一个说法:Secure 和 Non-secure 不是两段代码,而是两种执行态。这句话很精确。同一个 core,在不同状态下,即使执行同一条指令,访问同一个地址,结果都可能完全不同。

1.2 核心资源也被一分为二

我最初以为 TrustZone-M 只是给地址空间打了标签,后来才发现处理器核心的资源也被拆成了双份。

  • 栈指针:MSP 和 PSP 在 Secure 侧和 Non-secure 侧各有一份,也就是 MSP_S、PSP_S 与 MSP_NS、PSP_NS 完整分开。这样从非安全世界进入安全世界时,系统栈不会被非安全代码污染。
  • 异常处理:SecureFault、Secure HardFault 这类安全异常会进入安全世界的异常模型,非安全世界无法篡改,也无法屏蔽。
  • 调试逻辑:CoreSight 调试架构也支持安全属性。调试器能不能看到安全世界,由芯片的调试锁定配置决定。有些芯片默认连上调试器只能看到 Non-secure 侧。
  • 控制寄存器:CONTROL 寄存器里新增了安全相关的控制位,比如 nPRIV 在不同世界里的管理方式不同。

也就是说,TrustZone-M 让 core 本身具备了“双世界运行”的能力,而不是靠外部加一个安全芯片。对开发者的直接要求就是:写安全固件时,要考虑栈、中断、调试等所有环节在双世界下的行为。

1.3 为什么题目要强调 Cores

“A Look at Cores with TrustZone-M”这个题目,我觉得“Cores”用得很准确。TrustZone-M 不是一个 IP 块,不能像 SPI 控制器一样随便外挂,它必须内嵌在 Cortex-M 核心内部。从芯片厂商的角度看,要支持 TrustZone-M,需要把 SAU、IDAU、安全调试这些模块整合进核心里面。从用户角度看,同一个 Cortex-M33,不同厂商实现的 TrustZone 行为可能有差异,很多细节取决于 IDAU(Implementation Defined Attribution Unit)。

这一点很容易被忽略。不少团队选芯片时只看“有没有 TrustZone-M”,拿到手才发现:厂商对安全区域的默认划分、安全启动流程、调试锁定策略全都不一样。所以选型第一步不是选“有 TrustZone-M”,而是选“这颗核心对 TrustZone-M 的实现程度以及厂商的生态支持”。

2. 支持 TrustZone-M 的核心家族:M23/M33/M55/M85 的定位与差异

2.1 四个核心的基础画像

目前主流的 ARM Cortex-M 核心里,Cortex-M23、Cortex-M33、Cortex-M55、Cortex-M85 都支持 TrustZone-M,但它们的架构定位和性能等级差别很大。

核心架构TrustZone-MDSP/FPUHelium 向量扩展典型定位
Cortex-M23ARMv8-M Baseline支持可选低成本、超低功耗、简单安全
Cortex-M33ARMv8-M Mainline支持可选通用安全 MCU、主流 IoT
Cortex-M55ARMv8.1-M Mainline支持可选支持端侧 AI 推理 + 数据隔离
Cortex-M85ARMv8.1-M Mainline支持可选支持高性能控制、复杂安全场景

Cortex-M23 是四个里最“轻”的,适合传感器节点、电池供电设备这类对功耗和成本敏感,但依然需要密钥隔离和固件保护的产品。它的 TrustZone 能力并不比 M33 弱,只是整体算力更有限。Cortex-M33 是目前最常见的 TrustZone 芯片选择,DSP 和浮点可选,兼顾了能效比和安全,大量无线 SoC 都基于它。Cortex-M55 和 M85 加入了 Helium 向量扩展,适合需要在设备端跑神经网络模型,同时对模型权重和敏感数据做隔离的场景。

2.2 TrustZone 机制几乎一致,差异在处理器周边

从安全机制本身来说,M23 和 M33 非常接近,都支持 SAU、IDAU、SG 指令、NSC 内存区域,调用协议也相同。差异主要集中在处理器周边的性能资源:M23 没有 DSP 指令,也没有浮点单元,M33 可以带 FPU 和 DSP,M55/M85 则可以带完整的向量扩展。

这里想提醒一点:不要因为选了 M85 就默认它的 TrustZone 实现更高级。TrustZone-M 的安全能力上限和核心性能关系不大,真正决定安全能力的是芯片厂商怎么实现安全分区、安全启动、调试锁定和密钥管理。M23 做出来的安全设备,完全可以比一颗 M85 更安全,只要前者的芯片厂商把边界和生命周期管理做扎实。

2.3 真正影响选择的其实是厂商实现

我在一个项目里做过一次“看似平滑”的迁移:把基于 A 厂 M33 的 TrustZone 工程搬到 B 厂的 M33 芯片上。结果几乎重写了整套 SAU 配置链接脚本和中断归属设置。两个芯片厂对安全 Flash 的默认划分完全不同,A 厂把高地址 128KB 默认设为 Secure,B 厂把低地址 64KB 默认设为 Secure。链接脚本、启动文件、veneer 地址全部要调。

所以我的经验是:选型时先找厂商的 TrustZone 应用笔记和示例工程,确认下面几个问题再动手:

  • IDAU 默认的安全属性是可以在 SAU 里覆盖,还是硬件固定?
  • 安全启动流程是否开放,Secure 侧启动代码是厂商封装好了,还是需要自己写?
  • 调试端口默认权限是什么,量产时怎么锁?
  • 有没有双核/多实例的参考工程,特别是 RTOS 跑在 Non-secure 侧的案例?

如果这些答案在 Datasheet 和 SDK 里都找不到,这个芯片的 TrustZone 项目大概率会卡在前期。

3. 核心如何“知道”哪块内存属于谁:SAU、IDAU 与安全属性分配

3.1 两个单元的分工

TrustZone-M 里决定内存安全属性的核心单元有两个:SAU(Security Attribution Unit)和 IDAU(Implementation Defined Attribution Unit)。

SAU 在 core 内部,由软件通过安全代码配置。它可以在安全启动阶段由高权限固件设置,用来细化或修改某块地址空间的安全属性。IDAU 是芯片设计时由厂商定义实现的,它决定了芯片上电后默认的安全地图。两者综合作用后,才得到每个地址最终的安全归属。

你可以把 IDAU 理解成芯片的“出厂底图”,SAU 则是允许你在底图上做细化的图层。厂商如果不开放 SAU 对某些区域的覆盖权,那这些区域的安全属性就是硬性的。很多芯片的 Flash 和 SRAM 区域都允许 SAU 调整,但有些外设总线区域只能由 IDAU 决定,配置 SAU 无效。查寄存器手册时,要特别留意哪些区域写的是“IDAU 固定”。

3.2 三种区域类型

在 core 的视角里,每个地址有三种可能的安全类型:

  • Secure:只允许安全状态访问。
  • Non-secure:两个状态都可以访问。
  • NSC(Non-secure Callable):比较特殊,地址本身属于安全区域,但允许非安全状态从这里“半进入”安全世界。

NSC 区域就是后面要讲到的 veneer 跳板所在地。为什么需要专门划出一块 NSC?因为如果非安全代码随便跳到一个安全函数地址,core 会直接 fault,所以必须有一个明确定义好的“受控入口”,让非安全代码执行 SG 指令进入安全状态。

举个例子,假设某芯片的 Flash 布局是这样的:

0x00000000 - 0x0000FFFF Secure 启动代码、安全固件 0x00010000 - 0x0001FFFF NSC veneer 函数 0x00020000 - 0x0007FFFF Non-secure 应用代码

非安全应用只能调用位于 NSC 区间里的入口函数,直接跳去 Secure 区间的任何地址都会被 fault 拦下。这个设计很适合把安全固件放在低地址,应用放在高地址,中间留一块 NSC 做桥接。

3.3 属性不匹配会触发什么

当 Non-secure 代码访问 Secure 地址时,core 产生 SecureFault 或 BusFault,具体取决于访问类型和总线实现。Secure 侧代码访问 Non-secure 地址通常是被允许的,但在某些配置下也会受限。这个不对称的设计是刻意的:安全侧可以把数据放到非安全侧,反过来不行。

实际开发中最容易踩的坑,是 SecureFault 发生后系统直接 hang 或者跳进 HardFault,原因往往不是访问冲突本身,而是 SecureFault handler 没有被初始化。很多工程模板只把 attention 放在非安全侧,忘了在安全启动代码里注册 SecureFault 中断入口。我建议所有 TrustZone 项目的第一步,先把 SecureFault 和 Secure HardFault 的 handler 挂好,并让它们输出足够多的诊断信息,再去调 SAU 配置。否则后面排查问题时,你会在完全没有错误线索的状态下盯着汇编发懵。

4. 安全世界与非安全世界的调用机制:veneers、SG 指令与状态切换

4.1 为什么不能直接调用安全函数

在 TrustZone-M 里,Non-secure 侧不能直接 BL 或 BLX 到一个 Secure 地址,因为普通跳转指令不会切换安全状态,core 会直接判定这是一次非法访问。唯一合法的入口,是 NSC 区域里的一条 SG(Secure Gateway)指令。

SG 指令的作用,是在执行到 NSC 区域时,让 core 从 Non-secure 状态切换到 Secure 状态,并继续执行 SG 后一条指令。这意味着,SG 指令本身必须放在地址对齐好的 NSC 区域里,且它前面不能有别的跳板逻辑。编译器在生成带cmse_nonsecure_entry属性的函数时,会自动在函数入口放置这条指令。

4.2 veneer 函数的正确写法

在 C 代码层面,可以这样定义安全侧的函数:

__attribute__((cmse_nonsecure_entry, section(".nsc_veneers"))) uint32_t secure_get_key(uint32_t slot) { return key_store[slot]; }

注意这里有两个点:

  1. cmse_nonsecure_entry属性告诉编译器,这个函数是 Non-secure 世界可以调用的,要生成 SG 指令和对应的返回序列。
  2. section(".nsc_veneers")让函数落到链接脚本指定的 NSC 内存区域。

还有一种写法是用汇编定义 veneer 入口,再跳转到真正的安全处理函数。这种更底层,但更灵活。最重要的是记住:真正敏感的代码不要写在 veneer 函数体内,veneer 只是一个“过门”的桥,过门之后应该立刻把控制权转给内部更深层的安全函数。敏感逻辑放在安全世界更深处,即使 Non-secure 侧有人尝试做异常输入,也很难在 veneer 层搞出问题。

4.3 状态切换时的栈与寄存器行为

当 Non-secure 代码执行 SG 进入 Secure 状态时,core 会在某个时刻把栈指针从 MSP_NS/PSP_NS 切到 MSP_S/PSP_S。所以安全固件必须拥有独立的栈空间,而且这个栈不能被 Non-secure 侧访问。如果 Secure 栈配置得不够,或者被意外溢出,后果非常隐蔽——因为 fault 可能在切换回 Non-secure 侧时才显现。

我自己的习惯是:在环境安全栈的底部和顶部各放一个哨兵值,周期性的在安全任务里检查哨兵有没有被踩。MSP_S 和 PSP_S 都要单独看,因为如果安全侧代码既用特权模式又跑裸机,很容易只配了 MSP_S 而忘了 PSP_S 也有独立空间。

返回时,Secure 函数通过 BXNS 指令回到 Non-secure,同时恢复之前的参数寄存器内容。ARM 的 AAPCS 还额外规定,安全函数返回给非安全世界的参数,不能含安全地址的指针,否则会有泄漏风险。编译器通常会帮你做一部分检查,但最稳妥的是从设计上就保证安全侧只返回值和状态码。

4.4 中断优先级与安全属性冲突

很多开发者会把“安全世界”想象成绝对不可被抢占的。其实不对。Non-secure 中断在特定情况下可以抢占正在执行的 Secure 函数,具体取决于 NVIC 的优先级配置和 AIRCR.PRIS 的设置。

如果 Secure 函数正在处理密钥相关的临界区,而此时一个 Non-secure 外设中断到达,core 是会切换过去的。切换过程会把所有 Secure 上下文压入 MSP_S/PSP_S,然后进入 Non-secure 的中断处理函数。如果 Non-secure 中断处理函数被攻击者控制,理论上是可以通过观察时序、缓存状态等方式侧信道猜测 Secure 侧正在处理的数据。

缓解办法有两个层面:

  • 在安全代码里,进入敏感区域前用__disable_irq()或者操作 BASEPRI,把优先级低于某个阈值的中断全部屏蔽。
  • 在系统初始化阶段,把 AIRCR.PRIS 设置为 1,使得 Non-secure 中断永远无法抢占 Secure 异常处理。

两种手段组合使用,基本能保证 Secure 侧的敏感操作具备原子性。不过要留意,过度屏蔽中断会影响实时性,所以安全代码的临界区要尽量短。

5. 调试、RTOS 与量产中的实际坑:跨世界开发的几个大坑

5.1 调试器的世界切换与安全锁定

TrustZone-M 工程调试和普通 MCU 最大的区别,是你得意识到自己面对的是两个世界。主流调试器(比如 Keil MDK、IAR、SEGGER 的调试器)已经支持 TrustZone 调试,但默认行为可能各不相同。

一些调试器连接成功后,默认只能看到 Non-secure 上下文。如果 Secure 固件在启动早期就配置了安全调试锁定,你连读安全 Flash 都做不到。开发阶段我一般建议先不要去锁安全调试,等量产固件做最后一步固化时再启用锁定策略。否则出了 bug,想在线看 Secure 侧代码,整个团队会陷入“能编译但没法查”的窘境。

另外,烧录工具也要注意。TrustZone 工程通常有两个镜像:安全镜像和非安全镜像。烧录时两个镜像的地址段不能乱叠。如果烧录工具不认 TrustZone 分区表,很可能把非安全镜像安全区域的起始位置覆盖掉,造成上电后安全代码直接跑飞。

5.2 中断归属与 NVIC 配置

NVIC 里的每一个中断,都会绑定了安全属性。这个属性通常由外设所在的总线地址空间决定,同时受 NVIC 内部配置影响。如果 Non-secure 代码尝试配置一个 Secure 中断,操作会被忽略,甚至触发 fault。

常见的坑发生在芯片的外设归错世界。比如某个定时器被分到了 Non-secure 空间,但你在安全固件里想用它来做安全侧的 tick,就会发现在 Secure 侧配置完全正常,换到 Non-secure 侧后中断根本进不来。反过来,如果外设属于 Secure 世界,Non-secure 应用想直接驱动它,就必须通过安全侧提供的服务接口,这虽然看起来多一层,但反而是 TrustZone 想让你做的事:外设的访问权限和安全策略统一收口到安全侧。

5.3 RTOS 放在 Non-secure 侧的调度风险

在多数 TrustZone-M 工程里,RTOS 跑在 Non-secure 世界,Secure 侧是一个精简固件,只做安全服务。这种模型符合“最小可信”的原则,但有一个巨大的架构约束:安全侧的代码不能直接调用 Non-secure 侧的 RTOS API。

如果 Secure 侧的任务需要等待某个数据或者信号量,而信号量在 Non-secure RTOS 里,怎么办?我们在项目里的做法是:使用同步调用模型,也就是 Non-secure 侧发起一个安全函数调用后,必须阻塞等待返回;Secure 侧处理完直接返回,不做异步回调。这样避免了两个世界之间的调度器互相纠缠。

如果实在需要异步事件,就得做一套 IPC 机制。比如 Non-secure 侧通过一个共享内存区域投递请求,Secure 侧用安全中断或者轮询方式取消息。这个共享内存要放到 NSC 或者 Non-secure 区域,安全侧访问时需要格外小心边界校验,防止 Non-secure 侧塞入恶意参数。

5.4 两个经典故障案例

案例一:NSC 区域没标记,入口函数直接 HardFault。

现象是 Non-secure 侧调用一个安全函数指针,一进去就 HardFault。我第一反应是地址没对齐,后来反汇编 veneer 函数,发现函数入口根本没有 SG 指令,而且地址不在 NSC 区域内。查下来是链接脚本漏了.nsc_veneers段,导致 veneer 函数被放到了普通 Secure 区域。解决方法是把 NSC 段的地址范围写进链接脚本,并确认 SAU 配的 NSC 区域和链接脚本一致。

案例二:量产过一次的设备,Secure 镜像无法二次烧录。

现象是开发板第一次烧录没问题,返工后再烧 Secure 镜像就失败。原因是在安全启动流程里启用了安全调试锁定,并且没有预留解锁机制。量产时如果要把调试口锁掉,一定得想清楚怎么升级安全固件。常见的做法是把“解锁条件”绑定到安全引导的签名校验,只有签发过的新固件才能更改调试锁状态。不要一锁了之,否则产品返修时基本只能换芯片。

6. 选型与落地建议:什么样的产品才需要 TrustZone-M 核心

6.1 值得上 TrustZone-M 的场景

从我接触过的项目看,真正让 TrustZone-M 发挥价值的是这几类场景:

  • 需要在设备端保存对称密钥、私钥,并且希望即使固件被攻破,密钥也无法直接 dump。
  • 需要安全启动,启动代码在 Secure 世界校验 Non-secure 应用的签名,防止固件被篡改或回滚。
  • 需要通过 PSA 认证或者客户安全审计,TrustZone-M 是 PSA 认证方案里很重要的一环。
  • 需要保护关键算法和 AI 模型权重,比如指纹特征比对、声纹模型、边缘推理模型,这些资产如果随 Flash 一起被别人读出,商业价值就没了。

在这些场景里,TrustZone-M 提供的不是“绝对安全”,而是把攻击门槛提高到一个合理水平:攻击者需要先击穿 Secure 侧固件,才能拿到真正有价值的数据。

6.2 不值得的场景与成本评估

反过来,有些项目真的没必要折腾 TrustZone-M。比如产品本身是几块钱的简单控制器,安全需求只是“防误操作”,那多加一堆安全分区、双镜像和启动校验,纯属给自己找麻烦。再比如团队没有持续维护安全固件的计划,那安全代码一旦出问题,可能比非安全代码更容易变成黑盒。

成本上也要算清楚,TrustZone-M 不是免费功能。Secure 镜像要占 Flash,安全栈要占 RAM,veneer 接口和 IPC 要占开发时间,测试矩阵比普通 MCU 会大不少。我见过一个团队为了给极低成本的方案加 TrustZone,结果安全固件占的 Flash 比应用代码还大,最后只能砍功能。这个账,做决定之前就要算。

6.3 选型速查清单

我后来复盘整个智能门锁项目,整理了一份选型清单,每次评估新芯片时都会对着过一遍:

  1. 核心选 M23/M33/M55/M85?先根据算力需求和功耗预算定。
  2. 芯片厂商的 SDK 是否带 TrustZone 工程模板?没有模板会非常痛苦。
  3. 调试器对这颗芯片的 TrustZone 支持是否完善?建议先用官方的工程模板跑一圈跨世界调试。
  4. IDAU 默认安全属性是否可调?如果全是硬件固定,后续改分区会很受限。
  5. 有没有官方安全启动案例?Secure Boot 是 TrustZone 方案的标配,没有案例意味着要自己踩坑。
  6. 安全固件更新和回滚保护是否支持?产品上线后这是刚需。
  7. 安全调试锁定策略是什么?开发期是否容易绕过,量产期是否可靠。

这些问题在选型阶段搞清楚,基本能避开大部分产品和项目的坑。

6.4 我对 TrustZone-M 隔离边界的一点看法

最后说一点个人心得。TrustZone-M 的隔离是有边界的,它能隔离内存和状态,但应对不了功率侧信道攻击、故障注入这类物理攻击。如果你做的是银行卡、车钥匙这种强安全设备,光靠 TrustZone-M 远远不够,还要配合随机数发生器、防篡改检测、安全擦除等硬件措施。

对我来说,TrustZone-M 的核心价值从来不是“让产品不可被攻击”,而是把攻击者的门槛提高到足够高,同时让安全升级和维护变得可以控制。在我那个智能门锁项目里,最让我受益的并不是“终于没被破解”,而是整个开发流程变了:Secure 侧固件可以独立测试,Non-secure 侧应用崩了不影响密钥区,调试和回归效率反而提高了。只要你能接受它带来的额外复杂度,并把它当成一个长期运行的工程基础设施去建设,那这颗带 TrustZone-M 的核心,就真的值得留下来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询