尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

TrustZone-M深度解析:Cortex-M核心的安全世界与工程实践

TrustZone-M深度解析:Cortex-M核心的安全世界与工程实践 如果你跟我一样接手过一个号称“带 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 行为可能有差异很多细节取决于 IDAUImplementation 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、主流 IoTCortex-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 和 DSPM55/M85 则可以带完整的向量扩展。这里想提醒一点不要因为选了 M85 就默认它的 TrustZone 实现更高级。TrustZone-M 的安全能力上限和核心性能关系不大真正决定安全能力的是芯片厂商怎么实现安全分区、安全启动、调试锁定和密钥管理。M23 做出来的安全设备完全可以比一颗 M85 更安全只要前者的芯片厂商把边界和生命周期管理做扎实。2.3 真正影响选择的其实是厂商实现我在一个项目里做过一次“看似平滑”的迁移把基于 A 厂 M33 的 TrustZone 工程搬到 B 厂的 M33 芯片上。结果几乎重写了整套 SAU 配置链接脚本和中断归属设置。两个芯片厂对安全 Flash 的默认划分完全不同A 厂把高地址 128KB 默认设为 SecureB 厂把低地址 64KB 默认设为 Secure。链接脚本、启动文件、veneer 地址全部要调。所以我的经验是选型时先找厂商的 TrustZone 应用笔记和示例工程确认下面几个问题再动手IDAU 默认的安全属性是可以在 SAU 里覆盖还是硬件固定安全启动流程是否开放Secure 侧启动代码是厂商封装好了还是需要自己写调试端口默认权限是什么量产时怎么锁有没有双核/多实例的参考工程特别是 RTOS 跑在 Non-secure 侧的案例如果这些答案在 Datasheet 和 SDK 里都找不到这个芯片的 TrustZone 项目大概率会卡在前期。3. 核心如何“知道”哪块内存属于谁SAU、IDAU 与安全属性分配3.1 两个单元的分工TrustZone-M 里决定内存安全属性的核心单元有两个SAUSecurity Attribution Unit和 IDAUImplementation Defined Attribution Unit。SAU 在 core 内部由软件通过安全代码配置。它可以在安全启动阶段由高权限固件设置用来细化或修改某块地址空间的安全属性。IDAU 是芯片设计时由厂商定义实现的它决定了芯片上电后默认的安全地图。两者综合作用后才得到每个地址最终的安全归属。你可以把 IDAU 理解成芯片的“出厂底图”SAU 则是允许你在底图上做细化的图层。厂商如果不开放 SAU 对某些区域的覆盖权那这些区域的安全属性就是硬性的。很多芯片的 Flash 和 SRAM 区域都允许 SAU 调整但有些外设总线区域只能由 IDAU 决定配置 SAU 无效。查寄存器手册时要特别留意哪些区域写的是“IDAU 固定”。3.2 三种区域类型在 core 的视角里每个地址有三种可能的安全类型Secure只允许安全状态访问。Non-secure两个状态都可以访问。NSCNon-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 区域里的一条 SGSecure 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]; }注意这里有两个点cmse_nonsecure_entry属性告诉编译器这个函数是 Non-secure 世界可以调用的要生成 SG 指令和对应的返回序列。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安全栈要占 RAMveneer 接口和 IPC 要占开发时间测试矩阵比普通 MCU 会大不少。我见过一个团队为了给极低成本的方案加 TrustZone结果安全固件占的 Flash 比应用代码还大最后只能砍功能。这个账做决定之前就要算。6.3 选型速查清单我后来复盘整个智能门锁项目整理了一份选型清单每次评估新芯片时都会对着过一遍核心选 M23/M33/M55/M85先根据算力需求和功耗预算定。芯片厂商的 SDK 是否带 TrustZone 工程模板没有模板会非常痛苦。调试器对这颗芯片的 TrustZone 支持是否完善建议先用官方的工程模板跑一圈跨世界调试。IDAU 默认安全属性是否可调如果全是硬件固定后续改分区会很受限。有没有官方安全启动案例Secure Boot 是 TrustZone 方案的标配没有案例意味着要自己踩坑。安全固件更新和回滚保护是否支持产品上线后这是刚需。安全调试锁定策略是什么开发期是否容易绕过量产期是否可靠。这些问题在选型阶段搞清楚基本能避开大部分产品和项目的坑。6.4 我对 TrustZone-M 隔离边界的一点看法最后说一点个人心得。TrustZone-M 的隔离是有边界的它能隔离内存和状态但应对不了功率侧信道攻击、故障注入这类物理攻击。如果你做的是银行卡、车钥匙这种强安全设备光靠 TrustZone-M 远远不够还要配合随机数发生器、防篡改检测、安全擦除等硬件措施。对我来说TrustZone-M 的核心价值从来不是“让产品不可被攻击”而是把攻击者的门槛提高到足够高同时让安全升级和维护变得可以控制。在我那个智能门锁项目里最让我受益的并不是“终于没被破解”而是整个开发流程变了Secure 侧固件可以独立测试Non-secure 侧应用崩了不影响密钥区调试和回归效率反而提高了。只要你能接受它带来的额外复杂度并把它当成一个长期运行的工程基础设施去建设那这颗带 TrustZone-M 的核心就真的值得留下来。
返回列表