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编译器。

Cortex-M33 CORE ARM

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空间,那么将会产生很大的安全隐患。

cm33-coreregister

经验总结

在开发两种固件的时候,针对Secure部分,一定要注意veneer函数的链接空间和空间的各种分区,Non-Secure则一定要和这些分区保持一致。如果Non-Secure Callable函数位于*.a这样的库文件,那么链接的时候一定要加whole-archive参数,只有这样CMSE import lib才会包含内部的veneer函数。

相关资料


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

相关文章

发表新评论