6. Cortex-M Security Extensions (CMSE)¶
The tiarmclang compiler tools support the Cortex-M Security Extensions (CMSE) for the Armv8-M architecture, specifically the Arm Cortex-M33 processor.
Documentation on how to incorporate the use of the CMSE security extensions into an application is provided in the TrustZone(R) technology for Armv8-M Architecture.
A specification of the security mechanisms supported in the compiler tools can be found in the Armv8_M Security Extensions: Requirements on Development Tools - Engineering Specification.
This section of the user guide provides a tour through the CMSE security extension mechanisms that are supported in the compiler tools with particular attention to how the compiler tools are used to facilitate:
Accessing code in secure memory from code in non-secure memory, and
Calling code in non-secure memory from code in secure memory
6.1. Memory Partitions¶
As discussed in Section 3 of the TrustZone(R) technology for Armv8-M Architecture document, in an application that makes use of the security extensions, the memory space accessible to the Cortex-M33 processor is divided into Secure and Non-secure memory regions.
6.1.1. Secure Memory Region¶
The Secure Memory Region contains two partitions:
Secure
Memory in the Secure partition of the secure memory region contains code, data, and peripherals that are only accessible from secure software or secure masters.
Non-secure Callable
The Non-secure Callable partition of the secure memory region is the only area of target memory that may contain Secure Gateway (SG) instructions that facilitate transitions from Non-secure code state to Secure code state. The table of linker-generated secure gateway veneers that is allocated to the Non-secure Callable partition constitutes the only means whereby a function defined in the Non-secure Memory Region can gain entry to a function defined in the Secure partition.
Each secure gateway veneer in the table begins with an SG instruction that changes the processor state from Non-secure to Secure, followed by a branch to the definition of the secure function being called. Thus, the actual address is only known to code that resides in the Secure Memory Region.
6.1.2. Non-secure Memory Region¶
Memory in the Non-secure Memory Region contains code, data, and peripherals that are accessible from all software running on the Arm processor. Code that resides in non-secure memory may only access code and data that is defined in non-secure memory. Code defined in non-secure memory may only call a function defined in secure memory via the secure gateway veneer that is associated with that secure function.
6.2. Enabling CMSE Support¶
The Cortex-M Security Extensions (CMSE) are only available when building an application to be run on the Arm Cortex-M33 processor. Code that makes use of CMSE support mechanisms must #include the <arm_cmse.h> header file prior to the use of any CMSE intrinsics in a compilation unit.
To enable CMSE support attributes and pre-defined macro symbols in the tiarmclang compiler, the following options should be used:
-mcpu=cortex-m33 -mcmse
6.3. Example Application¶
To explore how different aspects of the CMSE support mechanisms work in the tiarmclang tool chain, consider a simple example application where:
Secure code on the Arm processor provides 3 functions:
set_event_handler() - registers the address of an event handler function that is to be called when an event occurs.
wait_on_event() - polls a secure peripheral for the occurrence of an event, and when the event occurs, will call the event handler function that is currently registered.
default_event_handler() - in the case where a custom event handler function has not been registered, the default_event_handler() function will get called when an event occurs
The application running in non-secure memory relies on secure code to monitor a secure peripheral that is used to trigger an action to be performed by the non-secure application.
6.3.1. Defining Non-secure Entry Functions¶
An non-secure entry function in this context refers to a function defined in secure memory that can be called from non-secure code via a secure gateway veneer. Such a function must be annotated with a cmse_nonsecure_entry attribute.
For example, the definition of set_event_handler() could be written as follows:
#include <arm_cmse.h>
typedef void __attribute__((cmse_nonsecure_call)) nsfunc(void);
extern int receive_signal(void);
extern void default_event_handler(void);
nsfunc *callback_fcn = (nsfunc *)default_event_handler;
__attribute__((cmse_nonsecure_entry)) void set_event_handler(nsfunc *fp)
{
if (fp)
callback_fcn = cmse_nsfptr_create(fp);
else
callback_fcn = (nsfunc *)default_event_handler;
}
__attribute__((cmse_nonsecure_entry)) void wait_on_event()
{
while (!receive_signal()) ;
if (cmse_is_nsfptr(callback_fcn))
callback_fcn();
else
((void (*)(void))callback_fcn)();
}
Compiling the source file with tiarmclang as follows:
%> tiarmclang -mcpu=cortex-m33 -mcmse -S secure_code.c
Yields the following compiler-generated assembly definition of the set_event_handler() function:
@ Definition of set_event_handler function
.section .text.set_event_handler,"ax",%progbits
.hidden set_event_handler @ -- Begin function
.globl set_event_handler
.p2align 1
.type set_event_handler,%function
.code 16 @ @set_event_handler
.thumb_func
.globl __acle_se_set_event_handler
.type __acle_se_set_event_handler,%function
__acle_se_set_event_handler:
set_event_handler:
.pad #4
sub sp, #4
str r0, [sp]
ldr r0, [sp]
cbz r0, .LBB0_2
b .LBB0_1
.LBB0_1: @ %if.then
ldr r0, [sp]
movw r1, :lower16:callback_fcn
movt r1, :upper16:callback_fcn
str r0, [r1]
b .LBB0_3
.LBB0_2:
movw r1, :lower16:callback_fcn
movt r1, :upper16:callback_fcn
movw r0, :lower16:default_event_handler
movt r0, :upper16:default_event_handler
str r0, [r1]
b .LBB0_3
.LBB0_3: @ %if.end
add sp, #4
mrs r12, control
tst.w r12, #8
beq .LBB0_5
vmov d0, lr, lr
vmov d1, lr, lr
vmov d2, lr, lr
vmov d3, lr, lr
vmov d4, lr, lr
vmov d5, lr, lr
vmov d6, lr, lr
vmov d7, lr, lr
vmrs r12, fpscr
bic r12, r12, #159
bic r12, r12, #4026531840
vmsr fpscr, r12
.LBB0_5: @ %if.end
mov r0, lr
mov r1, lr
mov r2, lr
mov r3, lr
mov r12, lr
msr apsr_nzcvqg, lr
bxns lr
The application of the cmse_nonsecure_entry attribute to the definition of set_default_handler() has a few notable impacts on the above compiler-generated code:
In addition to the normal set_event_handler label to mark the start of the function, the compiler also defines __acle_se_set_event_handler at the same address. When the program is linked, the label set_even_handler will get redefined to the address of the first instruction in the secure gateway veneer that is generated by the linker for set_event_handler(). The definition of the __acle_se_event_handler label will remain at the location of the actual function’s first instruction. See Building a Secure Image and Creating an Import Library for more details about what happens at link-time.
The compiler generates code to clear the contents of caller-saved registers (including FPU registers) except those that hold the result value and the return address of the entry function.
The compiler generates code to clear registers and flags that have undefined values at the return of a procedure, according to the Arm procedure call standard.
The compiler generates code to save and restore callee-saved registers as dictated by the Arm procedure call standard.
The compiler generates a BXNS instruction to return to its caller. If the function was called from a non-secure function, then the BXNS instruction will change the state of the processor from Secure to Non-secure before returning to the non-secure caller function.
6.3.2. Calling Non-secure Functions¶
A function defined in non-secure memory may only be called from code in secure memory via a function pointer that has been annotated with a cmse_nonsecure_call attribute. Such a function pointer is referred to as a non-secure function pointer.
In the imagined CMSE example application under consideration, the non-secure application code relies on the wait_on_event() function that is defined in secure memory to monitor a secure peripheral for a signal and then call an event handler function. The definition of wait_on_event() must account for the possibility that a non-secure function has been registered as an event handler that must be called via a non-secure function pointer.
Here is a possible implementation of the wait_on_event() function:
#include <arm_cmse.h>
typedef void __attribute__((cmse_nonsecure_call)) nsfunc(void);
extern int receive_signal(void);
extern void default_event_handler(void);
nsfunc *callback_fcn = (nsfunc *)default_event_handler;
__attribute__((cmse_nonsecure_entry)) void set_event_handler(nsfunc *fp)
{
if (fp)
callback_fcn = cmse_nsfptr_create(fp);
else
callback_fcn = (nsfunc *)default_event_handler;
}
__attribute__((cmse_nonsecure_entry)) void wait_on_event()
{
while (!receive_signal()) ;
if (cmse_is_nsfptr(callback_fcn))
callback_fcn();
else
((void (*)(void))callback_fcn)();
}
Things to note about the above C code:
The set_event_handler() function uses the cmse_nsfptr_create intrinsic that is defined in <arm_cmse.h> to convert the address of the non-secure function pointer argument to a value that can be recognized as a non-secure function address by clearing the least significant bit of fp as its address is assigned to callback_fcn. Note that all functions in the application are Thumb functions which have their least significant bit set to indicate that they are thumb mode functions. The CMSE ABI exploits this fact and repurposes the least significant bit of a function address to enable secure code to recognize it as a non-secure function.
The wait_on_event() function is also a non-secure entry function as indicated via application of the cmse_nonsecure_entry attribute.
wait_on_event() uses the cmse_is_nsfptr intrinsic to recognize a function as being defined in non-secure memory. The intrinsic will recognize a function pointer as a non-secure function if the least significant bit of the function pointer value is zero.
The following code is generated by the compiler for the definition of wait_on_event():
@ Generated code for wait_for_event function
.section .text.wait_on_event,"ax",%progbits
.hidden wait_on_event @ -- Begin function wait_on_event
.globl wait_on_event
.p2align 1
.type wait_on_event,%function
.code 16 @ @wait_on_event
.thumb_func
.globl __acle_se_wait_on_event
.type __acle_se_wait_on_event,%function
__acle_se_wait_on_event:
wait_on_event:
push {r7, lr}
b .LBB1_1
.LBB1_1: @ %while.cond
@ =>This Inner Loop Header: Depth=1
bl receive_signal
cbnz r0, .LBB1_3
b .LBB1_2
.LBB1_2: @ %while.body
@ in Loop: Header=BB1_1 Depth=1
b .LBB1_1
.LBB1_3: @ %while.end
movw r0, :lower16:callback_fcn
movt r0, :upper16:callback_fcn
ldrb r0, [r0]
lsls r0, r0, #31
cbnz r0, .LBB1_5
b .LBB1_4
.LBB1_4: @ %if.then
movw r0, :lower16:callback_fcn
movt r0, :upper16:callback_fcn
ldr r0, [r0]
push.w {r4, r5, r6, r7, r8, r9, r10, r11}
bic r0, r0, #1
sub sp, #136
vlstm sp
mov r1, r0
mov r2, r0
mov r3, r0
mov r4, r0
mov r5, r0
mov r6, r0
mov r7, r0
mov r8, r0
mov r9, r0
mov r10, r0
mov r11, r0
mov r12, r0
msr apsr_nzcvqg, r0
blxns r0
vlldm sp
add sp, #136
pop.w {r4, r5, r6, r7, r8, r9, r10, r11}
b .LBB1_6
.LBB1_5: @ %if.else
movw r0, :lower16:callback_fcn
movt r0, :upper16:callback_fcn
ldr r0, [r0]
blx r0
b .LBB1_6
.LBB1_6: @ %if.end
pop.w {r7, lr}
mrs r12, control
tst.w r12, #8
beq .LBB1_8
vmov d0, lr, lr
vmov d1, lr, lr
vmov d2, lr, lr
vmov d3, lr, lr
vmov d4, lr, lr
vmov d5, lr, lr
vmov d6, lr, lr
vmov d7, lr, lr
vmrs r12, fpscr
bic r12, r12, #159
bic r12, r12, #4026531840
vmsr fpscr, r12
.LBB1_8: @ %if.end
mov r0, lr
mov r1, lr
mov r2, lr
mov r3, lr
mov r12, lr
msr apsr_nzcvqg, lr
bxns lr
Some things to note about the compiler-generated code:
Since wait_on_event() is also a non-secure entry function, it is impacted by the application of the cmse_nonsecure_entry attribute in the same way that set_event_handler() is. Namely,
An __acle_se_wait_on_event label is defined at the function entry point in addition to the normal wait_on_event label.
The compiler generates code to clear the contents of caller-saved registers.
The compiler generates code to clear registers and flags that have undefined values at the return of a procedure, according to the Arm procedure call standard.
The compiler generates code to save and restore callee-saved registers as dictated by the Arm procedure call standard.
The compiler generates a BXNS instruction to return to its caller.
The effect of calling a function via a non-secure function pointer is:
The compiler will generate code to save all callee- and live caller-saved registers in secure memory.
The compiler will generate code to clear all callee- and caller-saved registers except:
The link register (lr),
Registers that hold the arguments of the function being called, and
Registers that do not hold secret information.
The compiler generates code to clear registers and flags that have undefined values at the entry to a procedure, according to the Arm procedure call standard.
The compiler generates a BLXNS instruction to effect an indirect call to the non-secure callback function.
6.3.3. Building the Example Application¶
An application that utilizes CMSE mechanisms will be built in two parts: the non-secure executable application and the secure image that the non-secure application can call into via a secure gateway to access data and functionality that is strictly managed by code in secure memory.
Using the above described example application as a model, this section describes first how to create a secure image, generate a table of secure gateway veneers, and produce an import library. Then how to incorporate the import library into the build of the non-secure executable application.
6.3.3.1. Building a Secure Image and Creating an Import Library¶
Recall that some non-secure entry functions have been defined in secure_code.c:
#include <arm_cmse.h>
typedef void __attribute__((cmse_nonsecure_call)) nsfunc(void);
extern int receive_signal(void);
extern void default_event_handler(void);
nsfunc *callback_fcn = (nsfunc *)default_event_handler;
__attribute__((cmse_nonsecure_entry)) void set_event_handler(nsfunc *fp)
{
if (fp)
callback_fcn = cmse_nsfptr_create(fp);
else
callback_fcn = (nsfunc *)default_event_handler;
}
__attribute__((cmse_nonsecure_entry)) void wait_on_event()
{
while (!receive_signal()) ;
if (cmse_is_nsfptr(callback_fcn))
callback_fcn();
else
((void (*)(void))callback_fcn)();
}
In addition, dummy definitions of main, default_event_handler() and receive_signal() are provided in more_secure_code.c:
#include <stdio.h>
int main() {
printf("dummy entry point for secure image\n");
return 0;
}
void default_event_handler(void) {
printf("Default event handler invoked\n");
}
__attribute__((optnone)) int receive_signal() {
int i = 20;
while (i--) {
asm(" ");
}
return 1;
}
The secure image can then be compiled and linked as follows:
%> tiarmclang -mcpu=cortex-m33 -mcmse secure_code.c more_secure_code.c \
-o secure.out -Xlinker -lsecure_lnk.cmd -Xlinker --import_cmse_lib_out=implib.o
In the above command, secure.out contains definitions of the secure functions set_event_handler() and wait_on_event() and their addresses are indicated by the values of the __acle_se_set_default_handler and __acle_se_wait_on_event labels in the following disassembly output:
%> tiarmdis secure.out secure.dis
%> cat secure.dis
TEXT Section .text, 0x10bc bytes at 0x00000020
...
0405c8: __acle_se_wait_on_event:
0405c8: .thumb
0405c8: :
0405c8: 80B5 PUSH {R7, LR}
0405ca: FFE7 B 0x000405CC
0405cc: 00F0FAFC BL receive_signal [0x40fc4]
0405d0: 08B9 CBNZ R0, 0x000405D6
0405d2: FFE7 B 0x000405D4
...
04070c: __acle_se_set_event_handler:
04070c: .thumb
04070c: :
04070c: 82B0 ADD SP, #-8
04070e: 0190 STR R0, [SP, #4]
040710: 0198 LDR R0, [SP, #4]
040712: 58B1 CBZ R0, 0x0004072C
040714: FFE7 B 0x00040716
...
During the link step of the above tiarmclang command, the linker will generate a table of secure gateway veneers:
%> cat secure.dis
...
TEXT Section Veneer$$CMSE, 0x10 bytes at 0x00500000
050000: set_event_handler:
050000: .thumb
050000: Veneer$$CMSE:
050000: 7FE97FE9 SG
050004: FEF7EEBA B.W __acle_se_set_event_handler [0x72c]
050008: wait_on_event:
050008: .thumb
050008: 7FE97FE9 SG
05000c: FEF748BA B.W __acle_se_wait_on_event [0x5e8]
...
Note that the linker has redefined the values of the set_event_handler and wait_on_event labels to the first instruction in the secure gateway veneers for the set_event_handler() and wait_on_event() functions, respectively.
At link-time, the linker-generated secure gateway veneer functions should be allocated into non-secure callable memory. For the purposes of this discussion, it is assumed that non-secure callable memory starts at address 0x050000. The secure_lnk.cmd linker command file used in the above tiarmclang command will instruct the linker to place the Veneer$$CMSE section in the non-secure callable memory area:
/****************************************************************************/
/* secure_lnk.cmd */
/* */
/* Set aside some target memory for secure and non-secure callable areas */
/* for made-up secure image example in tiarmclang user guide. */
/* */
/****************************************************************************/
-c
-stack 0x8000
-heap 0x2000
--args 0x1000
/* SPECIFY THE SECURE AND NON-SECURE CALLABLE MEMORY AREAS */
MEMORY
{
SEC_DMEM : org = 0x0030000 len = 0x10000
SEC_PMEM : org = 0x0040000 len = 0x10000
NSC_MEM : org = 0x0050000 len = 0x00100
}
/* SPECIFY THE SECTIONS ALLOCATION INTO MEMORY */
SECTIONS
{
.intvecs : {} > 0x0 /* INTERRUPT VECTORS */
.bss : {} > SEC_DMEM /* GLOBAL & STATIC VARS */
.data : {} > SEC_DMEM
.sysmem : {} > SEC_DMEM /* DYNAMIC MEMORY ALLOCATION AREA */
.stack : {} > SEC_DMEM /* SOFTWARE SYSTEM STACK */
.text : {} > SEC_PMEM /* CODE */
.cinit : {} > SEC_PMEM /* INITIALIZATION TABLES */
.const : {} > SEC_PMEM /* CONSTANT DATA */
.rodata : {} > SEC_PMEM
Veneer$$CMSE: {} > NSC_MEM
}
Note
The definition of the non-secure callable memory area must be within range of the secure code memory area so that the 24-bit PC-relative branch in each secure gateway veneer can reach its destination without the need for a linker-generated trampoline.
Finally, the --import_cmse_lib_out=implib.o option in the above tiarmclang command creates an import library. An import library is simply a linker generated object file that contains a list of absolute symbols that constitute a mapping of each non-secure entry function symbol to its associated secure gateway veneer location in non-secure callable memory.
In the CMSE example under consideration, the linker generated implib.o file contains the following list of absolute symbols:
%> tiarmofd -v -o implib.ofd implib.o
%> cat implib.ofd
OBJECT FILE: implib.o
...
Symbol Table ".symtab"
...
<1> "set_event_handler"
Value: 0x00050001 Kind: absolute
Binding: global Type: function
Size: 0x0 Visibility: STV_DEFAULT
<2> "wait_on_event"
Value: 0x00050009 Kind: absolute
Binding: global Type: function
Size: 0x0 Visibility: STV_DEFAULT
...
The values assigned to set_event_handler and wait_on_event correspond to the locations of their secure gateway veneers in non-secure callable memory. When the linker incorporates the implib.o object file into the link of the non-secure executable application, references to set_event_handler and wait_on_event will resolve to these addresses as though they were normal thumb mode functions.
6.3.3.2. Building/Linking Non-secure Application with an Import Library¶
When building a non-secure application that relies on functionality provided in the secure code, the non-secure application should include an import library object file that was generated during the build of the secure image.
To conclude the CMSE example that is under consideration, assume that we have a simple implementation of a non-secure application that references the set_event_handler() and wait_on_event() functions that were defined in the secure.out image created in the above discussion.
#include <stdio.h>
extern void set_event_handler(void (*fp)(void));
extern void wait_on_event(void);
void my_event_handler(void);
int main() {
wait_on_event();
set_event_handler(my_event_handler);
wait_on_event();
return 0;
}
void my_event_handler(void) {
printf("My event handler has been invoked\n");
}
The non-secure application can then be compiled and linked with the following tiarmclang command:
%> tiarmclang -mcpu=cortex-m33 non_secure_app.c implib.o -o non_secure.out \
-Xlinker non_secure_lnk.cmd
An examination of the disassembler output for non_secure.out shows the definition of main() has resolved its references to set_event_handler() and wait_on_event() to the correct locations in the Veneer$$CMSE section of the secure image:
%> tiarmdis non_secure.out non_secure.dis
%> cat non_secure.dis
...
060e30: main:
060e30: .thumb
060e30: :
060e30: 80B5 PUSH {R7, LR}
060e32: 82B0 ADD SP, #-8
060e34: 0020 MOVS R0, #0
060e36: 0090 STR R0, [SP]
060e38: 0190 STR R0, [SP, #4]
060e3a: EFF7E5F8 BL wait_on_event [0x50008]
060e3e: 40F6F160 MOVW.W R0, #3825
060e42: C0F20600 MOVT.W R0, #6
060e46: EFF7DBF8 BL set_event_handler [0x50000]
060e4a: EFF7DDF8 BL wait_on_event [0x50008]
060e4e: 0098 LDR R0, [SP]
060e50: 02B0 ADD SP, #8
060e52: 80BD POP {R7, PC}
When non_secure.out is loaded and run on a system where the secure.out has already been loaded, the first call to wait_on_event() will trigger an indirect call to default_event_handler() and the second call to wait_on_event() will trigger an indirect call back from the secure code to my_event_handler().
6.3.4. Summary of CMSE Mechanisms¶
Pre-Defined Macro Symbols
- __ARM_FEATURE_CMSE¶
The
__ARM_FEATURE_CMSE
pre-defined macro symbol describes what CMSE extensions are available in a given compilation. The value associated with__ARM_FEATURE_CMSE
is a bit-field where each bit is defined as follows:Bit Index
Meaning
0
if set, support for TT instructions is available
1
if set, tiarmclang targets the secure state of CMSE
The value of
__ARM_FEATURE_CMSE
is defined according to what compiler options are specified on the tiarmclang command-line, as described below.
Compiler Options
- -mcpu=cortex-m33¶
Instructs the tiarmclang compiler to target the Arm Cortex-M33 processor. In addition to other pre-defined macro symbols that are relevant to the Cortex-M33 processor, the tiarmclang compiler will also set bit 0 of the
__ARM_FEATURE_CMSE
symbol’s value when the -mcpu=cortex-m33 option is specified.
- -mcmse¶
The -mcmse option can only be used in combination with the -mcpu=cortex-m33 and will set bit 1 of the
__ARM_FEATURE_CMSE
symbol’s value to indicate that the tiarmclang compiler should target the secure state of CMSE.The use of this option instructs the tiarmclang compiler to enable the use of CMSE mechanisms that are only relevant for secure state code, like the cmse_nonsecure_entry and cmse_nonsecure_call attributes, for example.
Attributes
- cmse_nonsecure_entry¶
Syntax
__attribute__((cmse_nonsecure_entry))
Description
The cmse_nonsecure_entry attribute can be applied to a function defined in secure to designate the function as a non-secure entry function. This will instruct the compiler to generate code to clear registers and flags that may contain secret information prior to returning to a non-secure caller.
- cmse_nonsecure_call¶
Syntax
__attribute__((cmse_nonsecure_call))
Description
The cmse_nonsecure_call attribute can be applied to a function pointer type to designate that pointer type as a non-secure function pointer. This will instruct the compiler to generate a BLXNS instruction to effect a call to non-secure code via that function pointer type. In addition, the compiler will generate code prior to the BLXNS instruction to preserve the state of callee- and caller-saved registers and prevent leakage of secret information.
Non-secure Function Pointer Intrinsics
- cmse_nsfptr_create¶
Signature
The cmse_nsfptr_create intrinsic is implemented as a macro in <arm_cmse.h>:
#define cmse_nsfptr_create(p) ((typeof(p))((intptr_t)(p) & ~1))
Description
The cmse_nsfptr_create() macro will clear the least significant bit of a pointer to a thumb mode function. The pointer value can then be checked via the cmse_is_nsfptr() macro to determine whether the function pointer is to be interpreted as the address of a non-secure function.
- cmse_is_nsfptr¶
Signature
The cmse_is_nsfptr intrinsic is implemented as a macro in <arm_cmse.h>:
#define cmse_is_nsfptr(p) (!((intptr_t)(p) & 1))
Description
The cmse_is_nsfptr() macro can be used to test whether a given function pointer value should be interpreted as a non-secure function address. When such a function pointer is created by the cmse_nsfptr_create() macro, the least significant bit of its value will be cleared.