ARTICLE DETAIL

资讯详情

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

TrustZone实战:基于Cortex-M33的安全隔离按键点灯例程解析

TrustZone实战:基于Cortex-M33的安全隔离按键点灯例程解析 1. 一个按键点灯例程为什么要大动干戈谈TrustZone1.1 普通点灯和TrustZone点灯的本质差异点灯这件事在嵌入式圈子里都快被写烂了。从51到STM32从寄存器到HAL库新手第一课几乎都是按键控制LED。可当你拿到一颗带TrustZone的MCU打算照老套路点灯时会发现第一个亮红灯的不是LED而是你的工程配置——代码要分安全世界和非安全世界外设要标安全属性连中断都得想清楚归谁管。LAT1483这个带TrustZone的按键点灯工程示例其实就是把这些新规矩用最小工程讲了一遍。传统单片机上点灯本质是读IO电平翻转IO电平两件事中间加一层延迟去抖就完事了。整个系统只有一个信任域CPU可以访问任何地址外设寄存器也全部敞开。这在纯裸机Demo里没有任何问题但一旦设备联网、跑RTOS、接第三方协议栈风险就来了只要应用代码里有一个漏洞攻击者拿到非安全态的代码执行权限后整颗芯片的寄存器、内存、密钥、固件都等于裸奔。TrustZone在Armv8-M架构上做的事情是把系统硬性切成两个世界。安全世界Secure和非安全世界Non-Secure两者有独立的执行状态、独立的栈指针、独立的异常向量表最关键的是——非安全代码在物理上就无法访问被标记为安全的地址区域。这种隔离不是软件层面的约定而是总线层面的硬性拦截一旦访问违规直接触发异常。LAT1483这个例程选的场景非常讨巧按键点灯。它足够简单简单到你能一眼看明白安全边界画在哪但它又完整覆盖了TrustZone开发的三个核心环节——外设的安全属性划分、中断的安全归属、安全世界与非安全世界之间的函数调用。换句话说把这份例程吃透你再去移植复杂的加密存储、安全OTA、防抄板方案思路就是同一套。1.2 LAT1483例程的隔离设计按键归安全LED归非安全拿到这份工程后我第一件事是找它的安全划分说明。这份例程的设计思路很朴素但非常典型按键所在的GPIO被配置为安全外设LED所在的GPIO被配置为非安全外设。为什么按键要放在安全侧因为按键在这里扮演的是受信任输入的角色。如果按键事件可以被非安全代码随意读取、伪造、屏蔽那么安全侧基于按键做出的任何决策都不可信。放在真实场景里这就好比一个支付终端的确认键——你可以让应用层去刷新界面、播放音效但用户是否按下了确认键这个事实本身必须由安全侧来认定。所以例程把按键GPIO划给安全世界按键中断也做成安全中断非安全代码碰不到它。LED放在非安全侧理由也直白LED本身只是状态指示不需要受保护放在非安全侧反而能演示非安全世界可以正常驱动自己的外设这条通路。这样整个例程就形成了一条完整的信任链路安全侧读取按键并确认事件通过一个安全调用接口通知非安全侧非安全侧收到事件后翻转LED。这个链路单独拎出来看很简单但你已经能感受到TrustZone项目的核心方法论先把数据按可信程度分级再按等级部署到不同世界。按键状态是可信数据LED状态是不可信输出两边之间只留一个受控的通信口。1.3 看这份例程之前你最好有这些底子如果你之前只在STM32F103、ESP8266这类单片机上点过灯直接上手LAT1483可能会在第一个工程配置界面就卡住。不过也不用慌需要补的知识点其实就三个。第一得知道Cortex-M33和Cortex-M0/M3/M4的差异。带TrustZone的MCU基本都是M33内核Armv8-M架构它比M3/M4多了一整套安全扩展指令包括SAU安全属性单元、CMSECortex-M安全扩展、SG指令等。你不需要一开始就把每条指令背下来但至少要知道这些机制的存在。第二得习惯一个芯片两个工程的开发模式。安全工程负责安全侧代码、veneer表、安全中断非安全工程负责应用逻辑两个工程编译后一个烧到安全Flash区一个烧到非安全Flash区。这和你以前一个工程编译出一个hex就完事完全不同。第三得对中断控制器有一点深入了解。TrustZone引入了中断安全属性的概念一个外设中断可以被标记为安全中断或非安全中断两者的使能、挂起、屏蔽是隔离开的。以前写NVIC_EnableIRQ一个函数搞定的事现在需要多问一句这个中断归哪个世界管。另外如果你用过带TrustZone的MCU开发板调试器比如DAPLink、J-Link最好确认一下你的调试器版本是否支持M33内核调试以及是否支持同时加载安全工程和非安全工程的符号表。这些准备工作做好了后面踩坑会少一半。2. 工程骨架的秘密安全/非安全两个工程怎么合成一个固件2.1 两个独立工程的编译与链接到底在分离什么很多第一次接触TrustZone的工程师会问一个芯片里跑两套代码那不就是把两个hex拼在一起烧进去吗表面看确实是这样但内部细节比拼要多得多。LAT1483的工程文件分成了Secure和Non-Secure两个独立工程这背后至少有三层原因。第一层是编译期的强制隔离安全代码和非安全代码使用的编译选项不同安全侧支持的某些指令和属性非安全侧不应该出现。第二层是链接期的地址绑定两个工程各自使用不同的Flash和RAM区间通过分散加载文件把地址固定好。第三层是安全审查的角度拆成两个工程后审计人员可以单独审查安全侧的每一行代码而非安全侧代码无论怎么写、写多少都不会影响安全边界的完整性。实际编译时安全工程会生成一个veneer表这是TrustZone的关键机制之一。非安全代码想调用安全函数不能被编译器直接生成一条BL指令跳过去——因为在跳转瞬间CPU会从非安全状态切换到安全状态这中间必须经过一个由编译器自动生成的门卫也就是veneer。veneer里包含一条SG指令配合硬件完成安全状态切换。链接脚本里必须为veneer表预留专门区域否则安全侧的函数无法被非安全侧安全调用。以我手头这颗Cortex-M33 MCU为例安全工程分散加载定义大致类似下面这样具体基址按芯片手册调整LR_SECURE 0x08000000 0x00040000 { ER_SECURE 0 { *(RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_SECURE 0x20000000 0x00010000 { .ANY (RW ZI) } }非安全工程的起始地址则要安排在安全区后面比如LR_NONSECURE 0x08040000 0x00040000 { ER_NONSECURE 0 { *(RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_NONSECURE 0x20010000 0x00010000 { .ANY (RW ZI) } }两个工程的RO段、RW段互不重叠这是TrustZone工程能否跑起来的前提。曾经有人图省事把两个工程都链接到同一块Flash区间结果烧录后M33跑得莫名其妙有时候连启动跳转都完不成后来查了一圈才发现是非安全工程的向量表把安全工程的向量表覆盖了。2.2 内存和外设如何按安全属性切分内存和外设的安全属性切分是TrustZone工程中最核心的地图问题。CPU取址、读数据、访问外设时硬件会同步判断当前访问属于安全还是非安全状态再和目标地址的安全属性做比对不一致就抛异常。内存的安全属性由SAUSecurity Attribution Unit和IDAUImplementation Defined Attribution Unit共同决定。IDAU是芯片厂商出厂时固定的SAU是软件可配置的。例程里在系统初始化阶段会有一段SAU配置代码把Flash和RAM分成安全区、非安全区。我这里给出一段示意代码读者在自己芯片上改地址参数即可void TZ_Config_SAU(void) { SAU-CTRL 0; /* 配置期间关闭SAU */ SAU-RNR 0; SAU-RBAR 0x0C000000; /* 非安全Flash起始 */ SAU-RLAR (0x0DFFFFFF ~1) | 1; /* 非安全Flash结束使能 */ SAU-RNR 1; SAU-RBAR 0x0E000000; /* 非安全SRAM起始 */ SAU-RLAR (0x0E0FFFFF ~1) | 1; SAU-CTRL 1; /* 使能SAU */ }外设的安全属性通常由芯片厂商提供的安全配置寄存器决定不同厂家命名不同有的叫SECCFG有的叫AON有的叫GTZC。机制都是同一套每个外设基地址对应一个安全属性位置安全就是安全外设置非安全就是非安全外设。LAT1483例程里按键GPIO外设的对应安全位被置为SecureLED GPIO外设被置为Non-Secure。这张表我建议每个做TrustZone项目的人都画一份资源安全属性用途Flash 安全区Secure安全固件、密钥、veneer表Flash 非安全区Non-Secure应用逻辑、RTOS、协议栈SRAM 安全区Secure安全栈、按键状态、事件标志SRAM 非安全区Non-Secure应用堆栈、公共变量按键GPIOSecure受信任输入LED GPIONon-Secure状态输出按键中断Secure中断非安全侧无法干扰这种表格不只是在写文档时用调试出问题的时候它就是排查地图。有一次我遇到LED点不亮的情况查了半天IO初始化、时钟配置都没问题最后回头看这张表发现LED所在外设不小心被配成了Secure属性非安全侧代码操作它自然无效。2.3 安全侧启动和跳崖到非安全侧TrustZone的启动流程和普通单片机完全不同。芯片上电后首先进入安全世界此时M33以安全状态执行安全工程的Reset_Handler。在Reset_Handler里要做的事按顺序大概是配置SAU、配置外设安全属性、初始化安全侧时钟和GPIO、设置中断的安全归属、最后跳转到非安全世界的Reset_Handler。这个跳转不是简单的函数调用因为普通函数调用会保留安全状态的调用栈而跳转完成后非安全世界需要彻底接管CPU。正确做法是用一个带cmse_nonsecure_call属性的非安全函数指针来调用目标地址这个指针的最低位被置1表示这是一个非安全调用硬件看到这个标志后会在跳转时同步切换到非安全状态。跳转代码示意如下typedef void (*nsfunc)(void) __attribute__((cmse_nonsecure_call)); #define NONSECURE_FLASH_BASE 0x08040000u void Jump_To_NonSecure(void) { nsfunc jump_to_ns; uint32_t ns_reset_addr *(uint32_t *)(NONSECURE_FLASH_BASE 4); uint32_t ns_sp_addr *(uint32_t *)(NONSECURE_FLASH_BASE); __set_MSP(ns_sp_addr); /* 先设好非安全侧栈顶 */ jump_to_ns cmse_nsfptr_create((nsfunc)ns_reset_addr); jump_to_ns(); /* 跳转后进入非安全世界 */ }这一步我特别提醒跳转前必须确认非安全向量表已经放在正确的起始地址而且MSP要设置成非安全侧的栈顶。如果跳转时用的还是安全侧的栈那么非安全代码一运行就会因为栈指针安全属性不匹配立刻触发异常。你自己写代码时最好在跳转前加一个状态指示灯这样调试器观察变量就能快速定位是根本没过跳转还是跳转后立刻挂了。3. 按键部分中断安全配置比你想的更关键3.1 电路先稳独立按键的上拉与去抖软件写得再好按键电路设计疏忽了照样白搭。TrustZone只是负责保护输入可信但如果按键本身硬件抖动就大、电平逻辑又模糊安全侧读到的数据依然不可信。所以先花几十秒把电路确认清楚。独立按键最常用的接法是按键一端接IO另一端接地IO内部上拉。按键未按下时IO读到高电平按下时接地被拉低。这个接法最稳定而且不需要外部上拉电阻适合大多数MCU。如果你的按键放在嘈杂环境建议在IO对地并联一个100nF电容形成一个简单的RC低通滤波能显著减少毛刺。还有一点按键到MCU的走线尽量短别绕过大功率器件否则静电和耦合噪声很容易让GPIO状态异常。有人会把按键接成下拉模式按键接VCCIO内部下拉。不是不行但要注意这种接法按下时IO被拉高松开时依赖内部下拉把电平拉低而内部下拉电阻一般偏弱容易受干扰。我实测下来还是按键接地上拉更稳。另外提一句如果计划做矩阵键盘按键数量超过8个时矩阵方案确实省IO但矩阵扫描天然带串键问题需要在扫描算法里做处理。LAT1483是做单按键点灯直接用独立按键就好别在这个例程里强行上矩阵。3.2 轮询与中断在安全模型下的取舍传统点灯例程里读取按键最常见的方式就是轮询主循环里反复读GPIO检测到电平时延一段时间再做一次确认然后翻转LED。这个写法在裸机Demo里没有任何问题但在TrustZone工程里轮询会带出一个很微妙的安全问题如果这个按键GPIO是非安全外设那么非安全代码随时可以读取、甚至伪造按键状态。假如你想让长按按键触发一个安全操作而按键GPIO是非安全的攻击者只需要拿到非安全代码执行权限就能直接在寄存器层面模拟一个按键信号绕过物理按键。所以LAT1483把按键中断配成了安全中断从硬件层面杜绝了非安全侧的干扰。选择轮询还是中断在TrustZone下的实际判断标准很简单。如果按键只是用来控制LED亮灭这种公开功能不涉及任何安全状态轮询也行。但如果按键事件会影响到安全逻辑比如确认支付、导出密钥、恢复出厂设置那就必须用安全中断让事件处理完全发生在安全世界内部。这个选择本质上是在回答一个问题这个按键输入到底值不值得被保护。3.3 让按键中断成为安全中断的具体配置要让按键中断成为安全中断需要配置两个层级的安全属性外设层和中断控制器层。第一步把按键GPIO所在外设标记为安全外设这一步在芯片的安全配置寄存器里完成对应前面讲的外设安全属性位。第二步在NVIC嵌套向量中断控制器里把这个中断源的目标安全属性设为Secure具体来说就是操作中断目标非安全寄存器通常叫ITNS0/ITNS1把对应bit清零代表安全中断、置1代表非安全中断。用一个伪代码描述整个配置流程void TZ_Config_SecureButtonInterrupt(void) { /* 1. 开启按键GPIO外设时钟外设本身被配为Secure */ CLOCK_EnableClock(BTN_GPIO_CLK); /* 2. 配置按键引脚为输入模式带上拉 */ GPIO_SetPinDir(BTN_PORT, BTN_PIN, kGPIO_DigitalInput); GPIO_SetPinPull(BTN_PORT, BTN_PIN, kGPIO_PullUp); /* 3. 把该外设中断目标属性配置为Secure */ NVIC_SetTarget(BTN_IRQn, 0); /* 0表示Secure1表示Non-Secure */ /* 4. 外部中断触发条件下降沿按键按下瞬间 */ GPIO_SetPinIntMode(BTN_PORT, BTN_PIN, kGPIO_IntFallingEdge); /* 5. 使能外部中断和NVIC */ GPIO_EnablePinInt(BTN_PORT, BTN_PIN); NVIC_ClearPendingIRQ(BTN_IRQn); NVIC_EnableIRQ(BTN_IRQn); }注意第3步特别关键。如果你漏掉这一步按键中断默认可能是非安全中断也就是说非安全侧可以直接使能、屏蔽、操作这个中断。而一旦配置为安全中断NVIC里对应的非安全使能位区域会变成只读非安全代码无论怎么写都没用这在调试时是一个很直观的验证手段非安全侧调用NVIC_EnableIRQ(BTN_IRQn)再去读使能位发现完全没变化。安全中断的ISR要写在安全工程里编译后放到安全Flash区。中断向量表也分安全/非安全两组安全中断的向量必须在安全向量表里不能写到非安全向量表去。3.4 去抖逻辑为什么要放在安全侧按键去抖是老生常谈的话题。机械按键在按下和松开的瞬间触点会反弹持续时间几毫秒到几十毫秒不等直接读IO很容易读到一次按下当成多次触发。传统做法是在检测到电平变化后延时10到20毫秒再确认一次两次读数一致才认定按键有效。在LAT1483这个工程里去抖逻辑放在了安全侧这背后有条件反射式的安全考虑去抖本质上是事件有效性确认。既然按键状态是可信输入那么对它的去抖、滤波、防连击处理都必须在安全世界内完成防止非安全代码绕过滤波直接拿到原始的、不可靠的电平变化。这就像门卫在接待访客时不但要确认访客身份还要在登记表上做完整记录一样不能把登记这件事外包给访客自己。具体做法上如果按键是普通GPIO中断可以在安全侧ISR里开一个安全定时器延时20毫秒后再采样一次。如果按键连接的是带数字滤波功能的外设比如带有输入滤波的IO控制器那么配置滤波参数本身也应该由安全侧完成。还有一点要注意防连击的计数变量记得放在安全SRAM里别放在非安全区否则攻击者改掉计数变量就能绕过连击保护。4. 安全调用与LED点灯隔离边界上的数据通路4.1 非安全到安全要走veneer安全到非安全直接调TrustZone工程里有一段不成文交通规则非安全代码不能直接跳转到安全函数必须经过编译器生成的veneer表而安全代码可以直接调用非安全函数。为什么方向不对等核心原因是硬件在跳转时的安全状态切换机制。非安全代码想要进入安全世界CPU必须执行一条SG指令来触发状态切换这条指令通常就放在veneer表的第一条位置。编译器在编译安全工程时会自动为所有标记了cmse_nonsecure_entry属性的函数生成一个veneer条目并把函数入口地址重定向到veneer表对应位置。非安全代码调用这个地址就等于先过了一道门禁之后才进入真正的安全函数主体。而安全代码调用非安全函数CPU本来就可以从高特权状态访问低特权目标直接调用即可不需要额外的状态切换指令。但这并不意味着可以随便调安全侧代码仍然要对非安全函数指针做有效性检查理由稍后细说。4.2 一个可编译的代码示例安全侧读键非安全侧翻转LED把整个交互链路写出来LAT1483的核心逻辑就透明了。安全工程里定义了一个导出函数非安全侧通过它获取按键状态/* 安全工程secure_btn_service.c */ #include tz_service.h /* 记录上一次按键事件是否被消费 */ static volatile uint32_t s_btn_event; /* 安全侧按键中断ISR */ void BTN_IRQHandler(void) { if (GPIO_GetPinIntFlag(BTN_PORT, BTN_PIN)) { GPIO_ClearPinIntFlag(BTN_PORT, BTN_PIN); /* 10ms后再次确认简单去抖 */ s_btn_event 1; } } /* 导出给非安全侧调用的接口编译器自动生成veneer */ __attribute__((cmse_nonsecure_entry)) uint32_t SEC_GetBtnEvent(void) { uint32_t evt s_btn_event; s_btn_event 0; /* 读取后清零 */ return evt; }非安全工程这边应用主循环定时轮询安全接口拿到事件后翻转LED/* 非安全工程app_main.c */ #include tz_service.h extern uint32_t SEC_GetBtnEvent(void); /* 实际是veneer入口 */ void App_MainLoop(void) { if (SEC_GetBtnEvent() ! 0u) { GPIO_TogglePin(LED_PORT, LED_PIN); } }这段代码跑通后你会看到现象每次按下按键LED翻转一次和普通按键点灯例程效果一模一样。但背后已经发生了完整的世界切换——按键事件在安全侧产生通过安全调用接口跨世界传递到非安全侧非安全侧在自己的外设上完成输出。这就是TrustZone工程的最小闭环。有一个细节值得反复品s_btn_event这个变量放在安全SRAM里非安全侧无论怎么读写自己的内存都碰不到它。安全侧把它封装成一个只读并自动清零的服务这种模式非常适合处理敏感状态。4.3 事件传递与参数的安检习惯参数在跨世界传递时有个非常容易忽略的安全坑非安全代码传进来的指针真的指向非安全内存吗如果安全函数接收了一个指针参数而没有校验这个指针是否指向受信任区域攻击者就可以构造一个指向安全内存的地址骗安全函数去读写它这等于在安全边界上开了一个洞。ARM提供了一系列检查函数比如cmse_check_address_range安全侧在解引用任何非安全侧传入的指针之前都应该做一轮区域检查。养成这个习惯后你会发现TrustZone编程的思维方式开始转变不再默认所有输入都可信而是每条边界上的数据流都要过一遍安检。__attribute__((cmse_nonsecure_entry)) uint32_t SEC_StoreData(const uint32_t *ns_data, uint32_t len) { /* 检查指针落在非安全可访问区域 */ if (cmse_check_address_range((void *)ns_data, len, CMSE_NONSECURE) NULL) { return 0; /* 非法指针直接拒绝 */ } /* 拷贝数据到安全侧缓冲区 */ memcpy(s_secure_buf, ns_data, len); return 1; }另外事件传递的清零语义也值得养成习惯。安全侧把事件标志交给非安全侧时要么像上面的例子那样读后自动清零要么用递增计数器的方式让非安全侧通过对比前后两次计数差值来识别新事件。计数器方案在中断频繁时更可靠不容易丢失事件。4.4 引脚级安全按键和LED共用同一个GPIO端口时怎么办LAT1483例程如果用的是两个独立的GPIO端口按键端口配安全、LED端口配非安全那事情很清爽。但实际项目里经常遇到一个尴尬情况按键和LED刚好落在同一个GPIO端口的不同引脚上而这个GPIO外设整体被配置成了某个统一属性。遇到这种情况先查看芯片手册是否支持引脚级安全属性配置。有些MCU的GPIO可以做到同一端口内不同引脚分别配置安全/非安全属性在初始化代码里对每个引脚单独设置安全位即可。如果芯片不支持引脚级配置那就要么换引脚要么重新设计外设划分——把某个外设整体划给安全侧或非安全侧。还有一种思路是把LED控制改成通过复用功能连接到其他外设比如用定时器PWM输出控制LED亮度再把这个定时器划为非安全这样按键GPIO端口整体保持安全两边也不冲突。这个问题的本质是在TrustZone项目里外设规划比普通项目早一步。最好在画原理图、分配引脚阶段就把关键输入、关键状态和普通IO分开到不同的GPIO端口避免后期软件绕来绕去。5. 我实测的三种验证方法证明隔离不是摆设5.1 测试一非安全侧直接读按键寄存器触发SecureFault写完这个工程并跑通正常点灯逻辑之后我建议做一个反向测试专门确认隔离没有失效。做法是在非安全侧故意访问按键GPIO的寄存器比如直接读它的数据输入寄存器地址/* 非安全侧故意读安全外设 */ volatile uint32_t *secure_btn_reg (volatile uint32_t *)0x40000000; /* 示例地址实际以芯片手册为准 */ volatile uint32_t val *secure_btn_reg;在Cortex-M33上这行代码执行后会触发SecureFault程序进入异常处理。此时我到调试器里查看故障状态寄存器能清晰地看到SecureFault异常标志被置位。如果访问的是非安全外设同样的代码则能正常读回数据。通过这个对比能直观确认外设安全属性是否真正生效。这个测试唯一的注意点是真别把系统搞挂建议在测试代码段周围做好标记测试完立刻删除或用条件编译屏蔽掉别留在正式固件里。另外SecureFault的异常处理函数里最好加一个空循环或故障标志方便调试器停下来观察。5.2 测试二非安全侧尝试打开安全中断完全无效第二个反向测试更轻量不需要触发异常。我在非安全工程里写了一段代码尝试使能按键对应的安全中断/* 非安全侧尝试操作安全中断 */ NVIC_ClearPendingIRQ(BTN_IRQn); NVIC_EnableIRQ(BTN_IRQn);执行完这两行后再读NVIC中该中断的使能位你会发现它纹丝不动。非安全侧对安全中断的控制位操作被硬件直接忽略就像拿着一张普通门禁卡去刷高保密门禁一样没有权限就是没有权限。这个测试在调试器里看寄存器值变化非常直观适合用来跟同事或者客户演示TrustZone的隔离效果。5.3 测试三端到端按键点灯配合调试器确认运行模式正向的功能测试就是正常按键点灯但我们要用调试器多确认一个细节按键中断发生时CPU到底运行在安全模式还是非安全模式。在Cortex-M33上你可以通过CONTROL寄存器的某个状态位或者通过调试器的CPU状态视图来观察当前模式。我在安全中断ISR里加了一个断点按下按键后中断触发、程序停住确认ISR确实运行在Secure模式之后在非安全侧读取事件标志后再停一次确认此时已经回到Non-Secure模式。这样一轮下来三个测试互相印证普通功能正常、外设隔离有效、中断归属正确。TrustZone的隔离不再停留在口头上而是有了切实的实测证据。调试器选择方面我用的是支持M33的DAPLinkIDE里可以同时加载安全工程和非安全工程的符号表。平时跑非安全侧代码时我建议把调试焦点放在非安全工程上遇到SecureFault再切到安全工程看故障位置。两个工程的源码混着调试一开始确实有点乱多试几次就顺手了。6. 移植到自己的项目工具链、启动细节与常见坑6.1 编译器和链接选项要跟上TrustZone不是在原有代码上加个宏开关就能支持的特性你的工具链必须认识Armv8-M的安全扩展指令和CMSE属性。用ARM编译器的话要使用支持CMSE的版本并且安全工程要开启--cmse选项不同IDE叫法不同用GCC的话则要使用支持-mcmse选项的版本一般arm-none-eabi-gcc 10以上版本都支持。工程里那些__attribute__((cmse_nonsecure_entry))、__attribute__((cmse_nonsecure_call))属性只有编译器在CMSE模式下才会正确处理并生成veneer表。如果你拿到一个TrustZone工程编译设置里没开CMSE那么即使代码看起来一模一样链接时也可能报找不到veneer段或者生成的二进制根本没有安全状态切换能力。链接脚本里要显式保留veneer段。ARM Compiler的veneer段名通常是Veneer$$CMSEGCC环境下也有对应的段处理方式。别小看这一项我最初移植例程时链接脚本里漏掉veneer段定义结果非安全工程调用安全接口时链接器报出各种奇怪的未定义符号错误排查了很久才发现是linker文件缺了这一段。6.2 SecureFault查不到来源时的排查路径TrustZone工程调试中最让人头疼的问题之一就是SecureFault触发后现场犹如一堵高墙——你不清楚是代码访问了不该访问的内存还是外设摸了个不该摸的寄存器又或者只是栈指针的安全属性配错了。我总结了一条排查路径遇到SecureFault后按顺序做。第一步先记录故障状态寄存器的值Cortex-M33的System Control Block里有SecureFault状态位在调试器内存窗口直接看SCB-CFSR和SCB-SFARSFAR会记录触发故障的地址。第二步根据SFAR地址判断访问目标如果是安全外设区域但当前是Non-Secure状态基本就是外设安全属性配置有误。第三步如果SFAR没有有效值硬件没抓到地址重点查栈指针确认当前用的栈是安全SRAM还是非安全SRAM以及跳转前有没有把MSP设置到正确区域。第四步再看当前PC指向哪有时候故障不是出在错误访问的指令上而是出在veneer跳转的边界上。沿着这条路径排查绝大多数SecureFault都能在半小时内定位。最怕的是上来就乱猜改两行代码重新烧录那种碰运气式调试在TrustZone工程里会非常浪费时间。6.3 中断向量表的地址与安全属性非安全工程的向量表不能放在Flash的最开头因为最开头已经被安全工程的向量表占用了。非安全向量表一般放到非安全Flash区起始位置同时在非安全侧代码的初始化阶段将VTOR向量表偏移寄存器重定位到非安全向量表地址。这个步骤如果漏掉现象非常迷惑非安全工程的主函数能跑但一旦发生非安全中断CPU跑去安全向量表里找一个不存在的向量整个系统直接挂掉。我在调试一个移植例程时就踩过这个坑LED点灯功能正常但只要一开一个定时器中断程序立刻HardFault。后来看了启动代码发现VTOR没有设置非安全中断向量全部指到了安全Flash区域。设置方法很简单在非安全侧启动代码里加一行SCB-VTOR NONSECURE_VECTOR_TABLE_BASE;但要特别注意这里写入的地址必须是对应非安全内存区域的地址也就是在安全属性映射中标记为Non-Secure的那块区域。如果写成了安全地址即便数值正确中断来临时CPU依然会因为向量表的安全属性不匹配而无法正确取指。还有一个容易忽略的点非安全侧如果要使用SVC、PendSV、SysTick这类系统异常需要在非安全向量表里为它们单独配置向量。RTOS跑在非安全侧时PendSV和SysTick的归属尤其要提前确认好。6.4 从按键点灯换到真实业务时的边界设计思路LAT1483只是起点。把按键换成一个I2C温度传感器、把LED换成一个电机驱动器工程结构几乎不需要变——安全侧负责采集传感器数据通过安全调用接口给非安全侧非安全侧根据数据做业务逻辑并驱动执行器。这套安全输入、受控传输、非安全输出的模式可以复制到非常多场景。但有几个边界设计原则我想重点强调。第一安全侧尽量只做可信数据的最小处理别把复杂业务逻辑塞进安全世界安全代码越少越容易审计攻击面也越小。第二安全侧与非安全侧的交互接口要精简最好抽象成几个纯函数GetStatus、SetConfig、HandleEvent而不是暴露大量细节函数。接口越少边界越容易守住。第三跨世界传递的数据在安全侧出口处就做一次校验和封装永远不要相信非安全侧说我的数据没问题。我个人在实际项目中的体会是TrustZone真正难的不是配置那几条寄存器而是改变思考习惯——从所有代码都是自家人变成边界外的代码默认不可信。LAT1483这种小例程之所以值得反复做几遍不是因为它能点亮一个LED而是因为它帮你把这个安全思维模式练成了肌肉记忆。等哪天你拿到真正需要保护密钥、固件、用户数据的项目时这套思维就是你的第一道防线。
返回列表