STM32 TrustZone实战:从原理到安全双工程配置 📅 发布时间:2026/8/29 15:14:31 👁 浏览次数: TrustZone这个词做M系列的朋友最近两年应该没少听。它最早是Arm在Cortex-A上推出的硬件隔离方案用来保护Android、Linux这类复杂系统里的密钥和支付数据。后来Arm把TrustZone下放到Cortex-M在Armv8-M架构里重新实现了一套ST把它落到了STM32L5和STM32U5这两个系列上。如果你拿到的任务是把程序拆成安全和非安全两半或者要给物联网设备做安全启动、防抄板、密钥保护那TrustZone大概率是你绕不开的东西。在动手调之前先要有一个认知STM32L5和U5上的TrustZone并不是简单加一个“安全开关”它要求你在项目规划阶段就分出两个世界。ST的做法也很直接STM32CubeMX里选芯片时打开TrustZone会直接生成两个工程一个安全工程一个非安全工程。很多第一次接触的人会在这个环节懵掉不知道怎么把这个“双胞胎”工程跑起来。这篇文章就把我从原理到实战的完整路径讲清楚涉及的工具、配置和踩坑点都是实际验证过的希望对准备上手安全分区的朋友有参考价值。1. 项目概述与产品定位1.1 TrustZone到底在解决什么先抛开指令集和寄存器用业务语言说TrustZone就是在同一颗MCU上划出两个互不信任的执行环境。安全环境可以访问所有资源非安全环境只能访问被授权的资源。两者的隔离是硬件层面的不是MPU那种“软”隔离也不是靠软件约定。传统MCU做安全通常靠三板斧MPU隔离、RDP读保护、外部加密芯片。MPU能做到运行时保护但一旦非安全代码把自己提升到特权模式再改MPU配置是可能的也就是说它防的不是“自己人”。RDP只能整体保护Flash没法把Flash里的一部分开放给调试器、另一部分保护起来。外部加密芯片成本高通信链路还可能被监听。TrustZone的隔离发生在总线层面上处理器的每一次访问都带有安全属性访问被拒就直接拒绝和固件是否被攻破没有关系。在实际产品中TrustZone最常见的用法是密钥、证书、安全升级逻辑放在安全侧通信协议栈、UI、应用逻辑放在非安全侧。即使非安全侧被漏洞利用攻击者拿到的也只是一个没有密钥的世界。我在做L5的仪表类项目时就是把计费密钥和AES引擎锁在安全侧App侧无线升级随便折腾最坏情况也只是恢复出厂底线数据始终在安全区内。1.2 STM32L5与STM32U5怎么选STM32L5是目前ST基于Armv8-M架构的入门级TrustZone产品Cortex-M33内核主频110MHzFlash最大512KBSRAM 256KB。STM32U5则是L5的升级版主频拉到160MHz存储容量大幅提升最高2MB Flash、786KB SRAM还带了CORDIC/FMAC数学加速器、图形加速、LPBAM低功耗后台自主运行等能力。两者都有TrustZone都使用ST的GTZC做安全属性管理。选型时我一般这样判断如果产品只做安全启动、密钥存储、代码防抄板对算力和存储要求不高L5足够成本低、功耗也低。如果产品需要跑模型推理、图形界面或者要在休眠时让外设自主采集数据U5更合适毕竟大存储和LPBAM是实打实的优势。U5上ST还提供Secure Manager方案属于“开箱即用”的预配置安全固件如果不想从零设计安全架构可以走这条捷径。另外U5的硬件加解密单元更强AES、HASH、PKA、OTFDEC都有对安全启动和固件加密是很大的加分项。在TrustZone的用法上L5和U5基本一致下面讲的原理和步骤换个芯片型号也一样能跟下来。真正差别大的是芯片外设和低功耗策略TrustZone本身的使用逻辑完全通用。2. 核心原理Armv8-M TrustZone到底是怎么工作的2.1 安全状态与非安全状态Armv8-M的TrustZone为Cortex-M处理器引入了两种状态Secure和Non-Secure。处理器每时每刻都处于其中一种状态。安全状态可以访问全部内存和外设非安全状态只能访问被标记为Non-Secure的资源。切换不是简单的函数调用而是有专用的指令和时序要求。有一个关键点在TrustZone使能后处理器内部很多寄存器都变成了“两份”。比如MSP有MSP_S和MSP_NSCONTROL有CONTROL_S和CONTROL_NS。中断向量表也分成安全和非安全两部分。所以在调试时你经常会看到“S”和“NS”的标记这不只是命名习惯而是代表着硬件上物理隔离的两套状态。状态切换的规则是从非安全状态进入安全状态只能通过“非安全可调用”区域NSC里的SG指令从安全状态回到非安全状态使用带NS后缀的跳转指令比如BXNS。普通BL/BX跳转无法跨状态。这样设计的目的是保证进入安全代码的路径完全可控非安全代码不能随便跳到安全代码的任意位置。如果你在NS侧直接取一个Secure函数的地址并调用Cortex-M33会直接触发异常而不是帮你“宽容”地进入安全状态。2.2 IDAU、SAU与安全属性的判定每个地址在访问时该怎么判定安全属性靠两套硬件单元IDAU和SAU。IDAU是芯片厂商实现、不可编程的单元它把系统内存和总线地址“预先打标签”——哪些地址是安全的哪些是非安全的哪些是NSC在上电那一刻就是固定的。SAU是软件可编程的单元可以覆盖IDAU的部分判定。规则是IDAU判定为Non-Secure的区域SAU无法把它变成SecureIDAU判定为Secure的区域SAU可以把它改成Non-Secure或者NSC。也就是说IDAU定义了“底线”SAU在这个基础上只能做“放宽”。在STM32L5/U5上TZEN选项字节使能后内部Flash和SRAM默认都属于Secure。所以要跑非安全代码你必须在启动阶段显式地把一部分存储区标成Non-Secure。这个工作由GTZC下的MPC内存保护控制器完成它把Flash和SRAM切成一个个区块每个区块都可以单独设置安全属性。外设的安全属性由TZSC配置中断的安全属性由TZIC配置。这三者合起来就是ST的GTZC体系。为什么这么设计上电阶段默认全Secure是为了保证安全启动链复位后CPU先跑安全代码由安全代码初始化并“释放”非安全区域之后才把控制权交给非安全代码。这样非安全代码从出生起就被限制在笼子里这个“先安全后非安全”的时序是TrustZone方案的核心。2.3 NSC与SG指令跨世界的桥非安全代码要调用安全代码里的函数不能直接BL过去那会触发异常。必须经过一座“桥”这座桥就叫NSC区域。NSC区域是一段特殊的内存它的安全属性被标记为Non-Secure Callable。非安全代码可以调用NSC区域中的地址但在NSC区域里必须有一条SG指令SG指令执行后处理器状态才从Non-Secure切换到Secure然后才跳到真正的安全函数体。C编译器把这座桥的搭建自动化了。你写安全函数时只要加上__attribute__((cmse_nonsecure_entry))编译器会帮你生成相应的入口和跳板veneer并把它们放在NSC区域。你不需要手工写SG指令。类似地从安全代码调用非安全代码比如安全模块想调用非安全侧的状态上报回调要使用cmse_nonsecure_call属性编译器会生成正确的BXNS调用序列。需要注意的是跨状态调用的参数传递遵循标准AAPCS通常通过r0到r3传递。但CMSE还做了一些额外的安全检查比如会在入口处验证函数的调用地址确实在NSC区域、验证参数指针指向非安全内存等。这些检查是为了防止安全侧被非安全侧的恶意参数利用。不过要注意指针指向是否合法最终还是要靠在安全函数体内部做手动校验编译器不会替你把所有业务逻辑都验证完。2.4 GTZCST的TrustZone控制器体系GTZC的全称是Global TrustZone Controller它管三件事外设安全属性TZSC、中断安全属性TZIC、内部存储器安全分区MPC。MPC把Flash和SRAM分成若干区块每个区块的安全属性可独立设置。注意区块大小是固定的从几KB到几十KB不等分区时要考虑对齐。TZSC决定每个外设的寄存器访问属于安全还是非安全。比如可以把UART分给非安全侧做日志把AES留在安全侧做加解密。TZIC决定每个中断源由哪个世界处理。安全中断事件只能被安全状态下的ISR响应非安全中断则相反。在CubeMX里这三个配置都有图形化界面。但即便你不在CubeMX里配置直接在S工程的代码里调HAL库函数也能完成。无论用哪种方式都要理解一件事GTZC的配置生效时机非常关键必须在跳到非安全代码之前完成否则非安全代码连自己所在的Flash都读不了上电就是HardFault。3. 实操准备工具链与CubeMX配置3.1 工具链选择TrustZone工程对编译器有要求。用户手册里说得比较明确Arm Compiler 6从6.7开始完整支持Cortex-M33的CMSE扩展所以Keil MDK要使用AC6编译器不要再用AC5。IAR从8.40.x开始支持TrustZone。STM32CubeIDE自带GCC工具链但注意GCC对CMSE的支持是从arm-none-eabi-gcc 10开始的老版本没有-mcmse选项编译会报错。我自己的经验是刚开始做TrustZone项目时用STM32CubeIDE最省心生成的S/NS工程模板是配套好的不用自己去拼链接脚本。调试器方面ST-LINK、J-Link都能用但要注意调试器固件版本不要太老否则识别不了双工程的debug会话。烧录工具推荐STM32CubeProgrammer它和TrustZone的选项字节、RDP等级管理配合得最完整。另外在x86主机上如果想在买板子之前先验证M33的TrustZone行为可以试试QEMU对Cortex-M33的模拟但QEMU对GTZC这类芯片特定外设支持不完善验证平台相关代码还是得用真板子。3.2 CubeMX里怎么配置TrustZone我用STM32CubeMX 6.x为例。新建工程选好L5或U5型号后左侧“Security”分类里能找到TrustZone选项。打开它CubeMX会提示需要设置选项字节中TZEN为1同时会让你规划Flash/SRAM的安全分区。和普通工程一个明显的区别是生成代码时CubeMX会生成两个独立的工程目录一个带_S后缀一个带_NS后缀。说一下分区规划。默认情况下CubeMX会给出一个比例比如把Flash前64KB分给Secure剩余给Non-Secure。你可以按实际需求改。分区的粒度受GTZC的MPC区块大小限制并不是任意字节都能切需要对齐到区块边界。SRAM也是同理。还有一个隐含要求NSC区域必须落在Secure Flash中建议单独划出一块比如4KB并在GTZC中把这块配成NSC属性。另外在CubeMX的Project Manager里可以设置“TrustZone Security”相关的选项比如是否把某个外设默认配置到Secure或Non-Secure。但这些其实在代码里也能改不必太纠结于GUI里的初始化状态。真正要提前想清楚的是你的安全边界面在哪里哪些外设归S哪些归NS这决定了后面所有代码的存放位置。3.3 双工程结构与启动流程生成后的双工程S工程和NS工程是分开编译、分开烧录的。两者共享一个HAL库但S工程带全部启动文件和GTZC配置NS工程只在生成的链接脚本里做了内存布局。发布产品时通常要做一个打包烧录S镜像和NS镜像合并后一次烧进去。启动流程可以这样理解复位后CPU进入Secure状态从S工程的启动向量