打印
[信息]

STM32L5 入门课程系列(三) TrustZone环境下新的用户编程模型

[复制链接]
984|12
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
powerantone|  楼主 | 2023-11-27 15:34 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

欢迎大家继续关注STM32L5入门课程(三):Trustzone环境下新的用户编程模型。


通过上一期的介绍,大家应该理解了STM32L5是如何把“隔离”的概念和措施,从内核延伸出来,部署到全片系统。这一期,我们结合一个具体的GPIO trustzone例程,对前两期介绍的知识点,理论结合实际来深入体会一下。




GPIO TrustZone


我们要运行和分析的第一个STM32L5上的trustzone例程是GPIO toggle。它位于STM32CubeL5固件包里,Nucleo板子下、pre-build好的GPIO例程目录下。


从这个最基本的简单例程,我们可以体会,在前面讲到过的TZ软件开发模型。即一个工程空间里,至少包含两个工程项目,一个运行在安全世界,一个运行在非安全世界。它们有各自的源文件,包括应用文件,中断处理函数it.c文件,各自的linker链接文件。至于启动汇编文件startup.s,里面是中断向量表,和Reset handler,可以两个工程复用,因为在向量表和reset处理上,这个例程没有什么区别。如果实际应用中,需要有不同的reset处理,可以分开两个启动汇编文件。


我们借用这个最基本的trustzone例程来体会:一个基于TrustZone平台的项目,从安全世界启动,初始化和配置系统安全属性,切换到非安全世界运行,再调用安全世界代码的过程。






到工程目录下,我们以IAR为例,打开GPIO IOToggle TrustZone工程空间文件:project.eww

工程空间由上、下两个项目组成,项目名称分别是_S和_NS的尾缀。

>> 安全世界的工程包含的C文件,对应文件夹Secure中的src目录下所有C文件

>> 非安全世界的工程包含的C文件,对应文件夹NonSecure中的src目录下所有C文件

这些文件,熟悉STM32的开发者都不会陌生,除了一个文件,可能是在ST的例程第一次看到,就是在Secure文件夹的Src目录下的,secure_nsc.c。NSC就是前面讲的non secure callerble的意思。顾名思义,大家可以猜到,那些安全世界准备暴露出来给非安全世界调用的函数,就在这个文件里定义。安全项目编译链接后,会在Secure_nsclib文件夹下,生成secure_nsclib.o。该库文件和对应的头文件,会被包含到非安全世界的项目中。这样,非安全世界,才知道有哪些安全世界的API可以供他们调用。


在进一步分析项目结构和实现细节之前,现在我们先把例程跑起来~




首先,我们通过选项字节对芯片做初始化。我们使用STM32CubeProgrammer来把STM32L5的选项字节做如下配置:TZEN和DBANK都置1。然后把L5的片上512K flash的前半部分设置成安全区域,把后半部分设置成非安全区域。


如果你手上的Nucleo-L5板子,之前没有设置过TZ,就是说,在User Configuration的Tab里,TZEN没有被checked,你是看不到后面和User Configuration Tab并列的Secure Area 1和2的tab的。所以你要先把TZEN打上勾,同时DBANK也打上勾,然后点击Apply。先把TZEN给apply到系统里。然后第二次连接,就可以看到Secure Area1和2个Tab了。


如果你之前TZEN是使能了的,也在Secure Area的Tab里做过设置,而DBANK是没有使能的。这种情况下,去设置DBANK是无法成功的,需要先把secure memory或者HDP区域都disable掉,然后才能改变DBANK的设置。


芯片的选项字节配成我这个样子:TZEN、DBANK都是1;Secure Area1里,安全区域,从page编号0,到page编号0x7F,共128个page,256K字节,即整个bank1。后面256K字节,即bank2,设置成不安全区域,因此只要把安全范围起始地址设置成大于结束地址即可。


接下来,我们对工程空间里的 两个项目,分别进行编译和下载。注意,要先编译安全项目,然后再编译非安全项目。

先编译安全世界的工程。先不要问为什么,就照做,后面会讲为什么。


再编译非安全世界的工程。如果你实在想知道,可以试一试先编译非安全世界的工程。


好,两个项目都编译链接成功。接下来我们烧录两个项目的image,由于是烧录到不同的地址空间,因此烧写的顺序没有特别的要求。我习惯先烧NS的项目,使用Download  download active来下载;然后切换到S项目,同样使用Download -> download active来下载。然后按下板子的复位键,可以看到:黄灯和蓝灯,以不同的频率在闪烁。


大家可能觉得这是一个平淡无奇的例程演示,就是两个LED闪烁,但是它是基于TrustZone,和以往的LED闪烁的工程完全不一样。接下来,我们就庖丁解牛,把背后的从原理到实现,都一一梳理一遍




原理和实现


首先面对的第一个问题是,在这个GPIO闪烁的例程中,安全世界和非安全世界是如何划分的?

第一个问题中的第一个问题,片上512K用户flash和256K ram,是如何划分Secure区域,NSC区域,以及NS区域?


上一期讲L5片上系统对隔离的实现时,我们知道CPU的视角,看谁安全看谁不安全,是有IDAU和SAU决定。刚刚复位时,SAU的寄存器复位值,使得CPU看出去,整个4G地址空间都是安全的,因此无论IDAU的实现是如何划分,经过IDAU和SAU二者共同定义的结果,就是整个4G空间都是安全的。


但是IDAU还有一个非常重要的作用,上一期讲了,就是给Code区、SRAM区、Peripheral区,定义了别名,严格讲是定义了“别名地址空间”。这里的Code、SRAM、Peripheral区,不是芯片厂家实现的512K flash,256K ram的区域,而是ARM定义的memory map里的大区域。即图中最左边一栏,Code区域是0x0到0x1FFF,FFFF,整个512M空间;SRAM是0x2000 0000开始,到0x4000 0000的512M空间;Peripheral是接下去0x4000 0000到0x6000 0000的512M空间。


别名地址空间,大家其实不是第一次在ARM Cortex-M芯片里碰到了。以前CortexM3的时候,SRAM区域,和外设区域的bit-banding就是使用别名地址,别名地址空间的一个word,对应实际空间的一个bit,从而方便的对位进行操作。

这里的IDAU别名也一样,但不是一个bit对应一个word,而是一一对应。


IDAU对Code区域,0x0800 0000开始的64M空间,定义了别名区对应0x0C00 0000开始的64M空间。意思就是说,从0x0800 0000去访问一个字节,和从0x0C00 0000去访问的一个字节,物理上都是我们512K 片上flash的第一个字节。CPU去访问0x0800 0000的时候,从SAU/IDAU看来,它是去访问不安全的区域,此时无论CPU本身的状态是安全还是不安全的,SAU/IDAU都会被这条transaction降级成非安全的transaction。但是,如果CPU去访问0x0C00 0000的时候,从SAU/IDAU看来,它就是去访问安全区域。那么如果是指令访问,无论此时CPU的安全状态,SAU/IDAU都会放行这条访问,并且标志该transaction是安全的transaction。如果是数据访问,如果此时CPU处于非安全状态,那么SAU/IDAU认为它要去访问一个安全区域的数据,就会block掉这条访问企图。大家可以再回过头,看一下上一期结束时三个图解,展示的CPU访问规则。






CPU怎么看,是由IDAU以及上电后可配置的SAU决定的。


实际物理的512M片上flash,哪些区域是安全,哪些是不安全的,由flash controller来配置,因为flash memory是TZ aware的外设。我们刚才在做准备工作的时候,通过选项字节,把512Kflash的前半部分,即bank1配置成安全的,后半部分,bank2配置成非安全的。


对于取指访问,第一级检查,SAU/IDAU都会放行,出来的transaction安全性取决于SAU/IDAU看到的区域的安全性。为了CPU的视角看出去的目标地址的安全性,和物理设备实际配置的安全性一致,对低256K空间里的安全flash访问,CPU应该走0x0c000 0000开始的地址;对高256K空间里的非安全flash访问,CPU应该走0x0800 0000开始的地址。即0x0804,0000。


我们看一下两个项目的linker文件,是如何规定自己的工程项目,代码区域的地址范围。

主要关心三个区域,intvec、ROM_region、RAM_region

>> 我们把两个linker文件里,两个项目各自关键的三个区域的地址范围,放在这个表格里。

>> 像刚才说的,安全世界的代码,放在实际地址0开始的地方,而CPU要从0x0C00 0000去寻址,方能和flash自身这部分区域的安全属性匹配得上。就是0x0C00 0000开始的256K空间,从0x0C00 0000到0x0C03 FFFF。

>> 非安全世界的代码,放在实际地址0x4 0000开始的地方,CPU要从0x0800 0000去寻址,才能和这块不安全区域的属性一致,因此是非安全项目,ROM区域的范围是0x0804 0000开始的256K空间,到0x0807 FFFF结束。


安全区域,需要预留一小部分,作为NSC区域,这里例子,使用了安全世界里最后8K空间。


而SRAM,也是划分了两部分,从偏移地址0到0x1 7FFF,即SRAM的低96K空间,作为安全区域,分配给安全世界的工程,放置堆栈;而剩下160K的高偏移量空间,留给非安全世界的项目使用。


使用特权

评论回复
沙发
powerantone|  楼主 | 2023-11-27 15:35 | 只看该作者



我们来看运行时不同阶段,CPU对不同区域的访问情形。同样一个物理地址,在不同时候,CPU使用不同的别名地址去访问,看到的情形是不一样的。这个完全是TrustZone平台特有的现象,大家在以前传统STM32平台上调试,从未碰到过的情况。


首先,芯片刚刚复位时,程序还没有跑起来。这个时候IDAU作为静态配置,已经生效;而SAU的作用效果,取决于它的寄存器复位值。那么此时,从内核角度看出来,所有的4G地址空间都是安全的,包括所有的flash区域,RAM区域。


>>在刚刚复位时,选项字节对flash区域的安全属性配置也生效了:低256K是安全的,高256K是不安全的。一个物理区域,在IDAU的作用下,还有它的别名区,因此我们看到这里两条绿色的,两条红色。两条绿色的,是同一块flash区域的两个地址空间,红色的也是一个意思。

>> RAM区域的安全配置,也是由寄存器控制,来自GTZC模块的MPCBB,它的复位状态,是所有SRAM区域都是安全的。


表格里最后一列,是前面看的GPIO toggle两个工程各自linker文件里指定的代码区域,绿色字体是安全项目里定义的区域;红色字体是非安全项目定义的区域。其中,粗体字,是各自的linker文件里直接指明的地址,而对应的斜体字,是它的别名区域。比如说,0x0c00,0000到0x0c03,0000的256K空间,是在安全工程里定义的放置安全工程向量表,代码,以及供非安全世界调用的代码API所在区域。和它同享一块物理地址的,0x0800 0000到0x0803,0000地址空间,按照以往的经验,看到的应该是同样的内容





我们来看运行时不同阶段,CPU对不同区域的访问情形。同样一个物理地址,在不同时候,CPU使用不同的别名地址去访问,看到的情形是不一样的。这个完全是TrustZone平台特有的现象,大家在以前传统STM32平台上调试,从未碰到过的情况。


首先,芯片刚刚复位时,程序还没有跑起来。这个时候IDAU作为静态配置,已经生效;而SAU的作用效果,取决于它的寄存器复位值。那么此时,从内核角度看出来,所有的4G地址空间都是安全的,包括所有的flash区域,RAM区域。


>>在刚刚复位时,选项字节对flash区域的安全属性配置也生效了:低256K是安全的,高256K是不安全的。一个物理区域,在IDAU的作用下,还有它的别名区,因此我们看到这里两条绿色的,两条红色。两条绿色的,是同一块flash区域的两个地址空间,红色的也是一个意思。

>> RAM区域的安全配置,也是由寄存器控制,来自GTZC模块的MPCBB,它的复位状态,是所有SRAM区域都是安全的。


表格里最后一列,是前面看的GPIO toggle两个工程各自linker文件里指定的代码区域,绿色字体是安全项目里定义的区域;红色字体是非安全项目定义的区域。其中,粗体字,是各自的linker文件里直接指明的地址,而对应的斜体字,是它的别名区域。比如说,0x0c00,0000到0x0c03,0000的256K空间,是在安全工程里定义的放置安全工程向量表,代码,以及供非安全世界调用的代码API所在区域。和它同享一块物理地址的,0x0800 0000到0x0803,0000地址空间,按照以往的经验,看到的应该是同样的内容





我们实际来看一下:在刚才的GPIO toggle工程空间里,把安全工程,设置为active project,然后用IAR连接一下,在复位后停下来,看看flash高地址和低地址两块区域,在各自默认的地址空间,和对应的别名空间,CPU看到的情况。


可以看到,0800和0c00,内容都一样,就是安全工程的代码(看一下右边汇编窗口里的地址);寄存器SP和PC,就是从安全工程的向量表里取出来的两个值。而0804,0c04看到的就全部是0。为什么会这样呢?不是说好,既然别名空间,都是同一块物理地址吗?


原因就是,上一集讲到的,使能了TrustZone安全扩展的STM32L5上,CPU数据访问的访问规则。

第一,我们通过调试器去看,相当于CPU当前处于安全状态;

第二,我们去看memory窗口,汇编窗口,属于数据访问。


因此,无论我们要看哪里的内容,transaction都是S的;而Flash自身的配置,如果和transaction属性不一致,就不会访问成功。因此只有和transaction同为绿色的flash的区域,才能看到内容。






我们接着往下运行,当代码开始运行起来,配置了SAU后,从CPU看出去的区域,安全属性有了变化。它在4G地址空间默认的安全区域里,分别在Flash和SRAM挖出了两块非安全区域,这是给非安全工程放代码和堆栈的地方,然后在安全区域里还额外设置了一小块NSC区域。这样CPU的视角看我们的flash就和刚才不一样了。再结合flash自身的安全设置,可以想象,第一行,绿色对绿色,属性匹配,可以访问成功;第二行,红色对红色,属性匹配,也可以访问成功;第三行,同样绿色对绿色,属性匹配,可以访问成功;第四行,绿色对红色,属性不匹配,应该看不到内容。


SRAM的四个区域,也是一样的。





我们单步运行程序,在执行完SAU_Setup之后停下来,看一下flash的四个区域,和sram的四个区域,验证一下刚才的判断。

flash的四个区域,前三个都符合访问规则了,因此前三个地址看出去,都能看到内容。





SRAM区域本身的设置,要代码运行到安全工程里,调用GTZC的MPC配置后才能完成。也是配置低地址96K是安全的,给安全工程用于堆栈空间;高160K是不安全的,给非安全工程用于堆栈空间。配置好之后,可以想象,RAM区域,前三行的地址都可以正确读出内容。我们来验证一下:





单步运行程序,或者直接把断点打在安全工程main.c的main函数里,GTZC_init函数之后,然后运行到断点停下来。

RAM的前三个地址空间,符合trustzone的访问规则,可以看到正确内容。




使用特权

评论回复
板凳
powerantone|  楼主 | 2023-11-27 15:46 | 只看该作者



现在,我们按照程序的运行顺序,理一下运行逻辑。系统复位后,从Secure的工程开始运行。复位时,CPU看出去,看到全部flash和sram都是安全区域。CPU处于安全状态。


【1】开始执行初始化任务;

【2】首先设置SAU,重塑CPU的世界观,不是所有区域都是安全的,0x0804开始的256K是非安全的,那里应该放非安全工程的代码;0x2001 8000开始的96K是非安全的,那里应该放非安全工程的堆栈。

【3】选项字节的配置让物理Flash的下半部分,具有非安全属性,和CPU视角一致;代码执行,让SRAM的后半部

【4】分配到非安全世界;GPIO

【5】本来都是默认安全的,代码仅保留PC.7是安全的,驱动LED1,其他GPIO引进都分到非安全世界去,其中PB.7驱动LED2。

【6】默认所有中断发生后,都是target到安全世界,即去安全世界里查阅中断向量表,取出中断ISR地址,如果需要,可以把某些中断retarget到非安全世界里。retarget的操作,只能在安全世界执行。

【7】CM33内核有两个systick,在S和NS世界独立运行,中断也是各有一个。现在先使能安全世界的systick,产生1ms定时。

【8】使能GTZC中断,这个中断线,默认是target在安全世界的。

【9】最后安全世界的初始化全部完成,跳到非安全世界执行。





和以前一样,复位向量ResetHandler里先执行SysteInit。


TZ_SAU_Setup是SystemInit里新增的一个操作。它作为内联函数,在《partition_stm32l552xx.h》中实现。主要有四部分功能组成:


首先是设置SAU区域。本来SAU默认是4G地址空间都是安全的,用户需要在这里根据应用,至少挖出3个区域来重新设置,就像这个例程里面,两个NS区域,分别给非安全工程的代码和堆栈,一个NSC给安全工程的被外调用的函数入口。如果要修改的话,不是改函数体里面,在这个样本实现里,拉到文件的最上面,在这里修改宏定义,region起始地址,结尾地址,region的属性。默认不改就是S,0就是NS,1就是NSC。


第二部分,设置系统控制快SCB里和低功耗,中断相关的属性。内核的低功耗,复位,是仅能被安全代码使能,还是安全/非安全代码都可以。还包括上上集讲内核时,提到的是否要把retarget到安全世界的中断向量,优先级都集体抬高,是否要把Bus fault,NMI异常retarget到非安全世界,同时把HardFault优先级提到比NMI异常的优先级还高。在这个gpio toggle例程里,我们都没有用到这些属性,因此都是使用默认设置。


第三部分是关于FPU浮点单元的设置,我们这个gpio toggle例程中没有用到浮点,暂时不去管它,使用默认设置。


第四部分,很重要,如果用户想把某些中断retarget到非安全世界,就在这里配置。改这四个value中的一个或某个。当然如果不在这里修改,也可以后面使用NVIC的API修改,但是一定要是在安全世界里才能执行。这个gpio toggle例程里也没有用到这个功能,但是下一集,我们会用到它。





继续安全项目里的安全属性初始化。GTZC模块,可以配置内容SRAM,外部flash,AHB,APB外设的安全属性。这个例子,我们是配置SRAM的前96K部分为安全,后160K部分为非安全。使用相同的HAL层API:GTZC_MPCBB_ConfigMem,配置区域由第一个参数里指明,SRAM1和SRAM2。其实,根据这个函数的名字,大家可能已经猜到,它是由STM32CubeMX自动生成的,我们确实可以通过CUbeMX在图形界面里配置,下一集我们会演示这个功能。


接下来,在GPIO配置里,更改PB.7原来的安全属性,从安全配置成不安全,它将被非安全工程代码控制,去翻转LED2的亮灭,




这不是一条普通的函数指针,因为有了CMSE_NS_CALL关键字的加持,编译器才会有。


第二大段之前,先看一下IAR窗口,调试一下,看汇编;


>> 执行BLXNS之前 vs. 之后,寄存器上下文


编译器产生的汇编语句,把general register都清零,只有R4有值,但是包含的是NS世界的信息;SP是当前S世界的堆栈,LR此时是调用返回后S世界下一句的地址。R4还是刚才的值,未变;SP自动切换到NS世界的堆栈;LR被硬件刷成0xF,都未暴露S世界的信息







现在,我们跳到非安全世界开始执行了,通过刚才看到的“CMSE_NS_CALL”这个关键字修饰的函数指针,以绝对地址调用的方式跳了过来。先跳到的是非安全工程的向量表里reset handler entry里包含的地址,执行最开始的初始化,结构和安全工程里差不多,也是调用SystemInit,只是这里的SystemInit没有做太多事情,直接跳到非安全工程的main函数里。


在main函数一来,就是调用安全世界的代码。这是怎么实现的呢?

【1】首先安全世界要先定义一些可供非安全世界调用的API,

【2】然后把它的入口放在安全世界里的NSC区域。当然这些都是编译器帮你去做的,只要在定义这些对外函数时,使用编译器的一些关键字。

【3】NS的代码,先跳到NSC区域,执行第一条安全指令SG,把CPU切换到安全状态,然后就可以执行紧随SG指令后面的,跳转到真正安全函数定义的跳转指令了。当然这些也都是编译器会拆解成合适的指令来实现。


可以看到,NS世界调用S的代码时,是知道API函数名字的,不是通过指向绝对地址的指针来调用。


那么NS世界是如何知道它可以调用的S世界的函数名字的呢?








同样也是通过编译器以及IDE的帮忙。


安全项目里有一个特殊的C文件,secure_nsc.c,里面定义了Secure_registerCallback;这个函数在定义时,使用了编译器关键字“CMSE_NS_ENTRY”,那么链接器会把该函数的入口地址连同SG指令,一起放到安全工程定义的NSC区域。同时,在安全项目里,指定把secure_nsc.c生成库文件secure_nsc.o,然后导入到非安全世界里。非安全世界再把secure_nsc.h头文件包含到项目里,它就知道自己可以调用哪些安全世界暴露出来的函数API接口了。






我们来看一下运行时代码是如何从非安全世界,跳到安全世界的。注意观察:跳过去时,编译器做了哪些安排?跳回来时,编译器做了哪些事情。


观察点:简单的是:NS世界的通用寄存器,LR,都带了过去;复杂的是:跳转几次,

NS的起点,先到NS的veneer;再到S的NSC区域//SG,再到S的函数实际地方。执行完成后,返回到NS:相同的是要清零用到的通用寄存器(这里是R0、R1、R2),由于NS的LR还有效,直接BXNS LR,即可返回。


小结一下,函数调用引起的从NS跳到S过程中,编译器无需做现场清零工作,共享的通用寄存器内容可以带过去。进入安全世界执行的第一条指令,一定是SG,而且该指令一定是在NSC区域中。


从S世界返回到NS世界,编译器会产生指令来清零现场,以确保安全世界里执行的上下文,不会泄露到非安全世界,最后通过BXNS指令跳转回来。




我们来看一下NS工程的业务逻辑。首先一来,它通过安全世界API的Secure_RegisterCallback,把SecureFault_Callback和SecureError_Callback注册到安全世界里。这两个函数,实际是定义在非安全世界里的。这样当系统异常SecureFault,或者用户中断GTZC发生时,由于全部默认是target到安全世界的,因此会到安全工程里的it.c里,执行对应的ISR。由于注册了非安全世界的异常和错误处理函数,安全中断ISR中,可通过函数指针直接调用这些定义在非安全世界的回调函数。





接下去,非安全工程的初始化还包括:使能NS世界里的systick,也是产生1ms定时;配置GPIO的PB.7,驱动LED2;使能Cache。这个Cache也是属于Securable的外设,默认处于非安全状态,如果想把它配置成安全状态的外设,需要在安全工程的初始化那里,和配置SRAM的安全和非安全区域一样,通过GTZC来配置。





借这个GPIO toggle的例子,我们体会一下上上集介绍CM33内核时,关于异常和中断的retarget概念。

胶片中,左边这个表格,就是来自上上集的课件。系统异常里,有些是铁定target在安全世界的,比如绿色原点标志的Reset/HardFautl/SecureFault;有些异常,是两边各有一套,也很好理解,比如Systick异常,systick本身就是在安全世界和非安全世界各有一个硬件,一旦被使能后,就各自运行起来,到达各自配置的reload值,会触发各自systick的溢出中断。没有道理说,安全世界的systick倒计时到了,触发的中断去非安全世界处理。因此,systick的异常是各有一套。还有一些异常,和用户中断一样,不是banked的,而是通过寄存器来配置,要么target在安全世界,要么target到非安全世界。这些寄存器复位值都是0,意味着所有的可retarget的异常和中断,都是默认在安全世界去处理。结合gpio这个例程,一共根据应用定制实现了3个ISR,其中2个是systick,一个是SecureFault。我们去看看两个工程下各自的it.c。


NS的it.c,只实现了banked的那5个ISR;

关于这个基于TrustZone技术的gpio toggle的例程,就讲解完毕。我们从现成的例程空间讲起,先编译连接,然后下载,体会了TZ应用由两个工程组成。然后按照启动,运行的时间顺序,分别解析了从安全工程开始的初始化,配置,到非安全工程的业务运行,在此过程中,体会了两个世界是如何互相调用另一个世界的代码。


下一期,我设计了一个更加完备的小例程,一方面,我们会从CubeMX入手开始配置,体会CubeMX是如何支持TZ应用的;另一方面,它会覆盖前两集讲到的所有重点的理论知识,比这个gpio toggle的小例子更全面一些。敬请大家持续关注。谢谢观看。



使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

418

主题

1539

帖子

4

粉丝