In my day job and free time I frequently find myself debugging Arm Cortex-M microcontrollers (MCUs). In recent years, it has become more and more common for the cores in these MCUs to implement Armv8-M, with the Arm Cortex-M33 being a very popular variant. Armv8-M includes an optional security extension (Cortex-M Security Extension or “CMSE”), which is more commonly known by its marketing name, TrustZone.

The security extension allows for a core, or a Processing Element (PE) if using the official terminology in Arm reference manuals, to divide memory into Secure and Non-Secure regions. When executing instructions in a Secure memory region, the PE is said to be in the Secure state, and when executing in a Non-Secure region, it is said to be in the Non-Secure state.

The current stack pointer (SP / R13) register in an Armv8-M PE either matches the Main Stack Pointer (MSP) or Process Stack Pointer (PSP), depending on the operating mode (Handler or Thread) and the SPSEL field in the CONTROL special-purpose register. If in Handler mode, the SP register will always match MSP. When in Thread mode, the SP will match MSP if SPSEL is 0 or PSP if SPSEL is 1.

When the security extension is implemented, there are both *_S and *_NS variants of some registers, inluding MSP (MSP_S and MSP_NS) and PSP (PSP_S and PSP_NS). This expands the possible values of SP, adding an extra dimension of the current security state. This can be useful to quickly determine the security state when debugging a core. With the following GDB command, the current stack pointer, as well as the Secure and Non-Secure variants of the Main and Process Stack Pointers will be printed.

i r sp psp_ns msp_ns psp_s msp_s

If the SP matches either PSP_NS or MSP_NS, then the PE is currently in the Non-Secure state. If it matches PSP_S or MSP_S, then the PR is currently in the Secure state. For example, on reset in a Cortex-M33 core with the security extension implemented, the processor will be in Thread mode and Secure state, so SP will match MSP_S.

sp             0x20004000          0x20004000
psp_ns         0x2001ad40          0x2001ad40 <z_main_stack+4048>
msp_ns         0x0                 0x0 <_vector_table>
psp_s          0x0                 0x0 <_vector_table>
msp_s          0x20004000          0x20004000

As evidenced by the z_main_stack symbol, this is a Zephyr RTOS application. Breaking at main, then checking the the same registers expectedly results in SP matching PSP_NS because the processor is in Thread mode and Non-Secure state and CONTROL.SPSEL is set to 1.

sp             0x2001ad40          0x2001ad40 <z_main_stack+4048>
psp_ns         0x2001ad40          0x2001ad40 <z_main_stack+4048>
msp_ns         0x20019c30          0x20019c30 <z_idle_stacks>
psp_s          0x20001eb8          0x20001eb8 <z_main_stack-97976>
msp_s          0x20000bf8          0x20000bf8 <sector_buffers+1688>

The CONTROL.SPSEL field can be checked by printing the special-purpose CONTROL register with a mask on the second bit, with a non-zero value indicating that SPSEL is 1.

(gdb) print $control & 0x2
$0 = 2

While this is certainly not the only way to determine the current security state in Armv8-M cores, it can be a helpful way to do so quickly.