关键字: SecureFault, TrustZone, SBSFU, STM32U5
1. 问题描述
有客户在使用 STM32CubeU5 包下的 SBSFU 示例代码(STM32Cube_FW_U5_V1.2.0\Projects\B-U585I-IOT02A\Applications\SBSFU),移植过程中发现程序运行不起来,如下是打印信息 :

图 1 系统置 log 信息
同时客户反馈做了些代码修改:
原先 Secure App 使用的是 SRAM3, 现在改为 SRAM1
原先 Non-Secure App 使用的是 SRAM1, 现在改为 SRAM3
如此修改后程序运行不起来了。
2. 问题分析
从客户那里拿到可以重现问题的测试工程,由于可以每次必现,所以此问题解决起来应该相对容易。
把 SBSFU_Boot, SBSFU_Appli_Secure, SBSFU_Appli_NonSecure, SBSFU_Loader_Secure, SBSFU_Loader_NonSecure 依次编译(其中SBSFU_Appli_Secure 和 SBSFU_Appli_NonSecure 这两个工程的编译优化关闭,以方便调试)并通过运行 regression.sh, SBSFU_UPDATE.sh 脚本烧录固件到 MCU 后,从打印信息中确实能看到问题重现。与客户描述的现象一模一样。
从打印信息看,程序运行 SBSFU_Boot 工程代码并有相应打印信息,但不确定是否已经跳转到 SBSFU_Appli_Secure 代码,于是使用 STM32CubeProgrammer 的 hot plug模式查看程序当前 PC 值 :

图 2 当前 PC 值
可知,当前 PC 值为 0x0C01 B486。
再分别查看 SBSFU_Appli_Secure 和 SBSFU_Appli_NonSec 的 BuildAnalyzer 窗口 :

图 3 SBSFU_Appli_Secure 的 Build Analyzer 窗口
其所占 FLASH 范围为 0x0C01 A400 起始地址的 14.12KB 代码空间,使用了 SRAM1的 192KB 空间。

图 4 SBSFU_Appli_Secure 的向量表位置
进一步查看可知 SBSFU_Appli_Secure 的向量表位置是 0x0C01 A400。
再查看下 SBSFU_Appli_NonSecure 工程的 BuildAnalyzer 窗口:

图 5 SBSFU_Appli_NonSecure 的 Build Analyzer 窗口
可知其所占 FLASH 起始于 0x0802 0400, 使用 SRAM3 这个 512KB 的空间。进一步查 SBSFU_Appli_NonSecure 工程的向量表 :

图 6 SBSFU_Appli_NonSecure 向量表位置
可知 SBSFU_Appli_NonSecure 工程的向量表 位于 0x0802 0400.
当前 PC 值为 0x0C01 B486,可知位于 SBSFU_Appli_Secure 范围内。同时,在图 2 中,当前
IPSR 值为 7. 结合编程手册 PM0264:

图 7 IPSR 定义
可知,IPSR=0x7, 表示当前出现了 SecureFault 中断。
当前暂可得知这些信息,接下来需要调试下。
由于程序从 SBSFU_Boot 跳转到 SBSFU_Appli_Secure 时,会去检查 header 相关信息,以证实其真实有效后才会跳转过去。这会影响接下来的调试。为方便调试,我们可修改选项字节让程序直接从 SBSFU_Appli_Secure 入口处开始执行。
如之前所述,SBSFU_Appli_Secure工程的向量表位于0x0C01 A400, 于是通过STM32CubeProgrammer修改 SECBOOTADD0 的值为 0x0C01 A400 :

图 8 修改 SECBOOTADD0
如此一来,MCU每次复位都是从0x0C01A400上启动了,这样方便我们调试SBSFU_Appli_Secure 代码。
接下来像调试常规 TrustZone 工程一样,联调 SBSFU_Appli_Secure, SBSFU_Appli_NonSecure:
在 SBSFU_Appli_Secure 的 main()函数处设置一个断点。经确定,程序会在这里停住。这说明程序确实有运行 SBSFU_Appli_Secure 工程,这跟预想的一致。然后在SBSFU_Appli_NonSecure 工程内的 Reset_Handle()函数内设置一个断点,经调试,程序也会在这里停住:

图 9 程序有运行到 NonSecure App
可见程序确实也有运行到 SBSFU_Appli_NonSecure。
在 SecureFault 内设置一个断点,让程序在这里停下来:

图 10 程序最终触发了 SecureFault
如上图,查看 SAU->SFSR 寄存器,可知 INVEP 被置位。在 PM0264文档中查看INVEP 的定义:

图 11 SAU->SFSR.INVEP 定义
可知访问的目标资源属性不对,进一步调试,触发此SecureFault发生在SBSFU_Appli_NonSecure 工程代码刚启动,还没运行到 main() 函数之前,在运行 Reset_Handle() 内的__PROGRAM_START() 期间出现了问题,那么此函数是做什么的呢?
它是一个 cmsis 函数,位于 cmsis_gcc.h 文件中:


如上代码所示,它就做了两件事,将位于 flash 上的数据拷贝到 SRAM3 中 (对应全局变量初始化过程)。将全部未带初始值的全局变量赋初值 0. 它们所涉及到的变量均位于SRAM3 内。因此,我们再检查下它的 MPCBB 安全属性配置:

图 12 SRAM3 对应的 MPCBB3 的安全属性配置
如上可见,SRAM3 的 MPCBB3 各个位(MPCBB3_SECCFGRx)仍然为 1(Secure), 这明显不对,NonSecure 工程用到的 SRAM 对应的属性应该为非安全才对。于是找到SBSFU_Appli_Secure 工程内的 GTZC MPCBB3 配置代:



这段配置 MPCBB3 的代码明显有些问题,我们的目标是需要将 MPCBB3 所有安全属性对应位均配置为 0,因此,修改并简化 unsecure_sram3()函数:


修改后的代码直接将 SRAM3 内所有 block 设置 NonSec 属性,再次验证代码,结果从打印信息看到程序已经完全恢复正常工作了。

图 13 Non-Secure App 正常运行
至此,Non-Secure App 已经正常运行,至少说明,Secure App 和 Non-Secure 这两个程序本身加在一块运行并无太大的问题。
由于之前修改了选项字节 SECBOOTADD0,所以现在要修改回去让整个系统重头开始运行测试下。再次运行 regression.sh 和 SBSFU_UPDATE.sh 脚本烧录固件,让程序再次从最原始位置(0x0C0 6000)开始运行:

图 14 运行 log
如上,从打印信息可以看到,程序从 boot 上启动,并最终已经跳转到 Non-Secure App 中去了,但是并没有看到线程的打印信息。
于是查看客户的 Non-Secure App 的线程相关代码。在代码中,总共创建了两个测试线程。一个线程负责每 3 秒发送一次事件,另一个线程则接收事件并打印。但明显看到两个线程并没有为其分配安全侧的栈,于是为其添加(红色部分为添加的代码):


修改代码后再次测试,结果这次修改并没有改善效果。问题依旧存在。但由于这个基于 trustzone 的 FreeRTOS,在 NS APP 侧创建任务时,为了防止资源冲突,必须利用驻守在安全侧的 FreeRTOS 内核代码为每个任务分配 stack. 这个是肯定要添加的代码。这里添加代码并没有错。接下来需要进一步联调下 Secure App 和 Non-Secure App 代码。
如下图所示,通过不复位的 attach 上去的联调方法,等进入到调试模式下后,点击下暂停按键,让程序暂停住:

图 15 触发了 ASSERT
结果发现,原来触发了断言,FreeRTOS 为 hand_evt_task 分配安全侧 stack 时,其分配的 SecureContext 安全上下文为 0,与预期值不符。顺着 xSecureContext 摸瓜上去,进一步发现程序调用 pvPortMalloc 为 hand_evt_task 任务分配安全侧的 stack 空间时返回空指针。于是接着看为什么会分配失败。最终摸瓜摸到 FreeRTOS 安全侧的 heap初始化的结果有问题:
在 prvHeapInit()函数内部:

如上图所示,对于代码 172 行,明明 uxAddress 值为 0x30002908,而pxFirstFreeBlock 值为 0x30000118,两者相减后的值赋给pxFirstFreeBlock->xBlockSize, 结果赋完值后它依旧为 0. 有点怪异! 代码运行到这里的时候,已经是运行 Secure App 的代码,当前 CPU 处于 secure 状态。它的内存对应的是SRAM1。
我修改了下代码,直接在代码中给 pxFirstFreeBlock->xBlockSize 赋值 1024. 再次运行,结果发现 pxFirstFreeBlock->xBlockSize 依旧为 0!真是太怪异了!
然后突然发现,pxFirstFreeBlock 的值是 0x30000118,它是 SRAM1 的首个block。那么 SRAM1 的各个 block 的安全属性当前又是什么状态呢?于是查看 MPCBB1:

图 16 MPCBB1 的安全属性配置
如上图所示,MPCBB1_SECCFGR0.SEC0=0, 也就是说,SRAM1 的首个 block(512个字节)为非安全属性。
但是,当前的情况下,FreeRTOS 内核正在为 hand_evt_task 任务分配安全侧的 stack. 按理就是安全的,也就是说 MPCBB1_SECCFGR0.SEC0=1 才对。那么哪里的代码配置这个 MPCBB1_SECCFGR0.SEC0=0 的呢?
通过搜索代码和在调试下监视 MPCBB1 寄存器的变化,最终发现在 Boot 工程代码中:

将这行代码改成:
GTZC_MPCBB1_S->SECCFGR[0] = 0xffffffff;
再次测试,果然程序能够恢复正常。
从上述代码可知,这里有个宏 TFM_ERROR_HANDLER_NON_SECURE 控制这个配置,它的原意是当系统产生 Error_Handler()时,代码会转去调用位于 SRAM1 上的 NonSecure APP 代码进行处理。很显然,修改后的客户代码并不需要这样处理,因此,更好的处理方式是关闭宏 TFM_ERROR_HANDLER_NON_SECURE:
同时,boot 程序在跳出去之前会清空 NonSecre App 对应的内存 SRAM1, 这里应该换成清空 SRAM3:

改完后,最终所有代码均运行正常。

图 17 最终修改完后的运行效果
至此,此问题被最终定位和解决。
3. 小结
开发 trustzone 工程并不简单,本文基于一个现实的调试案例并分享给读者,基于trustzone 的开发对于大部分已经熟悉传统 MCU 的开发都来说还是一个比较新的事物,特别是面对各种奇奇怪怪的情况,能够知道该如何去定位问题就显得是一件非常复杂的事情,希望本文这种定位 trustzone 问题的方法和思路能够供大家参考一二。