ARMV8-M TRUSTZONE的基本概念
本文翻译自CONCEPTS BEHIND THE ARMV8-M TRUSTZONE
基本概念
本文章是我在使用Nodic的新产品nRF9160之后总结而来,这个SiP产品集成无线通信功能,可以用于创建IoT节点。内部集成新的高度优化的LTE动态调制器,支持NB.Iot和LTE-M标准,可以用于传统M2M 2G网络设备的升级。
Lobaro的这种SiP技术,对我们来说是比较新的概念,和传统的一个控制器通过AT命令与调制器交互的架构不同,SiP技术下的新产品集成了带有最近安全技术的ARM Cortex-M33 CPU和LTE调制器。二者通过共享内存的方式进行交互,内部的高度耦合是的功耗可以进一步降低,也使得IoT节点的PCB大小大大降低。
nRF9160内部的Cortex-M33使用了最新的ARMv8-M架构,支持最新的ARM TrustZone
功能。接下来会着重介绍一下我对于这个功能的理解,并针对GNU ARM GCC编译器和CMSE(Cortex M Secure Extension)功能实际应用做一个简单介绍。因为现在网上对于这部分的资料还是比较少,所以希望这篇文章可以帮你更好的理解这部分内容。
虽然我使用的是nRF9160 SiP,但是下面的内容同样适用于其他一些支持ARMv8-M架构的Coretx-M23设备,原理上也不仅仅局限于GCC编译器。
CPU安全和非安全运行模式
和传统的基于线程和中断的CPU运行模式不同,ARMv8-M架构引入了安全和非安全的两种新的运行属性。系统复位之后CPU默认为Secure状态,这个时候CPU可以访问所有的存储空间和外设,这个和M3, M4一般的线程模式类似,如果基于ARMv8-M的CPU一直处于这种模式,那么实际上和之前的Cortex-M3, Cortex-M4 CPU功能上没有区别,TrustZone的特性也就完全不起作用,这种状态下开发Secure代码难度很高,因为你必须严格审查你的每一行代码,一旦你的代码出现了bug,那么你将暴露整个系统的权限。
而引入TrustZone之后,我们就可以将一部分代码,数据甚至外设隐藏在设备的一小段空间。这段空间称之为Secure区域,而其它空间相应的(隐式的)称为Non-Secure区域。这种分区包含内存,中断和外设。而分区的配置也是用户可以自行定义的。一般系统初始化的时候就会根据应用需求将分区表配置好,比如nRF9160提供的Secrue Partition Manager就给出了一个Flash内存区域划分的例子。
ARMv8-M架构的CPU会维护两套(Secure, Non-Secure)和执行程序相关的寄存器,比如堆栈指针和系统控制单元(SCB)。当前的CPU模式决定了系统使用哪一套寄存器。类似的,像是一些外设,比如SysTick也是有两套的。Secure程序可以访问Secure和Non-Secure的外设。在配置好Non-Secure堆栈指针之后,Secure模式下可以直接通过地址调用Non-Secure分区的代码,也可以运行Non-Secure固件代码。
而Non-Secure固件开发时,可以默认没有Secure固件,可以直接按照正常的软件开发。用户可以按照以往经验正常开发应用代码,只是相应的link代码的时候,相应地址一应要是Non-Secure地址空间。当CPU跳转到Non-Secure固件之后,CPU处于Non-Secure状态,这个时候CPU不能访问任何的Secure代码、数据或者外设,意外的访问Secure区域将会触发Secure中断。Non-Secure状态如果需要调用Secure函数,只能通过Secure固件提供的跳转指令从特殊空间进行跳转调用。文章的后续部分将对这部分的细节做详细介绍。而这部分的代码的具体实现则是基于包含CMSE(Cortex-M Secure Extensions)的ARM GCC工具。
Non-Secure调用区域(NSC)和Secure Gateway(SG)汇编指令
为了实现Non-Secure到Secure API的调用,我们需要在Secure Flash(一般是Flash,当然ROM或者SRAM也是可以的)定义专门的一小段跳转调用区域(NSC),这段区域包含于Secure固件。nRF9160中这部分是通过SPU外设实现的。
正如我们之前讲到的一样,Non-Secure直接调用Secure区域的函数将会触发Secure中断。而当Non-Secure通过NSC区域中定义的接口进行调用的时候,系统将会正常跳转到Secure状态。这部分NSC区域实际是一系列的跳转指令,每次调用的第一条指令都是SG(Secure Gate)汇编指令。SG指令之后,程序就可以根据后续指令进行跳转,这个时候跳转的目标地址可以是Secure空间的地址(不限于NSC空间)。这个调用过程相当于分了两步跳转,CPU首先跳转到NSC区域,然后跳转到S区域。至于为啥不直接在S空间的函数开始放SG指令,大家可以参考这篇文章,后续可能也会翻译一下。
Secure固件中定义Non-Secure调用函数
虽然前面原理上表述起来有些复杂,但是实际ARMv8-M编译器可以直接隐式的完成上述的两步跳转。为了让编译器正确区分这些可跳转函数,我们要为这些函数添加特别是属性,比如如下的一个例子:
// some c file of secure firmware project defining veneer gateway functions
// must compiled with -mcmse gcc flag (!)
#include "arm_cmse.h"
__attribute__((cmse_nonsecure_entry)) void ControlCriticalIO(){
// do some critical things
}
在编译代码的时候,如果添加了-mcmse
参数,编译器就会自动为上面的代码生成SG指令:
// GCC COMPILER flags used during secure side building
arm-none-eabi-gcc -o secureFirmware.elf -mcmse [...]
编译器在链接(link)的时候会将函数体放到Secure空间的.text
区域,这个和我们之前的编译是相同的,除此之外,编译器还会生成相应的SG和跳转指令,这部分会放到一个特殊的区域.gnu.sgstubs
,对于其他编译器,名称可能略有不同。
这些位于NSC区域的小函数也称之为跳板函数(veneer function, SG stups), 因为他们本身很小,只有跳转的功能。Secure固件的Linker File必须将这部分代码放到NSC区域,并且这部分区域的内容在CPU跳转到Non-Secure区域之前一定要初始化。
// Linkerscript Section for TrustZone Secure Gateway veneers
.gnu.sgstubs : ALIGN (32)
{
. = ALIGN(32);
_start_sg = .;
*(.gnu.sgstubs*)
. = ALIGN(32);
_end_sg = .;
} > FLASH-REGION-WITH-NSC-ENABLED
Non-Secure固件调用Secure函数
一般来说Secure固件的开发和下载和Non-Secure固件是独立的,二者各有自己的镜像。Non-Secure调用Secure固件函数的时候一定要有一个描述固件接口的头文件(.h), 另外Non-Secure固件还需要知道这些SG指令(或者说Veneer Entry)的具体地址,这个是通过链接(linking)一个特殊的库文件(比如 CMSE_importLib.o),这个库文件会包含调用的地址信息。库文件是Secure固件链接的时候产生的:
// GCC LINKER flags used during SECURE side linking
arm-none-eabi-gcc -Xlinker --cmse-implib -Xlinker --out-implib=CMSE_importLib.o -Xlinker --sort-section=alignment [...]
上述命令实际还可选一个–in-implib=CMSE_importLib.o
参数,引入这个参数可以保证已有的函数入口地址保持不变。这样的好处是可以引入新的Secure调用函数,而保持原有的Non-Secure可以正常工作。
通过引入.h
文件和CMSE_importLib.o
,Non-Secure固件就可以直接调用前面定义的ControlCriticalIO()
函数了。
// GCC LINKER flags used during non-secure side linking
arm-none-eabi-gcc -o non-secureFirmware.elf CMSE_importLib.o [...]
Secure固件调用Non-Secure(回调)函数
到这里,我们已经可以实现一个Secure和Non-Secure相互隔离的固件,二者之间通过一小段Gateway/Veneer函数交互。现在我们已经可以实现Non-Secure到Secure方向的函数调用。
而对于反方向,也就是Secure函数调用Non-Secure函数,实际上是直接可用的,一旦Non-Secure函数人入口地址通过某舟方式传到了Secure代码,比如通过定义一个函数Non-Secure回调函数的GateWay函数:
// some c file of secure firmware project defining veneer gateway functions
// must compiled with -mcmse gcc flag (!)
#include "arm_cmse.h"
typedef void (*funcptr_ns) (void) __attribute__((cmse_nonsecure_call));
void ControlCriticalIO(funcptr_ns callback_fn) __attribute__((cmse_nonsecure_entry)){
funcptr_ns cb = callback_fn; // save volatile pointer from non-secure code
// check if given pointer to non-secure memory is actually non-secure as expected
cb = cmse_check_address_range(cb, sizeof(cb), CMSE_NONSECURE);
if (cb != 0) {
/* do some critical things e.g. use other secure functions */
cb(); // invoke non-secure call back function
}else {
// do nothing if pointer is incorrect
}
}
上面函数中的cmse_nonsecure_call
也是必不可少的,它可以在CPU跳转回到Non-Secure之前让编译器插入清空Non-Secure通用寄存器的代码,如果不做这个操作会有潜在的安全隐患。另外编译器还会清空函数指针地址的低位,这样CPU就会从Secure模式跳转到Non-Secure模式。
因为参数中的Non-Secure回调函数指针会被认为是一个volatile
类型的变量,也就是说它的值可能在Non-Secure中断中发生变化,所以我们要对这个函数指针进行必要的复制操作(cb),另外,调用函数之前,还要对函数地址和大小进行检查,确保函数在Non-Secure空间,如果函数指向了一个Secure空间,那么将会产生很大的安全隐患。
经验总结
在开发两种固件的时候,针对Secure部分,一定要注意veneer函数的链接空间和空间的各种分区,Non-Secure则一定要和这些分区保持一致。如果Non-Secure Callable函数位于*.a
这样的库文件,那么链接的时候一定要加whole-archive
参数,只有这样CMSE import lib才会包含内部的veneer函数。
相关资料
- A few intricacies of writing ARMv8-M Secure code – ARM Limited
- SAM-L11-Security-ReferenceGuide-AN-DS70005365A.pdf – Microchip Technology
- Trustzone with ARMv8-m and the NXP lpc55s69-evk – Erich Styger
- CM33 freeRTOS Example
最后更新于 2019-11-06 15:03:48 并被添加「ARM TrustZone firmware」标签,已有 6750 位童鞋阅读过。
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。