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 или блок ОЗУ остались безопасными, небезопасный код до них не доберётся.
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:
- небезопасная запись в безопасный ресурс — событие; небезопасное исполнение оттуда — отказ шины;
- безопасное обращение к небезопасному блоку ОЗУ по безопасному адресу —
тоже событие, пока в
GTZC_MPCBBx_CRне поставлен битSRWILADIS; - непривилегированное обращение к привилегированному ресурсу — тихий отказ: читаются нули, запись пропадает, события нет.
Прерывания. После сброса все линии 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] = { >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,
};
На что смотреть:
options_ok. Биты опций программой не проверить заранее — их ставит программатор. Поэтому первым делом код сверяетTZENи обе парыSECWMс тем, под что собран, и при расхождении дальше не идёт.s_mac. Оба указателя пришли снаружи и оба проверяются на весь диапазон; сообщение сначала копируется, потом считается (2б.5). Сам счёт — заглушка, а не криптография: на её месте должен стоять HMAC на блоке HASH, который для того и оставлен безопасным.s_on_violation. Указатель на функцию — тоже указатель снаружи.cmse_nsfptr_createсбрасывает его младший бит, а тип с атрибутомcmse_nonsecure_callзаставляет компилятор вызывать его черезBLXNS.- Стеки. У каждого состояния свой
MSPи своя нижняя границаMSPLIM: уход указателя стека ниже границы ядро ловит исключением.MSP_NSбезопасная часть берёт из первого слова небезопасной таблицы векторов. Над вершиной безопасного стека лежит печать — 8 байт0xFEF5EDA5FEF5EDA5; в CMSIS её ставит__TZ_set_STACKSEAL_S. - Чтение чужой таблицы векторов идёт по небезопасному адресу
0x08010000. По руководству ядра обращение считается безопасным, если безопасен его адрес; значит, это обращение — небезопасное, и Flash его пропускает.
Файл компоновки повторяет те же три области; шлюзы уходят в отдельную
секцию .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 Итоги
TZEN— бит 31FLASH_OPTR, ставится при RDP 0, снимается только откатом RDP с 1 на 0. После него всё безопасное, кроме периферии TZSC.- Границ три: SAU (8 областей, шаг 32 байта), Flash (
SECWM, страница 8 КБ) и ОЗУ (MPCBB, блок 512 байт, слово — 16 КБ). SAU видит только ядро. - Пример: 64 КБ и 24 КБ безопасной части, 1984 КБ и 744 КБ небезопасной;
SECWM1= 0…7, MPCBB1 =FFFFFFFF 0000FFFF, четыре области SAU. - Периферия: TZSC
SECCFGR3=0x0000F800для криптоблоков, выводы —GPIOx_SECCFGR, прерывания —NVIC_ITNS(USART1: слово 1, бит 29). - Безопасный образ — 1836 байт, небезопасный — 868; три шлюза по 8 байт
с
0x0C00FE00, в файле для небезопасного проекта — три адреса. - На выходе компилятор затирает
r1…r3,ipи флаги; на вызове наружу — все регистры доip. Указатели проверяет программист. SFSR— восемь бит; SecureFault рождает ядро, событие TZIC — память и периферия.
Полные исходники
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.