2е. TrustZone in practice: SAU, GTZC, the call gateway and two projects on the STM32U585
Section 2б showed the idea of TrustZone and ran it in an emulator on someone else's board:
two core states, the NSC window, the SG instruction, SecureFault. Section 2д took apart
the U585 itself — pins, power, clock. Here is what they lack: how a real
STM32U585 is partitioned once the TZEN bit is set. It has not one boundary
but three, and each has its own register and its own granularity. At the end there are two projects that
build: a secure one with a counter and a key, and a non-secure one that
uses them.
Prerequisites: the reference manual RM0456 (rev. 4) and the Cortex-M33 core manual
(Arm 100235) from the Keil pack STM32U5xx_DFP 2.2.1; the headers
stm32u585xx.h and partition_stm32u585xx.h from the same place, core_cm33.h and
cmsis_gcc.h from CMSIS 5.9.0; the board package B-U585I-IOT02A_BSP 1.1.0. Both
projects are built with arm-none-eabi-gcc 12.2 with the -mcmse flag, the gateways are disassembled
with objdump. The register values are computed by a model on a PC and checked against the headers.
QEMU does not model the U5, and nothing was run on a board.
Terms used in the documentation
Term Meaning TZEN option bit the option bit that enables TrustZone Secure watermark (SECWM) a secure Flash area "from page to page", set by option bits Block-based (SECBB, MPCBB) an attribute per block: a bit per Flash page or per 512 bytes of RAM Super-block 32 consecutive MPCBB blocks — one register word, 16 KB Securable peripheral a peripheral that the TZSC makes secure TrustZone-aware peripheral a peripheral that itself tells secure and non-secure accesses apart Illegal access event a flag in the TZIC and, if enabled, an interrupt Secure gateway veneer the gateway: SGand a branch to the real functionCMSE import library a file with the gateway addresses for the non-secure project Stack seal 8 bytes above the top of the secure stack
2е.1 The TZEN bit: what changes after reset
TrustZone is disabled at the factory in all STM32U5 (2б.7). It is enabled by the TZEN bit —
bit 31 of the FLASH_OPTR register, and it can be set only at read protection
level RDP 0. After that the chip comes out of reset differently:
| What | State after reset with TZEN = 1 |
|---|---|
| Core | the secure state; the boot address must be secure |
| Core memory map | the SAU is disabled — all addresses are secure |
| Flash | all secure: SECWMx_PSTRT = 0, SECWMx_PEND = 0x7F in both banks |
| RAM | all MPCBB blocks are secure and privileged (0xFFFFFFFF) |
| Pins | all secure: GPIOx_SECCFGR = 0x0000FFFF |
| Other peripherals | non-secure: GTZC_TZSC_SECCFGRx = 0 |
| Interrupts | all secure; TZIC events are masked |
Hence the order of work of the secure firmware: it starts first, gets everything and hands out to the non-secure part what belongs to it. If it does not hand it out, the non-secure part will not start at all.
Where it starts from. The boot address is taken from the SECBOOTADD0 field — bits 31:7
of the FLASH_SECBOOTADD0R register, in steps of 128 bytes. The factory value 0x0C00007C
points to 0x0C000000: the start of Flash at the secure alias. That is why
the secure project is linked at 0x0C000000, not at the usual
0x08000000.
The way back. TZEN can be cleared only together with an RDP regression from level 1
to level 0, and the regression erases the Flash and all the RAM (more in 2б.7). Between them
there is level 0.5: at it, debugging of the secure part is already unavailable, while
the non-secure part can be debugged as long as the core is in the non-secure state.
2е.2 Three boundaries: the SAU, the Flash option bits and the MPCBB
The address attribute of 2б.2 is only the view of the core. RM0456 states directly: the SAU and IDAU settings act on the Cortex-M33 and do not act on other bus masters, for example DMA. That is why on the U5 each memory also guards itself on its own:
| Boundary | Who holds it | Granularity | Where it is stored |
|---|---|---|---|
| The core's view | the SAU on top of the IDAU | 32 bytes | core registers, empty after reset |
| Flash | SECWM and SECBB |
an 8 KB page | SECWM — option bits, SECBB — registers |
| RAM | GTZC MPCBB | a 512-byte block | registers, all secure after reset |
All three must say the same thing. If the SAU declared a region non-secure but the Flash page or RAM block stayed secure, the non-secure code will not get to them.
The SAU and the IDAU. The Cortex-M33 in the U5 has eight SAU regions. A region is a pair of
registers: SAU_RBAR with the start address and SAU_RLAR with the end address; the lower
five address bits are not stored in them, so the granularity is 32 bytes. In SAU_RLAR bit 0
enables the region, bit 1 is the NSC flag. The region number is selected by SAU_RNR.
Everything not covered by regions stays secure.
The U5 IDAU is fixed: the non-secure addresses 0x08…, 0x20…, 0x40…
and their twins 0x0C…, 0x30…, 0x50…, which the IDAU marks as NSC.
The result is put together according to table 8 of RM0456:
| IDAU | SAU: secure | SAU: NSC | SAU: non-secure |
|---|---|---|---|
a non-secure address (0x08…, 0x20…, 0x40…) |
secure | NSC | non-secure |
NSC (0x0C…, 0x30…, 0x50…) |
secure | NSC | NSC |
The last cell is a trap. A non-secure SAU region set by mistake
at an address 0x0C… gives not non-secure memory but a window for gateways.
Flash. The secure area of each bank is set by a pair of fields in the option bits:
SECWMx_PSTRT and SECWMx_PEND — the first and the last page (bits 6:0
and 22:16 of the FLASH_SECWMxR1 register). If the start is greater than the end, there is no secure
area in the bank. The second possibility is FLASH_SECBBxRy: a bit per
page, in the U585 header four registers per bank. These bits are ordinary ones,
not options: zeros after reset, the secure code sets them on the fly and can
add a secure page, but not take one away from SECWM.
RAM. The MPCBB divides memory into blocks of 512 bytes. A SECCFGR word is
32 blocks, that is 16 KB; the RM calls it a super-block. The U585 has three
controllers in GTZC1: MPCBB1 for SRAM1 (192 KB, 384 blocks, 12 words), MPCBB2
for SRAM2 (64 KB, 128 blocks, 4 words) and MPCBB3 for SRAM3 (512 KB, 1024
blocks, 32 words). The CFGLOCKR1 register locks words until reset — a bit
per word.
The example's partitioning. The secure part is given little: 64 KB of Flash and 24 KB of RAM. All addresses and register values are gathered in one file for both projects:
/* Раздел 2е: карта памяти двух проектов на STM32U585 (Flash 2 МБ: два банка по 128 страниц, ОЗУ SRAM1…SRAM3).
Один файл на оба проекта. Значения регистров ниже считает model2e.py; при сборке их сверяет _Static_assert. */
#ifndef TZ_LAYOUT_H
#define TZ_LAYOUT_H
/* безопасный проект: начало банка 1 по безопасному псевдониму 0x0C000000 */
#define S_CODE_START 0x0C000000u
#define S_CODE_END 0x0C00FDFFu /* страницы 0…7 без последних 512 байт */
#define NSC_START 0x0C00FE00u /* окно шлюзов: 512 байт в конце страницы 7 */
#define NSC_END 0x0C00FFFFu
#define S_RAM_START 0x30000000u /* первые 24 КБ SRAM1 */
#define S_RAM_END 0x30005FFFu
/* небезопасный проект: остаток Flash и ОЗУ по небезопасным адресам */
#define NS_CODE_START 0x08010000u /* со страницы 8 банка 1 до конца банка 2 */
#define NS_CODE_END 0x081FFFFFu
#define NS_RAM_START 0x20006000u /* остаток SRAM1, SRAM2 и SRAM3 */
#define NS_RAM_END 0x200BFFFFu
#define NS_PERIPH_START 0x40000000u
#define NS_PERIPH_END 0x4FFFFFFFu
/* SAU: область 0 — шлюзы (NSC), 1 — Flash, 2 — ОЗУ, 3 — периферия небезопасной части */
#define SAU_RBAR0_VAL 0x0C00FE00u
#define SAU_RLAR0_VAL 0x0C00FFE3u /* LADDR | NSC | ENABLE */
#define SAU_RBAR1_VAL 0x08010000u
#define SAU_RLAR1_VAL 0x081FFFE1u
#define SAU_RBAR2_VAL 0x20006000u
#define SAU_RLAR2_VAL 0x200BFFE1u
#define SAU_RBAR3_VAL 0x40000000u
#define SAU_RLAR3_VAL 0x4FFFFFE1u
/* Flash: биты опций. Банк 1 — безопасны страницы 0…7; банк 2 — безопасной области нет (PSTRT > PEND) */
#define SECWM1_PSTRT 0x00u
#define SECWM1_PEND 0x07u
#define SECWM2_PSTRT 0x7Fu
#define SECWM2_PEND 0x00u
#define SECWM1R1_VAL 0x00070000u /* поля PSTRT и PEND регистра FLASH_SECWM1R1 */
#define SECWM2R1_VAL 0x0000007Fu
#define SECBOOTADD0_VAL 0x00180000u /* поле SECBOOTADD0 = адрес 0x0C000000 >> 7 */
/* MPCBB1 (SRAM1, 12 слов по 32 блока): 48 безопасных блоков по 512 байт, дальше небезопасные */
#define MPCBB1_WORDS 12u
#define MPCBB2_WORDS 4u
#define MPCBB3_WORDS 32u
#define MPCBB1_SEC0 0xFFFFFFFFu
#define MPCBB1_SEC1 0x0000FFFFu
#define MPCBB1_LOCK 0x00000FFFu /* CFGLOCKR1: замок на все 12 слов */
#endif
The numbers in it are not typed in by hand. The script model2e.py takes six ranges
and computes the rest from the field definitions in core_cm33.h and stm32u585xx.h:
| What | Calculation | Value |
|---|---|---|
| SAU 0, gateways | 0x0C00FE00…0x0C00FFFF, NSC |
RBAR = 0x0C00FE00, RLAR = 0x0C00FFE3 |
| SAU 1, Flash | 0x08010000…0x081FFFFF |
RBAR = 0x08010000, RLAR = 0x081FFFE1 |
| SAU 2, RAM | 0x20006000…0x200BFFFF |
RBAR = 0x20006000, RLAR = 0x200BFFE1 |
| SAU 3, peripherals | 0x40000000…0x4FFFFFFF |
RBAR = 0x40000000, RLAR = 0x4FFFFFE1 |
SECWM1 |
64 KB / 8 KB = 8 pages | PSTRT = 0, PEND = 7, register fields 0x00070000 |
SECWM2 |
no secure area | PSTRT = 0x7F > PEND = 0 |
| MPCBB1 | 24 KB / 512 = 48 blocks | word 0 = 0xFFFFFFFF, word 1 = 0x0000FFFF, words 2…11 — zeros |
| MPCBB2, MPCBB3 | entirely non-secure | 4 and 32 zero words |
The non-secure part is left with 248 Flash pages (1984 KB) and 744 KB of RAM.
The RAM boundary at 24 KB is deliberately not a multiple of 16 KB: it splits word 1 in half,
and you can see that the MPCBB cuts more finely than by super-blocks. The gateway window is 512 bytes,
not a page: the SAU allows a granularity of 32 bytes, and the narrower the window, the less
room for an accidental SG (2б.2).
For comparison — the partitioning from the pack: the file partition_gen.h, which is included by
partition_stm32u585xx.h. The regions there are described by the macros SAU_INIT_REGIONn (enable),
SAU_INIT_STARTn, SAU_INIT_ENDn and SAU_INIT_NSCn; the TZ_SAU_Setup function
turns them into the same RNR, RBAR, RLAR writes. Six regions are enabled:
gateways 0x0C0FE000…0x0C0FFFFF (8 KB at the end of bank 1), the whole of bank 2
from 0x08100000, SRAM3 from 0x20040000, the peripherals, external memory
0x60000000…0x9FFFFFFF and system memory 0x0BF90000…0x0BFA8FFF.
There the Flash is split in half at the bank boundary — simpler, but the secure part
gets a megabyte.
2е.3 Peripherals and interrupts: TZSC, TZIC, NVIC_ITNS
The U5 peripherals are of two kinds.
Those that the TZSC makes secure. For each block there is a bit in the registers
GTZC_TZSC_SECCFGR1…SECCFGR3; zeros after reset, that is, the block is
non-secure. In the example the crypto blocks are declared secure: AES, HASH, RNG,
PKA and SAES — bits 11…15 of the SECCFGR3 register, mask 0x0000F800. Next to them lie
the PRIVCFGRx — the same division, but into privileged and unprivileged
access; it works without TrustZone too. The LCK bit in GTZC_TZSC_CR locks both
sets until reset.
Those that tell accesses apart themselves. These are GPIO, RCC, PWR, Flash, EXTI, DMA,
RTC and TAMP, SYSCFG, the GTZC itself. Their security is set inside the block.
The main thing for the first start is the pins: GPIOx_SECCFGR after reset equals
0x0000FFFF, all pins are secure. Non-secure code accessing such a
pin reads zeros, and the write is lost — without any exception. The second
half of the same trap: as long as a port has at least one secure pin, the clock
bit of this port in the RCC is also secure. That is why in the example the port H clock
is enabled by the secure part, and it also hands over pin PH7 — on the
B-U585I-IOT02A board there is a green LED there.
The TZIC collects illegal accesses. Each source has three bits
with the same number: the enable in GTZC_TZIC_IERx, the flag in SRx, the flag clear
in FCRx. All events go into one secure GTZC interrupt (number 8);
after reset all are masked. What counts as illegal, RM0456
lists in 5.4.2:
- a non-secure write to a secure resource is an event; non-secure execution from there is a bus fault;
- a secure access to a non-secure RAM block at a secure address is
also an event, unless the
SRWILADISbit is set inGTZC_MPCBBx_CR; - an unprivileged access to a privileged resource is a silent rejection: zeros are read, the write is lost, there is no event.
Interrupts. After reset all NVIC lines are secure. A line is handed over to the
non-secure part by a bit in NVIC_ITNS: the word is the interrupt number divided
by 32, the bit is the remainder. For USART1 (number 61) this is NVIC_ITNS[1], bit 29;
in CMSIS this is done by NVIC_SetTargetState. The handler of such a line is taken
from the non-secure vector table: each state has its own VTOR.
The secure part sets its own VTOR_S at startup, and writes the non-secure VTOR_NS
through the alias 0xE002ED08 before the transition.
2е.4 The secure project
The header shared by both projects — three functions and error codes:
/* Раздел 2е: функции безопасной части, видимые небезопасной. Этот файл подключают оба проекта. */
#ifndef S_API_H
#define S_API_H
#include <stdint.h>
#define S_OK 0
#define S_ERR_LEN (-1) /* длина сообщения больше S_MAC_MAX */
#define S_ERR_PTR (-2) /* указатель ведёт не в небезопасную память */
#define S_MAC_MAX 64u
typedef void (*s_violation_cb)(uint32_t reg, uint32_t flags);
uint32_t s_counter_next(void); /* счётчик, который нельзя откатить снаружи */
int32_t s_mac(const uint8_t *msg, uint32_t len, uint32_t *tag); /* метка сообщения ключом безопасной части */
int32_t s_on_violation(s_violation_cb cb); /* сообщать о незаконных обращениях (TZIC) */
#endif
The whole secure project is one file. At the top is _Static_assert: the values
from tz_layout.h are recomputed from the header masks, so a typo
in an address will not build. Then come the entry functions, the handlers, the configuration and the start.
/* Раздел 2е: безопасный проект для STM32U585 с TZEN = 1. Регистры — по заголовкам CMSIS (stm32u585xx.h, core_cm33.h).
Порядок: проверка битов опций -> SAU -> GTZC (MPCBB, TZSC, TZIC) -> выводы и прерывания -> переход в небезопасную часть.
Сборка: arm-none-eabi-gcc -mcpu=cortex-m33 -mcmse; компоновщик с --cmse-implib делает шлюзы и файл s_veneers.o. */
#include <arm_cmse.h>
#include "stm32u5xx.h"
#include "tz_layout.h"
#include "s_api.h"
/* ── числа из tz_layout.h сверяются с полями заголовков ещё при компиляции ── */
#define SAU_RBAR(start) ((start) & SAU_RBAR_BADDR_Msk)
#define SAU_RLAR(end, nsc) (((end) & SAU_RLAR_LADDR_Msk) | ((nsc) << SAU_RLAR_NSC_Pos) | SAU_RLAR_ENABLE_Msk)
_Static_assert(SAU_RBAR(NSC_START) == SAU_RBAR0_VAL && SAU_RLAR(NSC_END, 1u) == SAU_RLAR0_VAL, "SAU 0: шлюзы");
_Static_assert(SAU_RBAR(NS_CODE_START) == SAU_RBAR1_VAL && SAU_RLAR(NS_CODE_END, 0u) == SAU_RLAR1_VAL, "SAU 1: Flash");
_Static_assert(SAU_RBAR(NS_RAM_START) == SAU_RBAR2_VAL && SAU_RLAR(NS_RAM_END, 0u) == SAU_RLAR2_VAL, "SAU 2: ОЗУ");
_Static_assert(SAU_RBAR(NS_PERIPH_START) == SAU_RBAR3_VAL && SAU_RLAR(NS_PERIPH_END, 0u) == SAU_RLAR3_VAL, "SAU 3: периферия");
_Static_assert(((SECWM1_PSTRT << FLASH_SECWM1R1_SECWM1_PSTRT_Pos) | (SECWM1_PEND << FLASH_SECWM1R1_SECWM1_PEND_Pos)) == SECWM1R1_VAL,
"SECWM1R1");
_Static_assert(((SECWM2_PSTRT << FLASH_SECWM2R1_SECWM2_PSTRT_Pos) | (SECWM2_PEND << FLASH_SECWM2R1_SECWM2_PEND_Pos)) == SECWM2R1_VAL,
"SECWM2R1");
_Static_assert((S_CODE_START >> FLASH_SECBOOTADD0R_SECBOOTADD0_Pos) == SECBOOTADD0_VAL, "адрес загрузки");
_Static_assert((NSC_END + 1u - S_CODE_START) == (SECWM1_PEND + 1u) * FLASH_PAGE_SIZE, "безопасный код = страницы 0…PEND");
_Static_assert((S_RAM_END + 1u - S_RAM_START) / 0x200u == 48u, "48 безопасных блоков по 512 байт");
_Static_assert(SRAM1_SIZE / 0x200u / 32u == MPCBB1_WORDS && SRAM2_SIZE / 0x200u / 32u == MPCBB2_WORDS
&& SRAM3_SIZE / 0x200u / 32u == MPCBB3_WORDS, "число слов MPCBB");
/* криптоблоки остаются безопасной части: биты одинаковы в SECCFGR3 (TZSC) и IER3 (TZIC) */
#define S_PERIPH3 (GTZC_TZSC1_SECCFGR3_AES_Msk | GTZC_TZSC1_SECCFGR3_HASH_Msk | GTZC_TZSC1_SECCFGR3_RNG_Msk \
| GTZC_TZSC1_SECCFGR3_PKA_Msk | GTZC_TZSC1_SECCFGR3_SAES_Msk)
/* о чём ещё сообщать: Flash и её регистры, SRAM1 и её MPCBB, сами TZSC и TZIC */
#define S_EVENTS4 (GTZC_TZIC1_IER4_FLASH_Msk | GTZC_TZIC1_IER4_FLASH_REG_Msk | GTZC_TZIC1_IER4_SRAM1_Msk \
| GTZC_TZIC1_IER4_MPCBB1_REG_Msk | GTZC_TZIC1_IER4_TZSC1_Msk | GTZC_TZIC1_IER4_TZIC1_Msk)
typedef void __attribute__((cmse_nonsecure_call)) ns_entry_fn(void);
typedef void __attribute__((cmse_nonsecure_call)) ns_violation_fn(uint32_t reg, uint32_t flags);
/* ── состояние безопасной части: снаружи не видно ── */
static const uint32_t s_key = 0x9E3779B9u;
static uint32_t s_counter;
static ns_violation_fn *ns_cb;
volatile uint32_t tzic_events, tzic_last_reg, tzic_last_flags;
volatile uint32_t fault_sfsr, fault_sfar;
/* ── функции для небезопасной части: компилятор делает к каждой шлюз SG и чистит регистры на выходе ── */
__attribute__((cmse_nonsecure_entry)) uint32_t s_counter_next(void)
{
return ++s_counter;
}
__attribute__((cmse_nonsecure_entry)) int32_t s_mac(const uint8_t *msg, uint32_t len, uint32_t *tag)
{
uint8_t copy[S_MAC_MAX];
if (len == 0u || len > S_MAC_MAX)
return S_ERR_LEN;
/* команда TT: весь диапазон должен быть небезопасным и доступным небезопасной части */
if (cmse_check_address_range((void *)msg, len, CMSE_NONSECURE | CMSE_MPU_READ) == NULL)
return S_ERR_PTR;
if (cmse_check_address_range(tag, sizeof *tag, CMSE_NONSECURE | CMSE_MPU_READWRITE) == NULL)
return S_ERR_PTR;
for (uint32_t i = 0; i < len; i++) /* своя копия: после проверки сообщение уже не подменить */
copy[i] = msg[i];
uint32_t h = s_key; /* заглушка вместо HMAC: FNV-1a, ключ — начальное значение */
for (uint32_t i = 0; i < len; i++)
h = (h ^ copy[i]) * 16777619u;
*tag = h;
return S_OK;
}
__attribute__((cmse_nonsecure_entry)) int32_t s_on_violation(s_violation_cb cb)
{
/* адрес функции тоже проверяется: по безопасному адресу «небезопасный» вызов не пойдёт */
if (cmse_check_address_range((void *)((uintptr_t)cb & ~(uintptr_t)1), 2u, CMSE_NONSECURE) == NULL)
return S_ERR_PTR;
ns_cb = (ns_violation_fn *)cmse_nsfptr_create(cb); /* младший бит 0: BLXNS уйдёт в небезопасное состояние */
return S_OK;
}
/* ── исключения безопасной части ── */
void GTZC_IRQHandler(void)
{
static __IO uint32_t *const sr[4] = { >ZC_TZIC1->SR1, >ZC_TZIC1->SR2, >ZC_TZIC1->SR3, >ZC_TZIC1->SR4 };
static __IO uint32_t *const fcr[4] = { >ZC_TZIC1->FCR1, >ZC_TZIC1->FCR2, >ZC_TZIC1->FCR3, >ZC_TZIC1->FCR4 };
for (uint32_t i = 0; i < 4u; i++) {
uint32_t flags = *sr[i];
if (flags == 0u)
continue;
*fcr[i] = flags; /* флаг снимается записью 1 в FCRx */
tzic_last_reg = i + 1u;
tzic_last_flags = flags;
tzic_events++;
if (ns_cb != NULL)
ns_cb(i + 1u, flags); /* вызов небезопасной функции: BLXNS с очисткой регистров */
}
}
void SecureFault_Handler(void)
{
fault_sfsr = SAU->SFSR;
fault_sfar = (fault_sfsr & SAU_SFSR_SFARVALID_Msk) ? SAU->SFAR : 0xFFFFFFFFu;
for (;;) { /* в изделии — запись в журнал и сброс */
}
}
void HardFault_Handler(void)
{
for (;;) {
}
}
void Default_Handler(void)
{
for (;;) {
}
}
/* ── настройка ── */
static int options_ok(void)
{
uint32_t wm2 = FLASH->SECWM2R1;
uint32_t pstrt2 = (wm2 & FLASH_SECWM2R1_SECWM2_PSTRT_Msk) >> FLASH_SECWM2R1_SECWM2_PSTRT_Pos;
uint32_t pend2 = (wm2 & FLASH_SECWM2R1_SECWM2_PEND_Msk) >> FLASH_SECWM2R1_SECWM2_PEND_Pos;
if ((FLASH->OPTR & FLASH_OPTR_TZEN) == 0u)
return 0; /* TrustZone выключен: безопасные регистры читались бы нулями */
if ((FLASH->SECWM1R1 & (FLASH_SECWM1R1_SECWM1_PSTRT_Msk | FLASH_SECWM1R1_SECWM1_PEND_Msk)) != SECWM1R1_VAL)
return 0; /* безопасная область банка 1 не та, под которую собран проект */
return pstrt2 > pend2; /* в банке 2 безопасной области быть не должно */
}
static void sau_setup(void)
{
SAU->RNR = 0u; SAU->RBAR = SAU_RBAR0_VAL; SAU->RLAR = SAU_RLAR0_VAL; /* шлюзы, NSC */
SAU->RNR = 1u; SAU->RBAR = SAU_RBAR1_VAL; SAU->RLAR = SAU_RLAR1_VAL; /* Flash небезопасной части */
SAU->RNR = 2u; SAU->RBAR = SAU_RBAR2_VAL; SAU->RLAR = SAU_RLAR2_VAL; /* её ОЗУ */
SAU->RNR = 3u; SAU->RBAR = SAU_RBAR3_VAL; SAU->RLAR = SAU_RLAR3_VAL; /* её периферия */
SAU->CTRL = SAU_CTRL_ENABLE_Msk; /* ALLNS = 0: всё вне областей остаётся безопасным */
__DSB();
__ISB();
}
static void gtzc_setup(void)
{
RCC->AHB1ENR |= RCC_AHB1ENR_GTZC1EN;
(void)RCC->AHB1ENR;
/* MPCBB: бит на блок 512 байт, 1 — безопасный. SRAM1 делится, SRAM2 и SRAM3 отдаются целиком */
GTZC_MPCBB1->SECCFGR[0] = MPCBB1_SEC0;
GTZC_MPCBB1->SECCFGR[1] = MPCBB1_SEC1;
for (uint32_t i = 2u; i < MPCBB1_WORDS; i++)
GTZC_MPCBB1->SECCFGR[i] = 0u;
for (uint32_t i = 0u; i < MPCBB2_WORDS; i++)
GTZC_MPCBB2->SECCFGR[i] = 0u;
for (uint32_t i = 0u; i < MPCBB3_WORDS; i++)
GTZC_MPCBB3->SECCFGR[i] = 0u;
GTZC_MPCBB1->CFGLOCKR1 = MPCBB1_LOCK; /* границу в SRAM1 до сброса уже не сдвинуть */
/* TZSC: кто из периферии безопасный; остальная после сброса небезопасна */
GTZC_TZSC1->SECCFGR3 |= S_PERIPH3;
GTZC_TZSC1->CR = GTZC_TZSC_CR_LCK_Msk; /* SECCFGRx и PRIVCFGRx заперты до сброса */
/* TZIC: после сброса все события замаскированы */
GTZC_TZIC1->IER3 = S_PERIPH3;
GTZC_TZIC1->IER4 = S_EVENTS4;
NVIC_EnableIRQ(GTZC_IRQn);
}
static void handover_setup(void)
{
/* выводы после сброса безопасны; пока в порту есть безопасный вывод, бит его такта в RCC тоже безопасный */
RCC->AHB2ENR1 |= RCC_AHB2ENR1_GPIOHEN;
(void)RCC->AHB2ENR1;
GPIOH->SECCFGR &= ~GPIO_SECCFGR_SEC7; /* PH7 — небезопасной части */
(void)NVIC_SetTargetState(USART1_IRQn); /* бит в NVIC_ITNS: прерывание USART1 уходит в небезопасное состояние */
SCB->NSACR |= SCB_NSACR_CP10_Msk | SCB_NSACR_CP11_Msk; /* FPU разрешён небезопасной части */
SCB->SHCSR |= SCB_SHCSR_SECUREFAULTENA_Msk; /* SecureFault отдельным обработчиком, а не HardFault */
}
__attribute__((noinline)) static void jump_to_ns(void)
{
const uint32_t *vec = (const uint32_t *)NS_CODE_START; /* таблица векторов небезопасной части */
if (vec[1] == 0xFFFFFFFFu)
return; /* Flash пуста: прыгать некуда */
SCB_NS->VTOR = NS_CODE_START; /* VTOR_NS — через псевдоним 0xE002ED08 */
__TZ_set_MSP_NS(vec[0]);
ns_entry_fn *entry = (ns_entry_fn *)cmse_nsfptr_create(vec[1]);
entry(); /* BLXNS */
}
int main(void)
{
if (options_ok()) {
sau_setup();
gtzc_setup();
handover_setup();
jump_to_ns();
}
for (;;) { /* сюда попадают при неверных битах опций или пустой Flash */
}
}
/* ── старт ── */
extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss, _sstack;
extern uint32_t _estack[]; /* вершина стека; над ней 8 байт печати */
void Reset_Handler(void)
{
__set_MSPLIM((uint32_t)&_sstack); /* переполнение стека — UsageFault, а не порча данных */
SCB->VTOR = S_CODE_START; /* VTOR_S: таблица векторов безопасной части */
__TZ_set_STACKSEAL_S(_estack); /* 8 байт печати над вершиной безопасного стека */
for (uint32_t *s = &_sidata, *d = &_sdata; d < &_edata;)
*d++ = *s++;
for (uint32_t *d = &_sbss; d < &_ebss;)
*d++ = 0u;
main();
for (;;) {
}
}
/* таблица векторов безопасной части: лежит по адресу загрузки SECBOOTADD0 */
__attribute__((section(".isr_vector"), used)) static void (*const vectors[16 + LSECSSD_IRQn + 1])(void) = {
[0] = (void (*)(void))_estack,
[1] = Reset_Handler,
[2] = Default_Handler,
[3] = HardFault_Handler,
[4 ... 6] = Default_Handler,
[7] = SecureFault_Handler,
[8 ... 15] = Default_Handler,
[16 ... 16 + GTZC_IRQn - 1] = Default_Handler,
[16 + GTZC_IRQn] = GTZC_IRQHandler,
[16 + GTZC_IRQn + 1 ... 16 + LSECSSD_IRQn] = Default_Handler,
};
What to look at:
options_ok. A program cannot check the option bits in advance — they are set by the programmer. So the first thing the code does is compareTZENand bothSECWMpairs with what it was built for, and if they differ it goes no further.s_mac. Both pointers came from outside and both are checked over the whole range; the message is first copied, then processed (2б.5). The computation itself is a stub, not cryptography: in its place there should be an HMAC on the HASH block, which is kept secure for exactly that purpose.s_on_violation. A function pointer is also a pointer from outside.cmse_nsfptr_createclears its lowest bit, and the type with thecmse_nonsecure_callattribute makes the compiler call it viaBLXNS.- Stacks. Each state has its own
MSPand its own lower limitMSPLIM: the core catches the stack pointer going below the limit with an exception. The secure part takesMSP_NSfrom the first word of the non-secure vector table. Above the top of the secure stack lies the seal — 8 bytes0xFEF5EDA5FEF5EDA5; in CMSIS it is set by__TZ_set_STACKSEAL_S. - Reading the other part's vector table goes through the non-secure address
0x08010000. According to the core manual an access is considered secure if its address is secure; so this access is non-secure, and the Flash lets it through.
The linker file repeats the same three regions; the gateways go into a separate
section .gnu.sgstubs:
/* Раздел 2е: безопасный проект. Адреса те же, что в tz_layout.h (сверяет model2e.py). */
MEMORY
{
S_CODE (rx) : ORIGIN = 0x0C000000, LENGTH = 0xFE00 /* страницы 0…7 банка 1 без окна шлюзов */
S_NSC (rx) : ORIGIN = 0x0C00FE00, LENGTH = 0x200 /* окно шлюзов: область SAU с признаком NSC */
S_RAM (rw) : ORIGIN = 0x30000000, LENGTH = 0x6000 /* первые 24 КБ SRAM1 */
}
_estack = ORIGIN(S_RAM) + LENGTH(S_RAM) - 8; /* 8 байт над стеком — печать */
_sstack = _estack - 0x800; /* стек 2 КБ; нижняя граница уходит в MSPLIM */
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > S_CODE
.text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > S_CODE
.gnu.sgstubs : { __sg_start = .; *(.gnu.sgstubs*) } > S_NSC /* символ не даёт выкинуть секцию, пока она пуста */
.data : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > S_RAM AT > S_CODE
_sidata = LOADADDR(.data);
.bss (NOLOAD) : { _sbss = .; *(.bss .bss.* COMMON) . = ALIGN(4); _ebss = .; } > S_RAM
ASSERT(_ebss <= _sstack, "данные дошли до стека")
}
2е.5 The non-secure project, the build and the gateways
The non-secure project knows almost nothing about TrustZone. It is built
without -mcmse, and the header itself substitutes the non-secure peripheral addresses.
For it the three secure functions are ordinary external functions from s_api.h.
/* Раздел 2е: небезопасный проект для STM32U585. О безопасной части знает только s_api.h и адреса шлюзов из s_veneers.o.
Собирается без -mcmse: заголовок подставляет небезопасные адреса периферии (GPIOH = 0x42021C00). */
#include "stm32u5xx.h"
#include "tz_layout.h"
#include "s_api.h"
volatile uint32_t ns_count, ns_tag, ns_violations, ns_last_reg, ns_last_flags;
volatile int32_t ns_rc_good, ns_rc_steal, ns_rc_long;
static void on_violation(uint32_t reg, uint32_t flags) /* зовёт безопасный обработчик GTZC */
{
ns_last_reg = reg;
ns_last_flags = flags;
ns_violations++;
}
static void delay(volatile uint32_t n)
{
while (n--) {
}
}
int main(void)
{
uint32_t tag = 0u;
/* PH7 отдан безопасной частью, такт порта H включён ею же */
GPIOH->MODER = (GPIOH->MODER & ~GPIO_MODER_MODE7_Msk) | GPIO_MODER_MODE7_0;
(void)s_on_violation(on_violation);
ns_rc_steal = s_mac((const uint8_t *)S_RAM_START, 16u, &tag); /* чужая память: ждём S_ERR_PTR */
ns_rc_long = s_mac((const uint8_t *)"x", S_MAC_MAX + 1u, &tag); /* слишком длинно: ждём S_ERR_LEN */
RNG->CR = RNG_CR_RNGEN; /* RNG безопасный: запись незаконна, о ней сообщит TZIC */
for (;;) {
uint32_t n = s_counter_next();
ns_rc_good = s_mac((const uint8_t *)&n, sizeof n, &tag);
ns_count = n;
ns_tag = tag;
GPIOH->ODR ^= GPIO_ODR_OD7;
delay(200000u);
}
}
/* ── старт ── */
extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss, _sstack, _estack;
void Reset_Handler(void)
{
__set_MSPLIM((uint32_t)&_sstack); /* здесь это MSPLIM_NS: у каждого состояния свой стек и своя граница */
for (uint32_t *s = &_sidata, *d = &_sdata; d < &_edata;)
*d++ = *s++;
for (uint32_t *d = &_sbss; d < &_ebss;)
*d++ = 0u;
main();
for (;;) {
}
}
void Default_Handler(void)
{
for (;;) {
}
}
/* таблица векторов небезопасной части: её адрес безопасная часть пишет в VTOR_NS */
__attribute__((section(".isr_vector"), used)) static void (*const vectors[16 + LSECSSD_IRQn + 1])(void) = {
[0] = (void (*)(void))&_estack,
[1] = Reset_Handler,
[2 ... 16 + LSECSSD_IRQn] = Default_Handler,
};
Its linker file has two regions at non-secure addresses; there is no gateway section in it:
/* Раздел 2е: небезопасный проект. Адреса небезопасные, те же, что в tz_layout.h (сверяет model2e.py). */
MEMORY
{
NS_CODE (rx) : ORIGIN = 0x08010000, LENGTH = 0x1F0000 /* со страницы 8 банка 1 до конца банка 2 */
NS_RAM (rw) : ORIGIN = 0x20006000, LENGTH = 0xBA000 /* остаток SRAM1, SRAM2 и SRAM3 */
}
_estack = ORIGIN(NS_RAM) + LENGTH(NS_RAM);
_sstack = _estack - 0x1000; /* стек 4 КБ; нижняя граница уходит в MSPLIM_NS */
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > NS_CODE
.text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > NS_CODE
.data : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > NS_RAM AT > NS_CODE
_sidata = LOADADDR(.data);
.bss (NOLOAD) : { _sbss = .; *(.bss .bss.* COMMON) . = ALIGN(4); _ebss = .; } > NS_RAM
ASSERT(_ebss <= _sstack, "данные дошли до стека")
}
The build is two commands. The first runs with -mcmse and two linker options of its own:
--cmse-implib and --out-implib. The second gets from the first
the file s_veneers.o:
F="-mcpu=cortex-m33 -mthumb -mfloat-abi=soft -Os -Wall -Wextra -ffreestanding -ffunction-sections -fdata-sections"
F="$F -fno-tree-loop-distribute-patterns -DSTM32U585xx -I. -isystem /inc/dev -isystem /inc/core"
arm-none-eabi-gcc $F -mcmse s_main.c -T s.ld -nostdlib -Wl,--gc-sections -Wl,--cmse-implib -Wl,--out-implib=s_veneers.o -o s.elf -lgcc 2>&1
arm-none-eabi-gcc $F ns_main.c s_veneers.o -T ns.ld -nostdlib -Wl,--gc-sections -o ns.elf 2>&1
Both pass without warnings with -Wall -Wextra. The -lgcc option in the first
command is mandatory: cmse_check_address_range and __gnu_cmse_nonsecure_call
live in libgcc, and without it the linker finds neither. The result:
| Secure image | Non-secure image | |
|---|---|---|
| Vector table | 568 bytes from 0x0C000000 |
568 bytes from 0x08010000 |
| Code and constants | 1236 bytes | 300 bytes |
| Gateways | 32 bytes from 0x0C00FE00 |
— |
| Variables | 28 bytes from 0x30000000 |
32 bytes from 0x20006000 |
Initial MSP |
0x30005FF8 |
0x200C0000 |
The tables are the same: 16 system entries and 126 interrupts, 142 words.
The gateways. For each function with cmse_nonsecure_entry the linker placed
eight bytes in the NSC window:
0c00fe00 <s_on_violation>:
c00fe00: e97f e97f sg
c00fe04: f7f0 bace b.w c0003a4 <__acle_se_s_on_violation>
0c00fe08 <s_counter_next>:
c00fe08: e97f e97f sg
c00fe0c: f7f0 ba7e b.w c00030c <__acle_se_s_counter_next>
0c00fe10 <s_mac>:
c00fe10: e97f e97f sg
c00fe14: f7f0 ba88 b.w c000328 <__acle_se_s_mac>
...
The name s_mac now belongs to the gateway, while the function itself is renamed
to __acle_se_s_mac and lives in ordinary secure memory. A 512-byte window
holds 64 such gateways; three are used.
The file for the non-secure project (--cmse-implib --out-implib) contains
no code — only three absolute addresses:
0c00fe08 A s_counter_next
0c00fe10 A s_mac
0c00fe00 A s_on_violation
The non-secure image calls them through its own long-branch veneer; in the address word the lowest bit is 1 — the usual Thumb marker:
8010272: f000 f871 bl 8010358 <__s_on_violation_veneer>
801027e: f000 f863 bl 8010348 <__s_mac_veneer>
801028c: f000 f85c bl 8010348 <__s_mac_veneer>
80102a0: f000 f856 bl 8010350 <__s_counter_next_veneer>
80102ae: f000 f84b bl 8010348 <__s_mac_veneer>
08010348 <__s_mac_veneer>:
8010348: f85f f000 ldr.w pc, [pc] @ 801034c <__s_mac_veneer+0x4>
801034c: 0c00fe11 .word 0x0c00fe11
In the whole non-secure image there is not a single sg, bxns or blxns instruction;
in the secure one there are three sg, three bxns and one blxns.
Leaving a secure function. The whole of s_counter_next after compilation:
0c00030c <__acle_se_s_counter_next>:
c00030c: 4b05 ldr r3, [pc, #20] @ (c000324 <__acle_se_s_counter_next+0x18>)
c00030e: 4671 mov r1, lr
c000310: 6818 ldr r0, [r3, #0]
c000312: 4672 mov r2, lr
c000314: 3001 adds r0, #1
c000316: 6018 str r0, [r3, #0]
c000318: 46f4 mov ip, lr
c00031a: 4673 mov r3, lr
c00031c: f38e 8c00 msr CPSR_fs, lr
c000320: 4774 bxns lr
c000322: bf00 nop
c000324: 30000018 .word 0x30000018
r0 carries the result. The registers r1, r2, r3 and ip are overwritten with the return
address, the flags are cleared, and only then comes bxns lr.
Calling out. The transition to the non-secure part and the callback go through
one subroutine from libgcc:
0c0006b4 <__gnu_cmse_nonsecure_call>:
c0006b4: e92d 4fe0 stmdb sp!, {r5, r6, r7, r8, r9, sl, fp, lr}
c0006b8: 4627 mov r7, r4
c0006ba: 46a0 mov r8, r4
c0006bc: 46a1 mov r9, r4
c0006be: 46a2 mov sl, r4
c0006c0: 46a3 mov fp, r4
c0006c2: 46a4 mov ip, r4
c0006c4: b0a2 sub sp, #136 @ 0x88
c0006c6: ec2d 0a00 vlstm sp
c0006ca: f384 8800 msr CPSR_f, r4
c0006ce: 4625 mov r5, r4
c0006d0: 4626 mov r6, r4
c0006d2: 47a4 blxns r4
c0006d4: f3ef 8514 mrs r5, CONTROL
c0006d8: f015 0f08 tst.w r5, #8
c0006dc: bf18 it ne
c0006de: eeb0 0a40 vmovne.f32 s0, s0
c0006e2: ec3d 0a00 vlldm sp
c0006e6: b022 add sp, #136 @ 0x88
c0006e8: e8bd 8fe0 ldmia.w sp!, {r5, r6, r7, r8, r9, sl, fp, pc}
Before blxns r4 all the registers from r5 to ip hold the branch address itself
(the free ones among r0…r3 are filled with it by the caller), the flags are cleared; vlstm and vlldm
put away and restore the FPU registers. On BLXNS the core pushes the return address
onto the secure stack and writes the special value FNC_RETURN into lr — the non-secure function
does not see the real address.
2е.6 SecureFault: all the SFSR bits
Section 2б.6 met two SFSR bits out of eight. The full list from the core manual
and core_cm33.h (register 0xE000EDE4, the violation address in SFAR,
0xE000EDE8):
| Bit | Name | Cause |
|---|---|---|
| 0 | INVEP | entry from the non-secure state not on an SG, or on an SG outside NSC |
| 1 | INVIS | the integrity signature of an exception frame on the secure stack is corrupted |
| 2 | INVER | an invalid exception return in the non-secure state |
| 3 | AUVIOL | a non-secure access to a secure address |
| 4 | INVTRAN | a transition from secure to non-secure memory with an ordinary branch, not BXNS/BLXNS |
| 5 | LSPERR | an SAU or IDAU violation during lazy saving of the FPU registers |
| 6 | SFARVALID | SFAR holds a valid address |
| 7 | LSERR | an error when activating or deactivating lazy FPU state saving |
The example's handler reads SFSR and takes SFAR only if
SFARVALID is set. SecureFault itself is enabled by bit 19 in SHCSR; without it the same
causes arrive as a HardFault.
A SecureFault and a TZIC event are different things. The first is raised by the core when
an address does not pass the SAU and IDAU. The second by the memory or a peripheral when
the access got past the core but ran into the MPCBB, the TZSC or a secure
Flash page. A non-secure read at address 0x30000000 is a SecureFault
with AUVIOL. A non-secure write to the secure RNG at 0x420C0800 is a TZIC
event: by the SAU the address is non-secure, the core let it through.
2е.7 Pitfalls
A HardFault on the very first call of a secure function. The first thing to check is
a SecureFault with INVEP that is not enabled in SHCSR. Causes: the gateway window in the SAU
without the NSC bit; the gateways landed outside the window; the non-secure project is built with an old
s_veneers.o, while the secure one has since been rebuilt and the gateway addresses have moved.
Against the last one the linker has the --in-implib option: it keeps the addresses of the
previous gateways in place.
Peripherals read as zeros. The pin stayed secure
(GPIOx_SECCFGR = 0x0000FFFF after reset), or the port clock is enabled by
non-secure code while the clock bit is secure. There is no exception, and no TZIC event
for pins either — just zeros. The same with TZEN = 0 and secure
registers (2б.7).
A non-secure SAU region at an address 0x0C…. The IDAU says NSC there,
the result is NSC, not non-secure memory (2е.2). Non-secure code lives
at 0x08… and 0x20….
Secure code reads non-secure memory through the secure alias.
For Flash RM0456 (table 68) promises zeros and a TZIC event, for RAM —
an event unless SRWILADIS is set. Non-secure memory is accessed
at its non-secure address.
The SAU is configured, but DMA writes into secure RAM. The SAU is only for the core.
Memory is protected from the other masters by the MPCBB and SECWM; all three boundaries
must match.
The debugger does not see the secure part. At RDP level 0.5 secure debugging is closed: only non-secure code can be debugged. The secure part is finished at level 0.
The secure project is built for 0x08000000. With TZEN = 1 the boot
address must lie in the secure area; the factory one is 0x0C000000.
The secure project is linked for that address; 0x08… is left to the
non-secure one.
The RAM boundary cannot be moved. CFGLOCKR1 and LCK are cleared only by a reset.
If the secure part changes the partitioning on the fly, the locks are set afterwards.
The option bit check was forgotten. On a chip with the factory SECWM the whole Flash
is secure, and the non-secure image ends up in secure pages.
A non-secure access to such a page reads zeros and raises a TZIC
event. Without options_ok the secure part would also read the other part's vector table
as zeros — and would jump to address 0.
2е.8 Summary
TZENis bit 31 ofFLASH_OPTR, set at RDP 0, cleared only by an RDP regression from 1 to 0. After it everything is secure except the TZSC peripherals.- There are three boundaries: the SAU (8 regions, 32-byte granularity), Flash (
SECWM, an 8 KB page) and RAM (MPCBB, a 512-byte block, a word is 16 KB). The SAU is seen only by the core. - The example: 64 KB and 24 KB for the secure part, 1984 KB and 744 KB for the non-secure one;
SECWM1= 0…7, MPCBB1 =FFFFFFFF 0000FFFF, four SAU regions. - Peripherals: TZSC
SECCFGR3=0x0000F800for the crypto blocks, pins viaGPIOx_SECCFGR, interrupts viaNVIC_ITNS(USART1: word 1, bit 29). - The secure image is 1836 bytes, the non-secure one 868; three gateways of 8 bytes
from
0x0C00FE00, three addresses in the file for the non-secure project. - On exit the compiler overwrites
r1…r3,ipand the flags; on a call out — all registers up toip. Pointers are checked by the programmer. SFSRhas eight bits; a SecureFault is raised by the core, a TZIC event by memory and peripherals.
Full sources
build.sh — building the secure and non-secure projects for the STM32U585 and disassembling the gateways and sections.
#!/bin/sh
# Раздел 2е: сборка безопасного и небезопасного проектов для STM32U585, разбор шлюзов и переходов.
# /v — этот каталог, /inc/dev — заголовки STM32U5xx из пака, /inc/core — CMSIS Core.
set -u
W=/tmp/tz2e; rm -rf $W; mkdir -p $W; cd $W
cp /v/*.c /v/*.h /v/*.ld .
F="-mcpu=cortex-m33 -mthumb -mfloat-abi=soft -Os -Wall -Wextra -ffreestanding -ffunction-sections -fdata-sections"
F="$F -fno-tree-loop-distribute-patterns -DSTM32U585xx -I. -isystem /inc/dev -isystem /inc/core"
D="arm-none-eabi-objdump -d"
echo "== tools"; arm-none-eabi-gcc --version | head -1; arm-none-eabi-ld --version | head -1
arm-none-eabi-ld --help | grep -A1 -e "--in-implib" | tr -s " "
echo "== build"
arm-none-eabi-gcc $F -mcmse s_main.c -T s.ld -nostdlib -Wl,--gc-sections -Wl,--cmse-implib -Wl,--out-implib=s_veneers.o -o s.elf -lgcc 2>&1
echo "secure rc=$?"
arm-none-eabi-gcc $F ns_main.c s_veneers.o -T ns.ld -nostdlib -Wl,--gc-sections -o ns.elf 2>&1
echo "nonsecure rc=$?"
echo "== libgcc"
echo "no -lgcc:"; ( arm-none-eabi-gcc $F -mcmse s_main.c -T s.ld -nostdlib -Wl,--gc-sections -o nolib.elf 2>&1 ) | grep -o "undefined reference to .*" | sort | uniq -c | tr -s " "
echo "in libgcc:"; arm-none-eabi-nm "$(arm-none-eabi-gcc $F -print-libgcc-file-name)" 2>/dev/null | grep -E " T (cmse_check_address_range|__gnu_cmse_nonsecure_call)$"
echo "== implib"; arm-none-eabi-nm s_veneers.o
echo "== veneers"; $D -j .gnu.sgstubs s.elf | sed -n '/^0c00/,$p'
echo "== entry"; $D s.elf | sed -n '/<__acle_se_s_counter_next>:/,/^$/p'
echo "== jump"; $D s.elf | sed -n '/<jump_to_ns>:/,/^$/p'
echo "== nscall"; $D s.elf | sed -n '/<__gnu_cmse_nonsecure_call>:/,/^$/p'
echo "== counts"
for i in sg bxns blxns; do echo "s.elf $i $($D s.elf | grep -cE " $i( |$)")"; done
for i in sg bxns blxns; do echo "ns.elf $i $($D ns.elf | grep -cE " $i( |$)")"; done
echo "tt $($D s.elf | grep -cE " (tt|tta|ttt|ttat) ")"
echo "== nsside"; $D ns.elf | grep -E "bl .*<__s_"; $D ns.elf | sed -n '/<__s_mac_veneer>:/,/^$/p'
echo "== sections"; arm-none-eabi-size -A -x s.elf ns.elf | grep -E "^(s\.elf|ns\.elf|\.isr_vector|\.text|\.gnu\.sgstubs|\.data|\.bss)"
echo "== sizes"; arm-none-eabi-size s.elf ns.elf
echo "== symbols"
arm-none-eabi-nm s.elf | grep -E " (_estack|_sstack|__sg_start|s_counter_next|s_mac|s_on_violation|__acle_se_s_counter_next|__acle_se_s_mac|__acle_se_s_on_violation|cmse_check_address_range)$"
echo "-- ns"
arm-none-eabi-nm ns.elf | grep -E " (_estack|_sstack|Reset_Handler|s_counter_next|s_mac|s_on_violation)$"
echo "== vectors"
arm-none-eabi-objdump -s -j .isr_vector s.elf | sed -n '5p'
arm-none-eabi-objdump -s -j .isr_vector ns.elf | sed -n '5p'
echo "== end"
Next: section 2ж "BlackPill on the STM32F411: a Cortex-M4 with FPU at a BluePill price — the board, USB loading and three examples" — a board that costs as much as a BluePill but has an M4 with FPU inside.