十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32H563 TrustZone下非安全代码发起Bank Swap的权限边界与实现方法

STM32H563 TrustZone下非安全代码发起Bank Swap的权限边界与实现方法 先给结论在 STM32H563 开了 TrustZoneTZEN1的前提下非安全代码能不能直接发起 bank swap答案不是简单的“能”或“不能”而是取决于一个很容易被忽略的可配置项——FLASH 控制器寄存器到底被标记成了 secure 还是 non-secure。默认情况下它属于 secure所以你在 App 侧一写 FLASH_CR基本就是 HardFault 或者 SecureFault 伺候。我最近在一个基于 STM32H563 TrustZone 的 OTA 项目里正好把这条链路完整走了一遍。这个标题问得特别实际因为“能 bank swap 吗”背后其实是两个问题一是非安全代码有没有权限操作 Flash 控制器二是就算有权限双区切换的时序和状态判断怎么做才稳。这篇文章把这两层都拆开讲清楚同时给出安全侧开放接口NSC和非安全侧直接操作寄存器两条路线适合正在做安全启动、OTA 双区升级、或者想搞清楚 H5 系列 TrustZone 访问边界的工程师参考。1. 先把概念理清TZEN、nonsecure code、bank swap1.1 为什么做 OTA 都会盯上双区交换先聊点背景。所谓 bank swap就是利用芯片内部 Flash 的双区结构把当前运行的程序放在 Bank1把新升级固件写到 Bank2然后通过硬件机制把两个 Bank 的地址映射交换让下一次复位直接从新固件启动。这个方案最核心的价值是不需要额外的外部存储也不需要 Bootloader 先把整个固件搬到 RAM 再跳转升级失败还能随时回滚到旧版本所以几乎所有没有外部 Flash 的 IoT 设备都在用。STM32H563 是 H5 系列里比较有代表性的型号Cortex-M33 内核主频能到 250MHzFlash 容量按型号有 1MB 和 2MB 之分。带 TrustZone 的 H563 在做双区 OTA 时天然会面临“安全侧代码 vs 非安全侧代码”的职责划分。通常我们把引导加载、固件校验、密钥管理放在安全侧应用逻辑放在非安全侧但 Flash 的擦写和 Bank 切换往往两边都想碰。于是问题就来了非安全代码到底能不能碰 Flash 控制器的 bank swap 功能。1.2 TrustZone 下的“看得见/看不见”边界很多人对 TrustZone 的理解停留在“代码分成安全和非安全两部分”但实际落到 Cortex-M33 上控制权是从硬件地址映射开始的。M33 通过 IDAUImplementation Defined Attribution Unit和 SAUSecurity Attribution Unit给每段地址打上安全属性标签所有访问在硬件层面就会被检查。你在非安全代码里访问一块被标记为 Secure 的地址根本到不了外设直接触发异常。这就像一栋楼里有两层门禁系统。TrustZone 是楼层的物理门禁非安全代码只有非安全楼层的门禁卡。就算门背后就是你要的东西你没权限开这个门。外设寄存器也一样STM32H563 的 Flash 控制器其实存在两套地址映射一套是 non-secure alias一套是 secure alias。但这两套 alias 对应的是同一个物理寄存器能不能从 non-secure 路径访问要看硬件里那块寄存器的“归属权”。1.3 双 Bank 结构本身不涉及安全属性再强调一句容易混淆的点Bank1 和 Bank2 的 Flash 存储区域和 TrustZone 的“安全/非安全”是两回事。Bank Swap 是 Flash 存储层的地址映射切换TrustZone 是总线访问层的安全属性检查。你可以让 Bank1 整体被标记为安全、Bank2 整体被标记为非安全也可以让两个 Bank 都被标记为非安全这取决于你 SAU/SAU 的配置。而 bank swap 本身只是把一个 Bank 的起始地址从 0x08000000 映射到另一个物理 Bank权限模型不会因此改变。2. 直接回答nonsecure 能不能换会撞什么墙2.1 默认配置下非安全代码直接写会被拦先给一个明确的判断标准。在 TZEN1 的情况下Flash 控制器寄存器FLASH_CR、FLASH_SR、FLASH_KEYR 这一组默认的安全属性是 Secure。也就是说你在非安全代码里执行类似FLASH-CR | FLASH_CR_SWAP_BANK_Msk的操作时总线访问就会被拒绝。这个拒绝不会静默丢弃而是触发异常。在调试器里最常见的表现是进入 HardFault。很多新手第一反应是去查 Flash 时序、查锁状态、查代码逻辑结果都没问题最后才发现是访问权限压根没给。如果打开了 SecureFault handler你会在 SCB-CFSR 里看到 SF 标志位明确告诉你这是一个安全属性访问错误。所以直接回答标题如果你不调整任何配置非安全代码是不能发起 bank swap 的连碰 FLASH_SR 读状态都会异常。2.2 FLASH_REG_SEC 是决定性的开关那是不是意味着非安全侧永远碰不了 Flash 控制器并不是。在 STM32H5 上Flash 寄存器区有一个可配置位决定 Flash 寄存器到底被标记为 Secure 还是 Non-secure。这个位在 Flash 安全配置项里具体看 FLASH_SECCFGR 相关寄存器。当你把 Flash 寄存器的安全属性配成 Non-secure 之后非安全代码通过 non-secure alias 地址访问 Flash 控制器就是合法的。换句话说这个问题的答案完全取决于一个可配置位。如果你在安全侧允许了非安全访问那非安全代码就可以直接解锁 Flash、设置 SWAP_BANK 位、轮询 SWAP_BANK_BSY一套操作和 TrustZone 没开的时候几乎一样。2.3 能直接访问不代表应该直接访问从硬件权限上看把 FLASH_REG_SEC 配成 0非安全侧就能直接操作 Flash。但从系统安全架构的角度看我不建议在正式项目里一上来就这么干。原因很简单一旦非安全应用代码可以直接解锁 Flash、改 Flash 选项字节、切换 Bank那整个安全启动链的可信边界就被压缩了。如果非安全侧漏洞被利用攻击者理论上可以篡改 Flash 内容安全侧再强也拦不住。这也是为什么 ST 官方在 TrustZone 相关的应用笔记里反复强调把安全敏感操作封装到安全侧通过 NSCNon-secure Callable接口暴露给非安全侧调用。非安全侧需要换 Bank 时调用一个由安全侧提供的函数函数内部完成解锁、置位、轮询、上锁整个过程非安全侧看不到也干预不了。这样代码职责清晰安全性和可维护性都更好。3. 方案A安全侧做服务、NSC 开放接口3.1 为什么这是工程上最稳的做法在正式项目里我最推荐的方式就是把 flash_bank_swap 做成一个安全侧的“服务函数”然后通过 NSC 接口让非安全侧调用。为什么要绕一圈因为这样既满足了非安全侧换 Bank 的需求又把 Flash 控制器的控制权留在安全侧。安全侧可以在换 Bank 前加校验逻辑换完后可以记录状态将来要加签名验证或者回滚策略都在一处改不用动非安全应用。从 TrustZone 编程模型看NSC 接口还有一个好处调用方向是明确的。非安全代码通过 SG 指令进入安全代码安全代码执行完毕后通过 bxns 返回。这个过程中硬件会自动处理栈切换和状态保存你不用像裸写汇编那样操心。CMSIS 和主流编译器都对 NSC 提供了很好的支持。3.2 安全侧实现代码以 GCC/Clang 环境为例安全侧定义一个函数加上cmse_nonsecure_entry属性编译器就会帮你生成安全调用入口。函数内部其实就是标准的 Flash 操作流程我们逐步拆解。#include stm32h5xx.h static void flash_unlock(void) { if (FLASH-CR FLASH_CR_LOCK_Msk) { FLASH-KEYR 0x45670123U; FLASH-KEYR 0xCDEF89ABU; } } static void flash_lock(void) { FLASH-CR | FLASH_CR_LOCK_Msk; } __attribute__((cmse_nonsecure_entry)) uint32_t secure_flash_swap_bank(void) { uint32_t sr; /* 如果 Flash 正忙直接返回错误码避免在忙碌状态下触发 swap */ if (FLASH-SR FLASH_SR_BSY_Msk) { return 0xFFU; } flash_unlock(); /* 清除可能残留的错误标志 */ if (FLASH-SR (FLASH_SR_EOP_Msk | FLASH_SR_WRPERR_Msk | FLASH_SR_PGSERR_Msk | FLASH_SR_STRBERR_Msk)) { FLASH-SR | (FLASH_SR_EOP_Msk | FLASH_SR_WRPERR_Msk | FLASH_SR_PGSERR_Msk | FLASH_SR_STRBERR_Msk); } /* 置位 SWAP_BANK硬件开始执行交换序列 */ FLASH-CR | FLASH_CR_SWAP_BANK_Msk; /* 等待硬件完成SWAP_BANK_BSY 清零表示交换完成 */ while (FLASH-SR FLASH_SR_SWAP_BANK_BSY_Msk) { /* 等待可加超时保护 */ } sr FLASH-SR; flash_lock(); return sr; }这段代码有几个细节值得说明。第一解锁用的是FLASH-KEYR这是 Flash 控制器标准的两段式解锁钥匙两个 key 分别是 0x45670123 和 0xCDEF89AB。写错一次就会触发 Flash 错误需要复位才能恢复。第二清标志位不是随便清EOP是编程结束标志WRPERR是写保护错误PGSERR是编程序列错误STRBERR是 strobe 错误。这些标志不清掉后续操作可能会一直报错。第三SWAP_BANK_BSY位必须在循环里等待不能用固定延时替代因为硬件交换时间会受到 Flash 状态影响固定延时做不好要么超时要么提前误判。3.3 非安全侧调用代码非安全侧调用 NSC 接口本质是拿到一个安全函数指针然后跳过去执行。具体地址必须由安全侧按 NSC 区域的布局分配通常在链接脚本里定义。这里的核心是非安全侧不能直接声明一个普通外部函数并调用因为那会把函数当成非安全代码来调用触发出错。#include stm32h5xx.h /* 声明的函数指针指向安全侧 NSC 区域 */ typedef uint32_t (*pfn_secure_flash_swap_bank)(void); #define SECURE_FLASH_SWAP_BANK_ADDR 0x100F0500UL int request_bank_swap(void) { pfn_secure_flash_swap_bank swap_func; /* CMSE 非安全调用要求使用非安全函数指针调用宏保证 SG 指令和返回序列正确 */ swap_func (pfn_secure_flash_swap_bank)cmse_nsfptr_create(SECURE_FLASH_SWAP_BANK_ADDR); uint32_t status swap_func(); if (status 0xFFU) { return -1; } return 0; }这里有两个坑要特别提一下。第一0x100F0500这个地址是我举例用的实际项目中 NSC 区域的地址由安全侧链接脚本和 SAU 配置共同决定。你不能随便填个地址就跳过去否则就是非法跳转硬件直接 HardFault。第二cmse_nsfptr_create宏会返回一个带 LSB 标记的非安全函数指针调用时硬件才知道“哦这是跨安全边界要走 SG 流程”。如果你漏了这个宏直接把普通地址转成函数指针来调调用会失败而且很难查。3.4 关于 HAL 库函数的提示如果你用的是 STM32CubeMX 生成的工程HAL 库里其实已经有封装好的 Flash 操作函数比如HAL_FLASH_SwapBank之类。但要注意HAL 函数默认操作的是当前代码所在的安全上下文能访问的 Flash 寄存器。在 TrustZone 开着的 H563 上如果 Flash 寄存器是 Secure 属性你在非安全侧调HAL_FLASH_SwapBank一样会异常。因此HAL 库函数不是银弹关键还是要搞清楚当前代码运行在哪个安全状态以及它访问的寄存器有没有权限。4. 方案B条件允许时非安全侧直接操作4.1 前提条件必须逐项确认虽然推荐方案A但确实有些场景会在非安全侧直接操作 Flash。比如产品团队已经定了安全边界Flash 寄存器本来就要给非安全侧管理或者项目早期调试阶段TZEN 还没正式配好。这时候非安全侧直接操作必须满足几个前提条件缺一不可。第一TZEN1 时FLASH_REG_SEC或你所用 H5 具体型号中对应的 Flash 寄存器安全属性位必须被配置为 Non-secure。这个配置通常是在安全侧的启动代码里改 Flash 选项字节完成的改完之后需要复位生效。第二RDP读保护等级不能过高特别是不能处于 Level 2 以上否则很多 Flash 操作被硬件锁死。第三你要确保自己操作的是 non-secure alias 地址而不是 secure alias 地址。非安全代码访问 secure alias 照样触发 fault。4.2 直接操作寄存器的方法当上述条件满足时非安全侧直接操作就和普通 MCU 上的 Flash 操作差不多了。不同点在于地址要用 non-secure alias。在 HAL/CMSIS 头文件里通常会同时定义FLASH和FLASH_NS这样的宏分别指向 secure alias 和 non-secret alias。#include stm32h5xx.h #define FLASH_KEY1 0x45670123UL #define FLASH_KEY2 0xCDEF89ABUL static void ns_flash_unlock(void) { if (FLASH_NS-CR FLASH_CR_LOCK_Msk) { FLASH_NS-KEYR FLASH_KEY1; FLASH_NS-KEYR FLASH_KEY2; } } static void ns_flash_lock(void) { FLASH_NS-CR | FLASH_CR_LOCK_Msk; } int ns_flash_bank_swap(void) { /* 等待 Flash 空闲 */ while (FLASH_NS-SR FLASH_SR_BSY_Msk) { /* 等待 */ } ns_flash_unlock(); /* 清除错误标志 */ FLASH_NS-SR | (FLASH_SR_EOP_Msk | FLASH_SR_WRPERR_Msk | FLASH_SR_PGSERR_Msk | FLASH_SR_STRBERR_Msk); /* 触发 bank swap */ FLASH_NS-CR | FLASH_CR_SWAP_BANK_Msk; /* 等待交换完成 */ while (FLASH_NS-SR FLASH_SR_SWAP_BANK_BSY_Msk) { /* 等待 */ } ns_flash_lock(); return 0; }这里有个很容易踩的坑有些芯片头文件默认只定义了FLASH这个宏它指向 secure alias。如果你在非安全侧写FLASH-CR编译器编译不会报错运行时就等着进 HardFault。所以代码里一定要审视自己用的是FLASH还是FLASH_NS。我有一个习惯在非安全侧写 Flash 操作时全部用FLASH_NS并且把FLASH的使用限定在安全侧文件里两边代码文件分开管理减少混淆。4.3 方案B风险的自我评估直接操作虽然代码短、调用简单但后面维护的人要清楚一件事这个接口把 Flash 控制器的整个控制权都开放给非安全侧了。一旦非安全侧代码出了问题比如解锁失败、错误擦了 Bootloader、改了不该改的选项字节安全侧是一点办法都没有的。所以方案B更适用于两类场景一是芯片根本没有使能 TrustZoneTZEN0二是应用架构明确允许非安全侧全权管理 Flash。如果都不是我建议你还是回到方案A。5. 实操中必须盯住的寄存器与时序细节5.1 bank swap 完整时序无论走方案A还是方案Bbank swap 本身的硬件时序是固定的这部分不会被 TrustZone 改变。以 STM32H563 的典型操作顺序来说完整流程大致是先保证当前代码在活动 Bank 里运行接着在非活动 Bank 完成新固件写入然后置位 SWAP_BANK 位等待 SWAP_BANK_BSY 完成最后复位让 CPU 从新的活动 Bank 启动。有一个非常容易出错的地方是“复位时机”。很多人以为置位 SWAP_BANK 之后就立刻复位结果发现有时候新固件没启动有时候 Flash 内容甚至异常。原因在于 SWAP_BANK_BSY 还没清零之前硬件还在做 Bank 地址映射的切换这时候复位会把状态机打断。正确做法是必须等到 SWAP_BANK_BSY 清零确认映射交换已经完成再执行系统复位。5.2 SWAP_BANK_BSY 期间千万别复位我在实际项目中给这条规则加粗标红了SWAP_BANK_BSY 等于 1 的窗口期内禁止任何形式的复位包括看门狗复位和外部复位引脚触发复位。看门狗这个坑尤其隐蔽因为你在代码里等待 SWAP_BANK_BSY 的时候如果喂狗不及时看门狗先咬一口MCU 就带着一个没完成的 Bank 交换复位了结果就是固件起不来甚至 Flash 内容损坏。解决办法有两个方向。一个是在执行 bank swap 前把看门狗暂停或者把喂狗动作挪到等待完成之后另一个是给等待循环加超时保护超时了就尝试恢复但恢复策略要提前设计好。我的习惯是swap 这个操作本身通常只需要几十到几百微秒所以等待循环里可以连续喂狗或者用 TI 的窗口看门狗配置一个足够覆盖整个 swap 周期的窗口。不管选哪种都要在硬件测试阶段反复验证边界情况不能靠运气。5.3 解锁/上锁与错误标志清理Flash 控制器上锁的逻辑也要说清楚。解锁之后如果长时间不执行操作或者操作完没有上锁Flash 控制器就处于“裸奔”状态任何有总线访问权限的代码都能改 Flash 配置。在 TrustZone 环境下这个问题会被放大因为安全侧和非安全侧都可能碰到这个寄存器。所以每次 flash_unlock 之后务必在函数出口执行 flash_lock。错误标志清理同样重要。很多人在 Flash 操作失败后只盯着返回值却忽略了错误标志位会一直卡着导致后续操作全部异常。常见的做法是在操作前清标志操作后读取 SR再根据 SR 判断成功还是失败。SWAP 操作结束后建议主动把 EOP 等可写 1 清零的标志位清掉这样下一次操作前读出来的状态就是干净的。5.4 代码从哪个 Bank 执行决定了你能否发现状态异常还有一个经验分享bank swap 执行完、复位之前当前代码还是跑在旧 Bank 上的。它的 Flash 映射已经被切到新 Bank 了吗没有。硬件会在复位后才让新 Bank 的代码接管 CPU。这意味着复位前的代码看到的外设状态和复位后新固件看到的外设状态不一致不能拿复位前的某个变量去推断复位后的运行状态。跨 Bank 的握手信息一定要存到备份寄存器、RTC 备份域或者 Flash 的非活动区不能只存在 RAM 里。6. 常见问题与排查记录6.1 一访问 FLASH 寄存器就 HardFault这是 TrustZone 环境下最典型的症状。排查路径按照下面的顺序走第一步确认 TZEN 到底是 1 还是 0可以在调试器里读选项字节确认第二步确认代码运行在非安全状态可以看 CONTROL 寄存器的 NPRIV 位或 SCB-NSACR 等第三步确认 Flash 寄存器安全属性配置如果配置成 Secure那非安全代码访问必然 fault第四步确认访问的是 non-secure alias 地址。这四个查完90% 的 HardFault 都能解决。如果再往下挖建议在工程里把 SecureFault 的 handler 打开。Cortex-M33 的 SecureFault 异常一旦 enable在 fault 时可以拿到更明确的信息SCB-CFSR 里的 SF 标志会告诉你哪条总线事务被拒了。之前有个项目调试同事一直盯着 HardFault_Handler 看不到有效信息后来发现是 SecureFault 没开默认升级成 HardFault导致方位完全跑偏。6.2 swap 后程序还在旧 Bank 跑如果你置位 SWAP_BANK 后程序还继续在旧 Bank 执行而且复位后也没有跳到新 Bank先不要怀疑 swap 没生效。第一步检查 DBANK 配置H563 必须处于双 Bank 模式才能执行 Bank 交换如果配置成单 BankSWAP_BANK 操作会被忽略。第二步检查当前 Bank 和目的 Bank 的启动配置有些项目在选项字节里固定了启动 Bank即使是双 Bank 模式启动地址也可能被锁定。第三步检查你写入新固件的时候是不是真的写到了非活动 Bank 的物理地址而不是写到了当前活动 Bank。6.3 回滚与确认机制怎么设计最后聊一下工程上很容易忽视的回滚。Bank swap 做完了新固件跑起来未必是好的。所以一套完整的双区 OTA 流程通常会在新固件启动后做一次“自检”然后把“固件可用”标记写入备份寄存器或 Flash 的某个固定区域。如果新固件自检不通过Bootloader 会再次执行 swap 切回旧 Bank。这个逻辑最好在 Bootloader 里就定好不要在应用代码里临时加因为谁也无法保证新固件能正确运行到加判断的那一行。我在项目里习惯把状态机分成三格等待升级态、升级完成待确认态、确认完成态。升级完成后先置为待确认应用跑起来定时自检通过后在固件头部的状态标记里写入确认完成。下次启动时 Bootloader 先看这个标记如果还是待确认且超出重试次数就自动回滚。每次 swap 后把启动次数计数器加一这样即使新固件一启动就崩也能通过计数器判断回滚条件。6.4 调试器无法连接怎么办操作 Flash 选项字节或者解锁流程写错最严重时芯片可能直接进入锁定状态SWD 都连不上。这种时候不要慌大多数 STM32 都有连接方式上的恢复手段。如果你能通过 boot 引脚进入系统 Bootloader可以尝试全擦除恢复。如果还不行看具体型号是否支持通过特定引脚组合进入调试模式。这个问题和 TrustZone 没有直接关系但开了 TZEN 之后更容易遇到因为一次非安全侧的非法访问可能把整个调试端口也锁掉。所以调试阶段我建议把 TZEN 的配置和调试口使能分开设置先跑通功能再收紧安全属性。7. 个人在实操中的一点体会走完这个流程我的体会是TrustZone 下的 Flash 操作最难的不是代码本身而是“权限边界”和“操作顺序”交织在一起出错时症状非常像普通 Bug但根因完全在安全模型层面。方案A 和方案B 没有绝对的好坏关键看你产品对安全边界的要求。如果你只是快速验证功能把 Flash 寄存器安全属性放开非安全侧直接写跑通流程再收紧如果你做的是量产产品强烈建议一开始就走 NSC 接口宁可多做一层封装不要留一个随时可能被利用的缺口。最后再分享一个调试技巧在安全侧代码里加一个调试入口把当前 Flash 寄存器安全属性和 Bank 映射状态读出来通过串口或者调试器输出。这样每次遇到 swap 失败先看状态再猜原因能省下大量时间。别问我是怎么知道的我就是靠这个入口把问题范围缩小了大半。
返回列表