拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32L5新架构解析:TrustZone如何重塑MCU安全设计

说实话作为一个在嵌入式圈子里折腾了十年的老家伙这几年听了太多把旧技术换个名字就叫“新架构”的培训。但STM32L5这次不太一样——它把原本只在Cortex-A上应用的TrustZone引入到了Cortex-M带来的不只是“更安全”三个字而是整个MCU的系统架构设计范式都变了你的代码、数据、外设甚至每一个中断源第一次被要求明确回答“我属于哪个世界”。这篇关于STM32L5系统新架构的经验分享我尽量把产品培训中反复强调、参考手册里藏着掖着的关键点都掏出来也补上我回来以后实际联调踩过的坑。适合正在评估L5的开发者也适合已经上手L5、但对TrustZone概念还是一团浆糊的人。1. 为什么说STM32L5的系统新架构是真新1.1 传统MCU的安全防御为什么不够用在没有TrustZone的年代我们是怎么给MCU做安全防护的最常见的三板斧开RDP读保护、用OTP区域做唯一ID、代码里加软件混淆。这几个手段各有各的问题。RDP只能对抗“静态读取Flash”不能防止固件被重新打包回灌也不能阻止调试器在部分场景下通过特定方式拿到内存内容软件混淆只能提高逆向的成本拦不住有心人花时间慢慢啃。这些问题放在十年前的传统工控现场还能忍放到今天连个开关面板都要上云联网的IoT设备上就完全不够用了。更麻烦的是传统MCU里所有代码都跑在同一个特权层级和同一个内存空间里只要有一个漏洞比如一个没有边界检查的网络协议栈攻击者拿到任意代码执行能力后整个设备就“裸奔”了——密钥、固件、业务代码全在一个锅里。STM32L5要解决的核心问题说白了就是让最值钱的东西密钥、安全协议栈、固件IP有一个烧不穿、读不走的“保险箱”即使应用侧被打穿也拿不到保险箱里的东西。这个需求不是某一家芯片厂拍脑袋想出来的而是物联网设备大规模部署之后被一次次攻击事件逼出来的刚需。1.2 TrustZone进入MCU一次架构级的下放TrustZone这个词写过手机芯片的开发者应该不陌生。它最早是Cortex-A系列上的安全扩展用来实现TEE可信执行环境。但本质上它是一套硬件级的隔离机制同一个物理核通过状态位划分出安全世界和非安全世界两个世界跑在同一颗CPU上总线、存储、外设和中断都被打上安全属性标记硬件在访问路径上做检查。ST把TrustZone从Cortex-A移植到Cortex-M上配合的是Armv8-M架构新增的一组安全扩展指令和系统寄存器这才是真正的“架构级”变化。在Armv8-M里这种机制有一个正式名字叫TrustZone for Armv8-M官方称“Cortex-M Security Extensions (CMSE)”。它不像软件隔离那样有性能损耗也不需要额外挂一颗安全MCU去承载而是在核内、总线、存储控制器和外设控制器的多个层面同时生效。对于MCU开发来说这意味着安全从“打补丁”变成了一种“设计约束”从芯片上电复位那一刻起两个世界就存在你的每一行代码、每一个变量、每一个外设在系统里都有一个归属。这个转变对老工程师来说其实是需要一段时间适应的因为你以前写代码不用先想“这段函数以后会不会被攻击者碰到”现在得想了。1.3 和STM32L4对比变化到底在哪多说一句我和不少人一样刚听说L5时第一反应是“这不就是加了加密引擎的L4吗”。等我把L4和L5的芯片手册放在一起过了一遍才确认这个判断是错的。L4也有AES、RNG这些安全外设但那些都是“功能模块”是给应用层调用的而L5的安全能力是贯穿到系统级和架构级的。此外L5最高跑到110MHzL4一般到80MHzFlash和SRAM容量也更大但这都不是重点。用一张表格看更清楚对比项STM32L4STM32L5内核Cortex-M4Cortex-M33支持CMSETrustZone不支持支持硬件强制隔离安全岛无有密码外设与关键存储独立隔离GTZC全局安全控制器无有统一管理安全属性分配防回读/防回灌机制RDPRDP PCROP Debug Authentication典型主频最高80MHz最高110MHz重点是L5引入了三样之前STM32全系都没有的东西第一TrustZone安全扩展这是架构级的不是某个模块第二安全岛把RNG、AES、PKA等密码外设以及一部分关键存储独立出来非安全代码在物理上就无法直接访问第三全局TrustZone控制器GTZC统一管理内存、外设的安全属性分配并提供非法访问事件上报。这三点加在一起意味着L5在应用侧就算跑了一个被攻破的通讯协议栈攻击者能下手的边界也被硬件钉死在高墙之外。理解到这个层面再去看L5的参考手册“System architecture”那一章读起来才有方向感。2. STM32L5系统新架构全景拆解2.1 两个世界Secure与Non-secure的硬件隔离先打个比方。想象你在一个办公室里有一间装了防爆门的机房。所有人都可以在办公区正常工作但只有授权人员刷门禁才能进机房拿服务器密码。在TrustZone体系里办公区叫Non-secure world非安全世界机房叫Secure world安全世界门禁就是一组硬件检查点。两个世界跑在同一个Cortex-M33上CPU会根据当前运行在哪个状态来决定走哪条访问路径。也就是说你不是在两颗芯片之间做隔离而是在一颗芯片内部做了逻辑上的物理级隔离。关键的一点这种隔离不是软件临时判定的而是从硬件管线开始就参与。Cortex-M33核内部有一个“安全状态”信号内存访问、外设访问、中断响应都会携带这个状态。如果Non-secure世界的代码试图去访问被标记为Secure的内存或外设总线层面的检查直接报错CPU会进入SecureFault或HardFault而不是像普通权限错误那样可以被软件屏蔽。这等于把安全判断下放到了每次总线事务里拦截粒度非常细任何一个总线主设备发起的访问都逃不掉这个检查。那CPU是怎么知道自己当前处于哪个世界呢靠的是状态位的切换而不是跑两套独立操作系统。安全世界到非安全世界的切换需要执行SG指令完成“安全网关”动作同时会把返回地址等信息处理妥当保证不会因为跳转而泄漏安全侧的上下文。写应用时你可以不用关心这些细节但理解这条切换路径后排查跳转类问题会快很多因为很多莫名其妙的崩溃正是因为跳转路径上没有经过NSC区域触发了硬件保护机制。2.2 总线与安全岛外设、内存的访问控制链路连接着Cortex-M33的AHB5和AHB3总线是L5内部数据流动的主干道。但与传统MCU总线最大的区别在于总线上每一个可访问的地址都带有一个安全属性标签。访问请求从CPU发出后会被沿途的IDAU、MPC、PPC等部件逐级检查标签不匹配就掐断。这也是为什么ST培训时反复强调“L5的地址不再只是一段地址而是地址安全属性”这个观点。你在配置寄存器的时候凡是涉及到地址范围、DMA目标地址、DMA源地址的地方都要先问一句这个地址当前被标成了哪个世界接着就是“安全岛”的概念了。你可以把它理解为芯片上专门划出来的一片区域里面住着的是最敏感的资源和外设。比如AES对称加密引擎、RNG真随机数发生器、PKA公钥加速器、OTP一次性可编程区域、部分Flash和SRAM都放在安全岛里。安全岛的访问规则很简单只有来自安全世界的请求才有资格进入Non-secure侧连操作入口都摸不到。这种设计有一个很实际的好处即使应用侧的复杂协议栈被攻击者完全控制攻击者也拿不到加密密钥和签名私钥因为他们根本无法发起访问。这里忍不住分享一个课堂上没展开、但实际联调时非常容易踩的坑DMA。Non-secure代码里用DMA去搬运数据时目标地址和源地址如果有一侧落在Secure区域DMA会立刻触发总线错误。因为DMA也是一个总线主设备同样要遵守安全属性检查。很多第一次跑通TrustZone工程的人第一步死在UARTDMA收发上就是这个原因后面在排查部分我会专门说。你只有把DMA的缓冲区和外设寄存器都归到同一个安全世界里总线才会放行。2.3 谁来决定“安不安全”IDAU、SAU与GTZC那地址的安全属性到底是谁拍板的呢在L5里这个决定权被拆成了两层理解这一点对整个架构的驾驭至关重要。第一层是芯片出厂时就定死的IDAUImplementation Defined Attribution Unit。每个地址属于安全还是非安全很大一部分是芯片设计者根据硬件逻辑固化的比如安全岛内的外设地址就一定被IDAU标为Secure这块你改不了。第二层是可配置的SAUSecurity Attribution Unit由软件在系统初始化时设置。SAU可以基于IDAU的默认结果做覆盖比如把一块原本不确定的区域划分成Non-secure供应用态使用。也就是说最终的安全属性等于IDAU硬件固定值和SAU软件覆盖后的综合结果。这个关系有点像你公司的门禁系统IDAU是物业规定哪些房间不让外人进SAU是管理员每个工作日临时放行其中几间房给访客用。外设侧还有单独的控制部件。GTZCGlobal TrustZone Controller是L5的总控中心往下分成两个子模块TZSC管“安全属性配置”负责给外设和一部分存储器分配安全/非安全属性TZIC管“非法访问中断”一旦有非安全侧请求撞上安全边界TZIC会记录事件并可以把它映射到指定的中断通道。你以后在调试代码时看到某个中断莫名触发先别急着怀疑定时器很可能是TZIC在背后报告了一次被拦截的非法访问。这块在培训时ST也特别强调过TZIC是排查TrustZone问题最快的一条线索学会看它的状态寄存器比瞎猜快得多。2.4 中断与调试隔离不止发生在数据路径上除了内存和外设中断也需要隔离。在L5的NVIC中每个中断源都会被标记为Secure或Non-secure。Secure中断只能由安全世界代码响应Non-secure中断只能被非安全世界代码处理。中断向量表也因此分成两张Secure vector table和Non-secure vector table分别放在各自世界的起始地址。这意味着你以前写中断服务函数的那套习惯要改一改——不是声明一个IRQHandler就能用了你得先确认这个中断源归哪个世界管。这里有个比较绕的地方如果你在Non-secure工程里手动改中断优先级把分组搞乱了很可能会影响Secure侧的响应。因为Cortex-M33的NPRNon-secure Priority Rebase机制非安全侧的优先级范围是可以被安全侧限制的。ST培训时给的建议是在SoC启动阶段由Secure侧代管整个NVIC的优先级分组配置给Non-secure侧只开放一个受控的优先级范围防止应用侧把重要中断顶掉。我实际做的时候是在Secure侧启动流程里统一设置好分组和优先级然后Non-secure侧只使用建议的优先级区间这样两边都不会出怪问题。调试隔离同样重要。在默认情况下如果一个调试器以Non-secure身份连接它是看不到Secure侧代码和存储内容的。要让调试器具备访问Secure侧的权限ST提供了一套Debug Authentication机制需要烧录者在开发阶段通过特定证书或口令完成“解锁”。这个机制到量产阶段会专门用来做固件保护它的存在让我这类习惯盯着变量窗口看的人也得改改习惯上了TrustZone以后不是所有变量你都能随便看了分清楚哪些变量属于Secure世界哪些属于Non-secure世界是刚开始调试时最需要适应的点。3. 从培训到实操跑通第一个TrustZone工程3.1 CubeMX配置划分安全与非安全资源理论聊完落地才是关键。实操的第一步是用STM32CubeMX生成工程。STM32CubeMX从V6.x开始已经支持TrustZone工程配置我在培训回来后用STM32CubeMX 6.10配合STM32CubeL5固件包搭了一套最小工程步骤大致是这样的。在CubeMX中选择STM32L552或L562型号进入“Security”面板。确保“TrustZone enabled”处于打开状态。这一选工程结构就变了Flash被切分成Secure和Non-secure两段SRAM同样被拆分。在Memory视图里用图形化拖拽的方式划分Flash和SRAM的边界。默认分区比例是Flash一半对一半SRAM也做类似划分对测试来说先用默认没问题。选好需要放在Secure侧的外设。比如我把RNG、AES放到SecureUART留到Non-secure用于打印日志。点击生成代码CubeMX会同时生成两个工程一个Secure工程和一个Non-secure工程。Non-secure工程通常会被自动关联到Secure工程下面形成父子工程关系。生成完之后有一个必须手动确认的点SAU区域配置。CubeMX会根据Flash和SRAM的划分自动生成SAU初始化的代码把非安全区域正确标出来。建议生成后打开Secure工程里的SAU初始化函数检查一遍确认Non-secure区、NSC区非安全可调用区都配置正确。NSC区非常重要它是两个世界之间唯一合法的“门”后面单独说。我自己第一次生成后对照手册查了一遍确认默认配置里把整块Non-secure Flash和SRAM都覆盖到了才敢继续往下写代码。3.2 安全的“门卫”NSC区域与veneer函数在TrustZone里Secure侧服务要暴露给Non-secure侧使用不是简单声明一下extern就行。Non-secure代码不能直接调用Secure函数它必须先跳到一个特殊区域。这个区域叫NSCNon-Secure Callable位于Secure内存中但被SAU标记为“可被非安全侧调用”NSC区域里放的是一组叫veneer跳板的小函数。你为什么需要这个跳板因为在非安全侧发出的调用请求只能先落到NSC区域再由NSC区域的SG指令闭锁安全状态跳转到真正的Secure实现。这不是ST定出来的规矩是Cortex-M33的硬件行为。所以我在写Secure侧服务时会刻意写一层独立的小函数加上编译属性让编译器在NSC区域生成对应的veneer__attribute__((cmse_nonsecure_entry)) uint32_t secure_entropy_and_generate_key(uint32_t seed) { // 真正在Secure世界执行的逻辑 return secret_generate(seed); }用GCC或者ARM Compiler构建时都要确保链接脚本和编译选项支持CMSE。具体到工程里Secure工程的链接脚本需要配置NSC区域地址范围编译器选项需要加-mcmseARM Compiler对应--cmse否则生成veneer时会报错。这条我在培训后的第一次联调中被卡了将近一个小时最后发现是IAR的工程没有启用CMSE选项白白浪费时间。所以第一次搭工程时先把编译器默认的CMSE开关找出来确认打开再去写业务代码。生成veneer后Non-secure工程可以正常调用secure_entropy_and_generate_key()但它的实际执行路径是被硬件保护过的。有一点要注意veneer函数本身要尽量短小、只做参数透传和状态切换不要把复杂业务逻辑写在里面。你在每一个需要暴露给外部世界的Secure函数上都要加cmse_nonsecure_entry编译器会替你生成跳板但设计上要保持这层“门卫”轻量尽量减少跨界调用带来的复杂度和性能影响。3.3 双工程联调Secure工程与Non-secure工程如何配合TrustZone工程的结构本质上是两个固件Secure固件和Non-secure固件它们有各自的链接地址、各自的启动文件、各自的main函数。最终烧录时Secure固件和Non-secure固件会被合并或按地址分别烧写。CPU复位后先进入Secure世界跑Secure工程的Reset_Handler和main初始化安全侧环境和NVIC的优先级管理后再跳转到Non-secure的Reset_Handler把主控权交出去。所以调试的时候有一个习惯需要纠正以前单工程调试时我们直接在main入口打断点现在打复位断点会先落在Secure工程跟着它跑几步才能到Non-secure的非安全main。我第一次联调时没意识到这个流程盯着调用栈看了半天还以为是启动配置出了问题。另一个配套的排查技巧是在Secure工程和Non-secure工程里分别加上不同颜色的串口打印固化到启动流程里能非常直观地确认两个世界各自由谁、在什么时刻接管。另外烧录地址也要特别注意。Secure固件烧到Secure Flash分区Non-secure固件烧到Non-secure Flash分区。如果我们用的是IDE一键烧录它通常会自动处理如果用命令行或第三方工具地址写错一个就会引发奇怪的启动异常。我自己习惯把两个.hex文件合并后一次烧写省去切换地址的麻烦。在培训的动手实验环节ST给的例程还提供了一个TF-M的参考实现如果你想快速验证安全侧能力直接跑它的示例工程会省掉不少配置时间。4. 常见问题与排查技巧实录4.1 一运行就HardFault八成是安全属性冲突我在培训后的测试板上跑一个简单LED闪烁程序时就遇到了典型的HardFault。现象是Non-secure代码跑着跑着突然进HardFault复位后能复现。我把TZIC/TZSC的中断打印打开后才定位到是UART的DMA请求访问了Secure侧的SRAM缓冲区。原因很简单我在Non-secure配置里把UART的DMA环形缓冲区放在了Secure SRAM区域总线主设备DMA用Non-secure属性去访问Secure地址直接被硬件拦下来。排查建议第一步先把工程里所有GPIO、UART、DMA的缓冲区和相关外设都放在Non-secure可访问区域。第二步确认MPCMemory Protection Controller已经把对应的SRAM块正确分配为Non-secure。第三步如果还崩溃去查TZIC的非法访问标志它会记录是哪位总线主设备、哪个地址触发了违规。这一步基本能覆盖绝大多数初期的HardFault问题。记住TrustZone下的HardFault往往不是堆栈溢出而是访问权限被硬件拦截方向别找错。4.2 中断进不去安全/非安全中断的配置新手常见的一个问题在Non-secure工程里使能了UART中断但串口收到数据后不进中断。排查时先检查NVIC里该中断源的安全属性。L5的每个外部中断源都有一个安全状态位如果这个位被配置成了Secure那么Non-secure侧代码即便使能了中断也没用因为中断在硬件层面就发给Secure世界去处理了。解决办法有两种。一种是把中断源划给Non-secure这样完全由Non-secure工程处理另一种是把中断源留在Secure侧但通过Secure侧代码去解析并转发。对大多数应用来说把特定外设的中断源明确归属到一个世界避免两个世界同时操作同一个外设中断就能减少很多麻烦。还有一个细节在Secure侧配置优先级分组后Non-secure侧的优先级范围受NPR限制。如果Non-secure中断优先级设置到了不允许的范围中断响应行为会变得很奇怪建议两边用统一的分组策略并在Secure侧启动时把NPR配置明确写出来。4.3 调试与烧录Debug Authentication的正确姿势调试一个带TrustZone的芯片与传统MCU不太一样。我是用ST-LINK加STM32CubeProgrammer组合做的调试。第一次连接后发现Secure侧开不了断点后面去翻培训资料才明白默认调试会话运行在Non-secure世界Secure侧镜像默认是“受保护”的想调试Secure侧代码需要先完成Debug Authentication的授权。Debug Authentication说白了就是一套认证流程通过已知的安全凭证把芯片的调试权限从受限状态解锁到可访问所有世界的状态。在开发阶段ST为每个芯片提供了调试认证相关配置窗口如果你是在自己的板子上做验证记得在量产前把调试认证的口令收好否则后面想升级Secure固件或降级保护级别都没办法。我在测试结束后用CubeProgrammer把RDP设成了Level 2然后尝试解除保护发现需要Debug Authentication口令才能取消。这个机制对量产设备来说非常重要但如果你踩到旧习惯量产时把RDP设成Level 2调试口就彻底锁死后续保修维修都需要额外维护解锁流程这个要提前规划好。4.4 性能与功耗TrustZone免费的代价最后聊一点性能实测。TrustZone开启后CPU本身不会因为多了一个“安全状态”而增加明显的性能损耗因为它是在硬件状态位层面做的切换不需要陷入额外的操作系统或管理器。我在标准测试中把同一份CoreMark代码分别放在Non-secure和Secure下跑两者分数几乎一样主频110MHz时CoreMark大概在400分上下具体数值和编译器优化相关。功耗方面TrustZone开启本身不会显著增加电流待机电流主要取决于低功耗模式和唤醒源设定。L5的低功耗表现延续了L4的水平同样是几个微安的STOP模式。不过有一点需要注意如果你的应用频繁在Secure和Non-secure之间切换调用每次切换涉及NSC跳转和上下文保护会带来额外的CPU周期开销。所以架构设计上要避免“把高频小函数都放Secure”这种反模式合理做法是把高价值的敏感操作比如密钥派生、固件校验整体放进Secure侧减少跨界调用的频率。这算是L5架构使用中一个不大不小的性能优化点。我在自己的工程里把所有加解密操作封装成几个大粒度的Secure API应用侧每次调用能完成一整段密钥交换或固件校验流程跨界次数控制在个位数性能损耗基本可以忽略。培训结束回到工位上我又把L5的参考手册“System architecture”那一章从头翻了一遍边翻边和手头的板子做对照。说实话TrustZone这套东西在上手初期确实让人别扭——它逼着你把代码、数据、外设都分家哪怕你只是点个灯也要先回答“这个灯归哪个世界管”这个问题。但熬过适应期后我是越来越喜欢这种设计方式安全从一开始就被写进系统架构里而不是最后补一层铁皮。如果你正准备入坑STM32L5我的建议是从一个最小TrustZone工程起步先跑通Secure和Non-secure两个工程再把一个小功能比如加密切到Secure侧调用你就能真正摸清这套新架构的脾性。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门