Embedded HandbookПриложения
Все разделы › I. Основы
STM32U585 · уровень продвинутый · 34 мин чтения
Открыть в интерактивном справочнике

2е. TrustZone на практике: SAU, GTZC, шлюз вызовов и два проекта на STM32U585

Раздел 2б показал идею TrustZone и прогнал её в эмуляторе на чужой плате: два состояния ядра, окно NSC, команда SG, SecureFault. Раздел 2д разобрал сам U585 — выводы, питание, такт. Здесь то, чего в них нет: как размечается настоящий STM32U585, когда бит TZEN поставлен. Границ у него не одна, а три, и у каждой свой регистр и свой шаг. В конце — два проекта, которые собираются: безопасный со счётчиком и ключом и небезопасный, который ими пользуется.

Опоры: справочное руководство RM0456 (ред. 4) и руководство по ядру Cortex-M33 (Arm 100235) из пака Keil STM32U5xx_DFP 2.2.1; заголовки stm32u585xx.h и partition_stm32u585xx.h оттуда же, core_cm33.h и cmsis_gcc.h из CMSIS 5.9.0; пакет платы B-U585I-IOT02A_BSP 1.1.0. Оба проекта собраны arm-none-eabi-gcc 12.2 с флагом -mcmse, шлюзы разобраны objdump. Числа регистров считает модель на ПК и сверяет с заголовками. QEMU U5 не моделирует, на плате не запускалось.

Как это называется в документации

Оригинал Перевод
TZEN option bit бит опций, включающий TrustZone
Secure watermark (SECWM) безопасная область Flash «от страницы до страницы», задаётся битами опций
Block-based (SECBB, MPCBB) атрибут по блокам: бит на страницу Flash или на 512 байт ОЗУ
Super-block 32 блока MPCBB подряд — одно слово регистра, 16 КБ
Securable peripheral периферия, которую делает безопасной TZSC
TrustZone-aware peripheral периферия, которая сама различает безопасные и небезопасные обращения
Illegal access event незаконное обращение: флаг в TZIC и, если разрешено, прерывание
Secure gateway veneer шлюз: SG и переход на настоящую функцию
CMSE import library файл с адресами шлюзов для небезопасного проекта
Stack seal «печать»: 8 байт над вершиной безопасного стека

2е.1 Бит TZEN: что меняется после сброса

TrustZone во всех STM32U5 с завода выключен (2б.7). Включает его бит TZEN — бит 31 регистра FLASH_OPTR, и поставить его можно только при уровне защиты чтения RDP 0. После этого камень выходит из сброса другим:

Что Состояние после сброса при TZEN = 1
Ядро безопасное состояние; адрес загрузки обязан быть безопасным
Карта памяти ядра SAU выключен — все адреса безопасные
Flash вся безопасная: SECWMx_PSTRT = 0, SECWMx_PEND = 0x7F в обоих банках
ОЗУ все блоки MPCBB безопасные и привилегированные (0xFFFFFFFF)
Выводы все безопасные: GPIOx_SECCFGR = 0x0000FFFF
Остальная периферия небезопасная: GTZC_TZSC_SECCFGRx = 0
Прерывания все безопасные; события TZIC замаскированы

Отсюда порядок работы безопасной прошивки: она стартует первой, получает всё и раздаёт небезопасной части то, что той положено. Не раздала — небезопасная часть не запустится вовсе.

Откуда старт. Адрес загрузки берётся из поля SECBOOTADD0 — биты 31:7 регистра FLASH_SECBOOTADD0R, шаг 128 байт. Заводское значение 0x0C00007C указывает на 0x0C000000: начало Flash по безопасному псевдониму. Поэтому безопасный проект компонуют под 0x0C000000, а не под привычный 0x08000000.

Обратный путь. Снять TZEN можно только вместе с откатом RDP с уровня 1 на уровень 0, а откат стирает Flash и всё ОЗУ (подробнее — 2б.7). Между ними есть уровень 0,5: на нём отладка безопасной части уже недоступна, а небезопасная отлаживается, пока ядро в небезопасном состоянии.


2е.2 Три границы: SAU, биты опций Flash и MPCBB

Атрибут адреса из 2б.2 — это только взгляд ядра. RM0456 оговаривает прямо: настройки SAU и IDAU действуют на Cortex-M33 и не действуют на других ведущих шины, например DMA. Поэтому у U5 каждая память сторожит себя ещё и сама:

Граница Кто держит Шаг Где хранится
Взгляд ядра SAU поверх IDAU 32 байта регистры ядра, после сброса пусто
Flash SECWM и SECBB страница 8 КБ SECWM — биты опций, SECBB — регистры
ОЗУ GTZC MPCBB блок 512 байт регистры, после сброса всё безопасно

Все три должны сказать одно и то же. Если SAU объявил область небезопасной, а страница Flash или блок ОЗУ остались безопасными, небезопасный код до них не доберётся.

Разметка STM32U585 под два проекта: Flash — безопасные страницы 0…7 банка 1 с окном шлюзов NSC 512 байт по 0x0C00FE00 и небезопасные страницы с 0x08010000; ОЗУ — 24 КБ по 0x30000000 и 744 КБ с 0x20006000; периферия — AES, HASH, RNG, PKA, SAES безопасные, остальное небезопасное; справа — кто держит каждую границу: SAU, биты опций SECWM, GTZC MPCBB и TZSC; внизу вызов s_mac через шлюз sg и возврат bxns

SAU и IDAU. У Cortex-M33 в U5 восемь областей SAU. Область — пара регистров: SAU_RBAR с адресом начала и SAU_RLAR с адресом конца; младшие пять бит адреса в них не хранятся, поэтому шаг — 32 байта. В SAU_RLAR бит 0 включает область, бит 1 — признак NSC. Номер области выбирает SAU_RNR. Всё, что не покрыто областями, остаётся безопасным.

IDAU у U5 задан жёстко: небезопасные адреса 0x08…, 0x20…, 0x40… и их двойники 0x0C…, 0x30…, 0x50…, которые IDAU помечает как NSC. Итог складывается по таблице 8 RM0456:

IDAU SAU: безопасная SAU: NSC SAU: небезопасная
небезопасный адрес (0x08…, 0x20…, 0x40…) безопасная NSC небезопасная
NSC (0x0C…, 0x30…, 0x50…) безопасная NSC NSC

Последняя клетка — ловушка. Небезопасная область SAU, по ошибке заданная по адресу 0x0C…, даёт не небезопасную память, а окно для шлюзов.

Flash. Безопасную область каждого банка задаёт пара полей в битах опций: SECWMx_PSTRT и SECWMx_PEND — первая и последняя страница (биты 6:0 и 22:16 регистра FLASH_SECWMxR1). Если начало больше конца, безопасной области в банке нет. Вторая возможность — FLASH_SECBBxRy: по биту на страницу, в заголовке U585 по четыре регистра на банк. Эти биты обычные, не опции: после сброса нули, безопасный код ставит их на ходу и может прибавить безопасную страницу, но не отнять её у SECWM.

ОЗУ. MPCBB делит память на блоки по 512 байт. Слово SECCFGR — 32 блока, то есть 16 КБ; RM называет его суперблоком. У U585 три контроллера в GTZC1: MPCBB1 для SRAM1 (192 КБ, 384 блока, 12 слов), MPCBB2 для SRAM2 (64 КБ, 128 блоков, 4 слова) и MPCBB3 для SRAM3 (512 КБ, 1024 блока, 32 слова). Регистр CFGLOCKR1 запирает слова до сброса — по биту на слово.

Разметка примера. Безопасной части отдано немного: 64 КБ Flash и 24 КБ ОЗУ. Все адреса и значения регистров собраны в одном файле для обоих проектов:

/* Раздел 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

Числа в нём не набраны руками. Скрипт model2e.py берёт шесть диапазонов и считает остальное по определениям полей из core_cm33.h и stm32u585xx.h:

Что Расчёт Значение
SAU 0, шлюзы 0x0C00FE00…0x0C00FFFF, NSC RBAR = 0x0C00FE00, RLAR = 0x0C00FFE3
SAU 1, Flash 0x08010000…0x081FFFFF RBAR = 0x08010000, RLAR = 0x081FFFE1
SAU 2, ОЗУ 0x20006000…0x200BFFFF RBAR = 0x20006000, RLAR = 0x200BFFE1
SAU 3, периферия 0x40000000…0x4FFFFFFF RBAR = 0x40000000, RLAR = 0x4FFFFFE1
SECWM1 64 КБ / 8 КБ = 8 страниц PSTRT = 0, PEND = 7, поля регистра 0x00070000
SECWM2 безопасной области нет PSTRT = 0x7F > PEND = 0
MPCBB1 24 КБ / 512 = 48 блоков слово 0 = 0xFFFFFFFF, слово 1 = 0x0000FFFF, слова 2…11 — нули
MPCBB2, MPCBB3 целиком небезопасные 4 и 32 нулевых слова

Небезопасной части остаётся 248 страниц Flash (1984 КБ) и 744 КБ ОЗУ. Граница ОЗУ в 24 КБ нарочно не кратна 16 КБ: она делит слово 1 пополам, и видно, что MPCBB режет мельче, чем суперблоками. Окно шлюзов — 512 байт, а не страница: SAU позволяет шаг 32 байта, и чем уже окно, тем меньше места для случайной SG (2б.2).

Для сравнения — разметка из пака: файл partition_gen.h, который подключает partition_stm32u585xx.h. Области там описаны макросами SAU_INIT_REGIONn (включить), SAU_INIT_STARTn, SAU_INIT_ENDn и SAU_INIT_NSCn; функция TZ_SAU_Setup превращает их в те же записи RNR, RBAR, RLAR. Включено шесть областей: шлюзы 0x0C0FE000…0x0C0FFFFF (8 КБ в конце банка 1), весь банк 2 с 0x08100000, SRAM3 с 0x20040000, периферия, внешняя память 0x60000000…0x9FFFFFFF и системная память 0x0BF90000…0x0BFA8FFF. Там Flash делится пополам по границе банков — проще, но безопасной части достаётся мегабайт.


2е.3 Периферия и прерывания: TZSC, TZIC, NVIC_ITNS

Периферия у U5 двух сортов.

Которую делает безопасной TZSC. На каждый блок — бит в регистрах GTZC_TZSC_SECCFGR1…SECCFGR3; после сброса нули, то есть блок небезопасный. В примере безопасными объявлены криптоблоки: AES, HASH, RNG, PKA и SAES — биты 11…15 регистра SECCFGR3, маска 0x0000F800. Рядом лежат PRIVCFGRx — то же деление, но на привилегированный и непривилегированный доступ; работает и без TrustZone. Бит LCK в GTZC_TZSC_CR запирает оба набора до сброса.

Которая различает обращения сама. Это GPIO, RCC, PWR, Flash, EXTI, DMA, RTC и TAMP, SYSCFG, сам GTZC. У них безопасность задаётся внутри блока. Главное для первого запуска — выводы: GPIOx_SECCFGR после сброса равен 0x0000FFFF, все выводы безопасные. Небезопасный код, обращаясь к такому выводу, читает нули, а запись пропадает — без всякого исключения. Вторая половина той же ловушки: пока в порту есть хоть один безопасный вывод, бит такта этого порта в RCC тоже безопасный. Поэтому в примере такт порта H включает безопасная часть и она же отдаёт вывод PH7 — на плате B-U585I-IOT02A там зелёный светодиод.

TZIC собирает незаконные обращения. У каждого источника три бита с одинаковым номером: разрешение в GTZC_TZIC_IERx, флаг в SRx, сброс флага в FCRx. Все события идут в одно безопасное прерывание GTZC (номер 8); после сброса все замаскированы. Что считается незаконным, RM0456 перечисляет в 5.4.2:

Прерывания. После сброса все линии NVIC безопасные. Линию передаёт небезопасной части бит в NVIC_ITNS: слово — номер прерывания, делённый на 32, бит — остаток. Для USART1 (номер 61) это NVIC_ITNS[1], бит 29; в CMSIS это делает NVIC_SetTargetState. Обработчик такой линии берётся из небезопасной таблицы векторов: у каждого состояния свой VTOR. Свой VTOR_S безопасная часть ставит при старте, небезопасный VTOR_NS пишет через псевдоним 0xE002ED08 перед переходом.


2е.4 Безопасный проект

Заголовок, общий для обоих проектов, — три функции и коды ошибок:

/* Раздел 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

Весь безопасный проект — один файл. Сверху — _Static_assert: значения из tz_layout.h пересчитываются из масок заголовка, так что опечатка в адресе не соберётся. Дальше функции-входы, обработчики, настройка и старт.

/* Раздел 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,
};

На что смотреть:

Файл компоновки повторяет те же три области; шлюзы уходят в отдельную секцию .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 Небезопасный проект, сборка и шлюзы

Небезопасный проект про TrustZone почти ничего не знает. Он собирается без -mcmse, и заголовок сам подставляет небезопасные адреса периферии. Три безопасные функции для него — обычные внешние функции из 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,
};

Его файл компоновки — две области по небезопасным адресам; секции шлюзов в нём нет:

/* Раздел 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, "данные дошли до стека")
}

Сборка — две команды. Первая идёт с -mcmse и двумя своими ключами компоновщика: --cmse-implib и --out-implib. Вторая получает от первой файл 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

Обе проходят без предупреждений с -Wall -Wextra. Ключ -lgcc в первой команде обязателен: cmse_check_address_range и __gnu_cmse_nonsecure_call лежат в libgcc, и без него компоновщик не находит обе. Что получилось:

Безопасный образ Небезопасный образ
Таблица векторов 568 байт с 0x0C000000 568 байт с 0x08010000
Код и константы 1236 байт 300 байт
Шлюзы 32 байта с 0x0C00FE00 —
Переменные 28 байт с 0x30000000 32 байта с 0x20006000
Начальный MSP 0x30005FF8 0x200C0000

Таблицы одинаковые: 16 системных записей и 126 прерываний, 142 слова.

Шлюзы. На каждую функцию с cmse_nonsecure_entry компоновщик положил в окно NSC восемь байт:

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>
    ...

Имя s_mac теперь принадлежит шлюзу, а сама функция переименована в __acle_se_s_mac и лежит в обычной безопасной памяти. В окне 512 байт поместится 64 таких шлюза; занято три.

Файл для небезопасного проекта (--cmse-implib --out-implib) кода не содержит — только три абсолютных адреса:

0c00fe08 A s_counter_next
0c00fe10 A s_mac
0c00fe00 A s_on_violation

Небезопасный образ зовёт их через собственную прокладку дальнего перехода; в слове адреса младший бит 1 — обычный признак Thumb:

 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

Во всём небезопасном образе нет ни одной команды sg, bxns или blxns; в безопасном — три sg, три bxns и одна blxns.

Выход из безопасной функции. Вся s_counter_next после компиляции:

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 уходит результат. Регистры r1, r2, r3 и ip затёрты адресом возврата, флаги сброшены, и только потом bxns lr.

Вызов наружу. Переход в небезопасную часть и обратный вызов идут через одну подпрограмму из 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}

Перед blxns r4 во всех регистрах от r5 до ip лежит сам адрес перехода (свободные из r0…r3 им же заполняет вызывающий), флаги сброшены; vlstm и vlldm убирают и возвращают регистры FPU. Ядро при BLXNS кладёт адрес возврата на безопасный стек, а в lr пишет особое значение FNC_RETURN — настоящего адреса небезопасная функция не видит.


2е.6 SecureFault: все биты SFSR

Раздел 2б.6 встретил два бита SFSR из восьми. Полный список по руководству ядра и core_cm33.h (регистр 0xE000EDE4, адрес нарушения — в SFAR, 0xE000EDE8):

Бит Имя Причина
0 INVEP вход из небезопасного состояния не на SG или на SG вне NSC
1 INVIS испорчена подпись кадра исключения на безопасном стеке
2 INVER неверный возврат из исключения в небезопасном состоянии
3 AUVIOL небезопасное обращение к безопасному адресу
4 INVTRAN переход из безопасной памяти в небезопасную обычным переходом, не BXNS/BLXNS
5 LSPERR нарушение SAU или IDAU при отложенном сохранении регистров FPU
6 SFARVALID в SFAR лежит верный адрес
7 LSERR ошибка при включении или выключении отложенного сохранения FPU

Обработчик примера читает SFSR и берёт SFAR, только если поднят SFARVALID. Сам SecureFault включён битом 19 в SHCSR; без него те же причины приходят как HardFault.

SecureFault и событие TZIC — разные вещи. Первое рождает ядро, когда адрес не проходит по SAU и IDAU. Второе — память или периферия, когда обращение прошло мимо ядра, но упёрлось в MPCBB, TZSC или безопасную страницу Flash. Небезопасное чтение по адресу 0x30000000 — SecureFault с AUVIOL. Небезопасная запись в безопасный RNG по 0x420C0800 — событие TZIC: адрес по SAU небезопасный, ядро его пропустило.


2е.7 Грабли

HardFault на первом же вызове безопасной функции. Первым проверяют SecureFault с INVEP, не включённый в SHCSR. Причины: окно шлюзов в SAU без бита NSC; шлюзы легли не в окно; небезопасный проект собран со старым s_veneers.o, а безопасный с тех пор пересобран и адреса шлюзов уехали. От последнего у компоновщика есть ключ --in-implib: он держит адреса прежних шлюзов на месте.

Периферия читается нулями. Вывод остался безопасным (GPIOx_SECCFGR = 0x0000FFFF после сброса), или такт порта включает небезопасный код, а бит такта безопасный. Исключения нет, события TZIC для выводов тоже нет — просто нули. То же при TZEN = 0 с безопасными регистрами (2б.7).

Небезопасная область SAU по адресу 0x0C…. IDAU там говорит NSC, итог — NSC, а не небезопасная память (2е.2). Небезопасный код живёт по 0x08… и 0x20….

Безопасный код читает небезопасную память по безопасному псевдониму. Для Flash RM0456 (таблица 68) обещает нули и событие TZIC, для ОЗУ — событие, пока не поставлен SRWILADIS. К небезопасной памяти обращаются по её небезопасному адресу.

SAU настроен, а DMA пишет в безопасное ОЗУ. SAU — только для ядра. От остальных ведущих память защищают MPCBB и SECWM; все три границы должны совпадать.

Отладчик не видит безопасную часть. На уровне RDP 0,5 безопасная отладка закрыта: отлаживается только небезопасный код. Безопасную часть доводят на уровне 0.

Безопасный проект собран под 0x08000000. При TZEN = 1 адрес загрузки обязан лежать в безопасной области, заводской — 0x0C000000. Под этот адрес и компонуют безопасный проект; 0x08… остаётся небезопасному.

Границу ОЗУ не сдвинуть. CFGLOCKR1 и LCK снимаются только сбросом. Если безопасная часть меняет разметку на ходу, замки ставят после.

Проверка битов опций забыта. На камне с заводскими SECWM вся Flash безопасная, и небезопасный образ оказывается в безопасных страницах. Небезопасное обращение к такой странице читает нули и поднимает событие TZIC. Без options_ok безопасная часть прочла бы нулями и чужую таблицу векторов — и перешла бы по адресу 0.


2е.8 Итоги

Полные исходники

build.sh — сборка безопасного и небезопасного проектов для STM32U585 и разбор шлюзов и секций.

#!/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"

Дальше: раздел 2ж «BlackPill на STM32F411: Cortex-M4 с FPU по цене BluePill — плата, загрузка по USB и три примера» — плата, которая стоит как BluePill, а внутри M4 с FPU.

Открыть в интерактивном справочнике