Embedded HandbookApps
All sections › I. Basics
STM32U585 · level advanced · 34 min read
Open in the interactive handbook

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: SG and a branch to the real function
CMSE 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.

Partitioning the STM32U585 for two projects: Flash — secure pages 0…7 of bank 1 with a 512-byte NSC gateway window at 0x0C00FE00 and non-secure pages from 0x08010000; RAM — 24 KB at 0x30000000 and 744 KB from 0x20006000; peripherals — AES, HASH, RNG, PKA, SAES secure, the rest non-secure; on the right, who holds each boundary: the SAU, the SECWM option bits, the GTZC MPCBB and the TZSC; at the bottom, a call to s_mac through the sg gateway and the bxns return

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:

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] = { &GTZC_TZIC1->SR1, &GTZC_TZIC1->SR2, &GTZC_TZIC1->SR3, &GTZC_TZIC1->SR4 };
    static __IO uint32_t *const fcr[4] = { &GTZC_TZIC1->FCR1, &GTZC_TZIC1->FCR2, &GTZC_TZIC1->FCR3, &GTZC_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:

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

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.

Open in the interactive handbook