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

2б. STM32U5 и TrustZone: чем новое поколение отличается от F4

STM32F405 — рабочая лошадь десятилетия: Cortex-M4, 168 МГц, мегабайт Flash. STM32U585 из новой линейки U5 по частоте почти такой же, но внутри другое ядро — Cortex-M33 — и у этого ядра есть то, чего у M4 нет совсем: TrustZone, аппаратное деление программы на «безопасную» и «небезопасную» части. Раздел о том, что даёт U5 по сравнению с F4 и как TrustZone устроен изнутри — с живым опытом в эмуляторе.

Цифры U585 взяты из его даташита, поведение TrustZone на U5 — из справочного руководства по семейству, правила переходов и регистры — из руководства по ядру Cortex-M33, параметры F405 — из описания пакета поддержки для Keil. Опыт собран компилятором arm-none-eabi-gcc и прогнан в QEMU на плате с Cortex-M33 (mps2-an505). Сверяет _verify/tz2b_check.py.

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

Оригинал Перевод
Secure / Non-secure state безопасное / небезопасное состояние
Non-secure callable (NSC) область, откуда небезопасный код может войти в безопасный
SAU — Security Attribution Unit блок атрибутов безопасности (настраивается программой)
IDAU — Implementation Defined Attribution Unit атрибуты безопасности, заданные производителем кристалла
SG — Secure Gateway команда-шлюз: вход в безопасное состояние
veneer «прокладка»: шлюз из SG и перехода на настоящую функцию
CMSE расширения языка C для безопасной части (-mcmse)

2б.1 U585 против F405: что изменилось

STM32F405RG STM32U585CI
Ядро Cortex-M4 с FPU Cortex-M33 с FPU и TrustZone
Частота до 168 МГц до 160 МГц, 240 DMIPS
Flash 1024 КБ 2 МБ
ОЗУ 192 КБ 786 КБ
Потребление в работе 238 мкА/МГц (40 мА на 168 МГц) 19,5 мкА/МГц при 3,3 В
Stop 2 (16 КБ ОЗУ сохранены) Stop: 280 мкА, ОЗУ сохранено целиком 4,0 мкА
Standby / Shutdown 2,2 мкА / нет 210 нА / 160 нА

Главное здесь не частота — она почти та же, — а три вещи: вчетверо больше ОЗУ, режимы сна с потреблением в микро- и наноамперах (U — «ultra-low-power») и TrustZone. F405 выбирают, когда нужен проверенный камень с огромной базой примеров; U5 — когда устройство живёт от батареи, должно хранить ключи и обновляться по воздуху так, чтобы подменённая прошивка не добралась до секретов.

Как читать маркировку STM32U585CIU6: U — ultra-low-power, 585 — подсемейство с USB OTG и аппаратным AES, C — 48 выводов, I — 2 МБ Flash, U — корпус UFQFPN, 6 — промышленный диапазон −40…85 °C.

Маркировка STM32U585CIU6 по буквам и U585 против F405: частота почти та же, Flash вдвое, ОЗУ вчетверо, сон от 4,0 мкА до 160 нА


2б.2 Идея TrustZone: два мира в одном ядре

У Cortex-M33 с TrustZone каждый адрес имеет атрибут безопасности: безопасный (Secure), небезопасный (Non-secure) или безопасный с правом входа снаружи (NSC). Ядро в каждый момент находится в одном из двух состояний. В безопасном ему доступно всё; в небезопасном обращение к безопасному адресу блокируется и вызывает исключение.

Атрибут адреса собирается из двух источников:

Если источники расходятся, побеждает более строгий атрибут. Это правило в опыте ниже сработало буквально: SAU объявил окно шлюзов NSC, а IDAU платы считал его просто безопасным — и вход не состоялся.

Зачем отдельный тип NSC? Команда-шлюз SG разрешена только в нём. Если бы её разрешили во всей безопасной памяти, любая константа, случайно совпавшая с кодом SG, стала бы дверью внутрь. Ограничив шлюзы узким окном, производитель прошивки точно знает, где у него двери.


2б.3 Как ходят между мирами

Откуда Куда переходит программа Что происходит
безопасное небезопасный адрес переход только командой BXNS/BLXNS с младшим битом адреса 0; иначе SecureFault
небезопасное безопасный адрес в окне NSC если там стоит SG — вход в безопасное состояние; если нет — SecureFault
небезопасное безопасный адрес вне NSC SecureFault

Обратно из безопасной функции возвращаются тоже BXNS. Никаких системных вызовов, как в большой ОС: вызов безопасной функции — это обычный BL, просто по адресу шлюза.


2б.4 Опыт: две прошивки в эмуляторе

Опыт — две отдельные программы, как и на настоящем U5.

Безопасная часть отдаёт наружу две функции:

uint32_t __attribute__((cmse_nonsecure_entry)) s_sign(uint32_t x)
{
    s_calls++;
    return x ^ secret_key;                   /* «подпись» — наружу уходит результат, не ключ */
}

void __attribute__((cmse_nonsecure_entry)) s_print(const char *msg)
{
    /* указатель пришёл из небезопасного мира: читать только проверенное и только из своей копии */
    char buf[33];
    for (;;) {
        if (!cmse_check_address_range((void *)msg, 32, CMSE_NONSECURE)) {
            puts_s("s_print: отказ — адрес не небезопасный\n");
            return;
        }
        int n = 0;
        while (n < 32 && (buf[n] = msg[n]) != 0)   /* копия: проверенное снаружи уже не подменить */
            n++;
        buf[n] = 0;
        puts_s(buf);
        if (n < 32)
            return;                          /* строка кончилась внутри проверенных 32 байт */
        msg += 32;                           /* следующие 32 байта — снова через проверку */
    }
}

Прогон:

S: старт в безопасном состоянии
S: SAU и MPC настроены, BLK_MAX ssram-0 0x0000007f
S: переход в небезопасную часть
NS: запуск в небезопасном состоянии
NS: s_sign(0x12345678) = 0x17d8449f
NS: подсовываю s_print безопасный адрес 0x38000000
s_print: отказ — адрес не небезопасный
NS: читаю безопасное ОЗУ 0x38001000
SecureFault: SFSR=0x00000008 SFAR=0x00000000
done

Три вещи произошли по плану. Небезопасная часть получила «подпись» 0x17d8449f, но не ключ. Попытка подсунуть безопасной функции указатель на безопасную память отбита самой функцией. Прямое чтение безопасного ОЗУ остановило ядро исключением SecureFault.

TrustZone в опыте: безопасный мир, небезопасный и узкая дверь между ними

Размеры: безопасная часть — 1293 байта кода, небезопасная — 482.


2б.5 Что сделал компилятор: шлюз и чистка регистров

Флаг -mcmse и атрибут cmse_nonsecure_entry заставляют компилятор и компоновщик сделать для каждой такой функции две вещи.

Шлюз в окне NSC. По адресу 0x10100000 компоновщик положил 8 байт:

10100000 <s_sign>:
10100000:   e97f e97f   sg
10100004:   f700 b86a   b.w 100000dc <__acle_se_s_sign>

Сама функция живёт по обычному безопасному адресу 0x100000dc под именем __acle_se_s_sign, и прыгнуть на неё напрямую из небезопасного мира нельзя — там нет SG, и адрес не в окне NSC. Шлюзы компоновщик собирает в секцию .gnu.sgstubs, а скрипт безопасной части ставит её в окно NSC. Ключи -Wl,--cmse-implib -Wl,--out-implib=veneers.o пишут файл для небезопасной части. Файл veneers.o содержит только адреса шлюзов: s_sign = 0x10100000, s_print = 0x10100008.

Чистка при выходе. Перед bxns lr функция перезаписывает r1–r3 и ip значением lr и сбрасывает флаги (msr CPSR_fs, lr). В r0 уходит результат, а всё, что осталось в регистрах от вычислений с ключом, — нет. Без этой чистки ключ мог бы утечь через «мусор» в регистрах.

Проверка указателей — ваша работа. Компилятор не знает, куда указывает msg. Функция cmse_check_address_range спрашивает ядро (командой TT), доступен ли весь диапазон небезопасному миру; без неё небезопасная часть заставила бы безопасную прочитать для неё секрет. Проверяют ровно то, что функция прочтёт: если проверить 32 байта, а печатать до нулевого байта, строка без нуля у самой границы небезопасного ОЗУ уведёт печать в безопасную память. Поэтому s_print читает кусками по 32 байта, каждый — только после проверки, и печатает свою копию: подменить проверенную строку из прерывания небезопасной части уже нельзя.


2б.6 SecureFault и регистр SFSR

SecureFault — отдельное исключение Cortex-M33; чтобы оно не превращалось в HardFault, его включают битом SECUREFAULTENA (бит 19 в SHCSR). Причину показывает SFSR по адресу 0xE000EDE4:

Бит Имя Когда
0 INVEP небезопасный код вошёл в безопасный по адресу без шлюза или вне NSC
3 AUVIOL небезопасный код обратился к безопасному адресу

В опыте прямое чтение дало SFSR = 0x00000008 — AUVIOL. Второй прогон — та же прошивка, но без бита CODENSC в регистре NSCCFG платы — упал на первом же вызове:

S: переход в небезопасную часть
SecureFault: SFSR=0x00000001 SFAR=0x00000000

Это INVEP: SAU считал окно 0x10100000 областью NSC, но IDAU платы — просто безопасной, а побеждает более строгий. Руководство по ядру описывает эту причину прямо: цель перехода — команда SG, но нет области SAU или IDAU с флагом NSC.

SFAR в обоих случаях — ноль: адрес нарушения там действителен, только когда поднят бит SFARVALID (бит 6); здесь он не поднят. Это особенность эмулятора: на кристалле при AUVIOL на чтении данных ядро записывает в SFAR адрес обращения и ставит SFARVALID. Поэтому сначала смотрят этот бит, а причину определяют по битам SFSR.


2б.7 TrustZone на настоящем STM32U5

Опыт показывает механику ядра; у U5 вокруг неё своя обвязка.

Карта атрибутов безопасности: в опыте на AN505 и на STM32U5 одна и та же память видна по небезопасному и по безопасному адресу; в опыте окно NSC 0x10100000 со шлюзами s_sign 0x10100000 и s_print 0x10100008, отказы на 0x38000000 и 0x38001000; у U5 Flash 0x08000000 и 0x0C000000, ОЗУ 0x20000000 и 0x30000000, периферия 0x40000000 и 0x50000000

Обычный путь — начать с TZEN = 0: U5 тогда работает как очень экономный Cortex-M33 без разделения, и все примеры собираются одним проектом. Два проекта (безопасный и небезопасный) нужны только тогда, когда TrustZone действительно включён.


2б.8 Когда брать U5, а когда F4

Задача Выбор
Прибор от батарейки, годами в сне U5: Stop 2 от 4,0 мкА, Standby 210 нА
Хранение ключей, защищённое обновление U5 с TrustZone
Много ОЗУ под буферы, графику, модель U5: 786 КБ
Готовый код и библиотеки под F4, серия идёт F405
Первые шаги с Cortex-M F4 или F1: меньше сущностей

Код на HAL переносится между сериями легче, чем на регистрах (раздел 5), но тактирование (раздел 6) и режимы сна (раздел 10) у U5 свои — их перечитывают заново.


2б.9 Грабли

Небезопасная часть падает на первом же вызове с INVEP (2б.6). SAU разметил NSC, а атрибут от производителя остался безопасным; побеждает строгий. На U5 шлюзы кладут в NSC-псевдоним Flash, на плате — включают соответствующий бит.

Компоновщик выкинул секцию шлюзов (2б.5). Если в .gnu.sgstubs при разметке ещё нечего класть, ld сочтёт её пустой; символ внутри секции её удерживает.

Безопасная функция доверилась указателю (2б.5). Всё, что пришло из небезопасного мира, проверяют cmse_check_address_range, иначе безопасная часть сама прочитает секрет для атакующего. Проверенный диапазон должен совпадать с прочитанным, а работают с копией — иначе строку без нуля или подменённую после проверки дочитают в безопасную память.

TrustZone «не работает», но и не мешает (2б.7). При TZEN = 0 безопасные регистры Flash — нули и пустая запись. Первым делом смотрят бит опций.

Причина в SFAR — ноль (2б.6). Адрес там только при поднятом SFARVALID; причину читают по битам SFSR.


2б.10 Итоги

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

s_main.c — безопасная прошивка опыта 2б.4: разметка SAU, шлюзы NSC и переход в небезопасную часть.

/* Раздел 2б: TrustZone на Cortex-M33 — безопасная часть. QEMU mps2-an505 (ядро Cortex-M33 с TrustZone).
 * Старт всегда в безопасном состоянии. Прошивка:
 *   1) делит память атрибутами SAU: код и ОЗУ небезопасной части, окно шлюзов (NSC);
 *   2) разрешает небезопасный доступ контроллерам памяти платы (MPC) к тем же областям;
 *   3) ставит небезопасной части таблицу векторов и стек и переходит в неё командой BLXNS.
 * Небезопасная часть видит только функции, помеченные cmse_nonsecure_entry, — через шлюз SG. */
#include <arm_cmse.h>
#include <stdint.h>

#define REG(a) (*(volatile uint32_t *)(a))
/* UART0 платы (CMSDK APB UART), безопасный адрес */
#define UART0 0x50200000u
#define UART_DATA REG(UART0 + 0x00)
#define UART_STATE REG(UART0 + 0x04)
#define UART_CTRL REG(UART0 + 0x08)
#define UART_BAUDDIV REG(UART0 + 0x10)
/* SAU — блок атрибутов безопасности ядра */
#define SAU_CTRL REG(0xE000EDD0)
#define SAU_RNR REG(0xE000EDD8)
#define SAU_RBAR REG(0xE000EDDC)
#define SAU_RLAR REG(0xE000EDE0)
#define SFSR REG(0xE000EDE4)
#define SFAR REG(0xE000EDE8)
#define SHCSR REG(0xE000ED24)
#define VTOR_NS REG(0xE002ED08)            /* VTOR небезопасного состояния — через его псевдоним */

#define NS_CODE 0x00200000u                 /* небезопасный код: вторая половина ssram-0, небезопасный псевдоним */
#define NS_RAM 0x28100000u                  /* небезопасное ОЗУ: вторая половина ssram-1 */

static void putc_s(char c)
{
    while (UART_STATE & 1u) {
    }
    UART_DATA = (uint32_t)c;
}

static void puts_s(const char *s)
{
    while (*s)
        putc_s(*s++);
}

static void puthex_s(uint32_t v)
{
    static const char h[] = "0123456789abcdef";
    puts_s("0x");
    for (int i = 28; i >= 0; i -= 4)
        putc_s(h[(v >> i) & 0xF]);
}

/* ── функции, которые небезопасная часть может вызвать ── */
static uint32_t secret_key = 0x5EC12E7u;    /* живёт только в безопасной части, наружу не отдаётся */
volatile uint32_t s_calls;

uint32_t __attribute__((cmse_nonsecure_entry)) s_sign(uint32_t x)
{
    s_calls++;
    return x ^ secret_key;                   /* «подпись» — наружу уходит результат, не ключ */
}

void __attribute__((cmse_nonsecure_entry)) s_print(const char *msg)
{
    /* указатель пришёл из небезопасного мира: читать только проверенное и только из своей копии */
    char buf[33];
    for (;;) {
        if (!cmse_check_address_range((void *)msg, 32, CMSE_NONSECURE)) {
            puts_s("s_print: отказ — адрес не небезопасный\n");
            return;
        }
        int n = 0;
        while (n < 32 && (buf[n] = msg[n]) != 0)   /* копия: проверенное снаружи уже не подменить */
            n++;
        buf[n] = 0;
        puts_s(buf);
        if (n < 32)
            return;                          /* строка кончилась внутри проверенных 32 байт */
        msg += 32;                           /* следующие 32 байта — снова через проверку */
    }
}

/* SecureFault: небезопасная часть полезла в безопасную память */
void SecureFault_Handler(void)
{
    puts_s("SecureFault: SFSR=");
    puthex_s(SFSR);
    puts_s(" SFAR=");
    puthex_s(SFAR);
    puts_s("\ndone\n");
    for (;;) {
    }
}

static void mpc_all_ns(uint32_t mpc, uint32_t from_blk)
{
    /* MPC платы: BLK_MAX — число слов таблицы, BLK_IDX — номер слова, BLK_LUT — по биту на блок (1 — небезопасный) */
    uint32_t max = REG(mpc + 0x10);
    for (uint32_t i = from_blk; i < max; i++) {
        REG(mpc + 0x18) = i;
        REG(mpc + 0x1C) = 0xFFFFFFFFu;
    }
}

int main(void)
{
    UART_BAUDDIV = 16;
    UART_CTRL = 1;                           /* TX */
    puts_s("S: старт в безопасном состоянии\n");

    /* SAU: 0 — код небезопасной части, 1 — её ОЗУ, 2 — окно шлюзов (NSC) */
    SAU_RNR = 0; SAU_RBAR = NS_CODE; SAU_RLAR = (NS_CODE + 0x1FFFE0u) | 1u;
    SAU_RNR = 1; SAU_RBAR = NS_RAM; SAU_RLAR = (NS_RAM + 0xFFFE0u) | 1u;
    SAU_RNR = 2; SAU_RBAR = 0x10100000u; SAU_RLAR = (0x10100000u + 0xFE0u) | (1u << 1) | 1u;   /* NSC */
    SAU_CTRL = 1u;
    /* IDAU платы разрешает NSC в области кода 0x1xxxxxxx только битом CODENSC регистра NSCCFG;
     * атрибут адреса — строжайший из двух: SAU говорит «NSC», IDAU — тоже должен */
#ifndef NO_CODENSC
    REG(0x50080014u) |= 1u;               /* NSCCFG.CODENSC */
#endif
    /* MPC платы для ssram-0 и ssram-1: вторые половины — небезопасные */
    uint32_t b0 = REG(0x58007000u + 0x10), b1 = REG(0x58008000u + 0x10);
    mpc_all_ns(0x58007000u, b0 / 2);
    mpc_all_ns(0x58008000u, b1 / 2);
    puts_s("S: SAU и MPC настроены, BLK_MAX ssram-0 ");
    puthex_s(b0);
    puts_s("\n");
    SHCSR |= 1u << 19;                       /* SECUREFAULTENA — отдельный обработчик, а не HardFault */

    const uint32_t *ns_vec = (const uint32_t *)NS_CODE;
    VTOR_NS = NS_CODE;
    __asm volatile("msr msp_ns, %0" ::"r"(ns_vec[0]));
    puts_s("S: переход в небезопасную часть\n");
    typedef void __attribute__((cmse_nonsecure_call)) ns_fn(void);
    ns_fn *ns_reset = (ns_fn *)cmse_nsfptr_create(ns_vec[1]);    /* младший бит 0 — переход в небезопасное состояние */
    ns_reset();
    for (;;) {
    }
}

/* ── старт ── */
extern uint32_t _estack;
void Reset_Handler(void)
{
    main();
}

void Default_Handler(void)
{
    puts_s("S: исключение без обработчика\n");
    for (;;) {
    }
}

__attribute__((section(".isr_vector"), used)) const void *const vectors[16] = {
    &_estack, Reset_Handler, Default_Handler, Default_Handler, Default_Handler, Default_Handler, Default_Handler, SecureFault_Handler,
};

Дальше: раздел 2в «STM8: восьмибитный родственник — что взять, а что нет» — чем восьмибитное ядро ST похоже на STM32 и где оно всё ещё к месту.

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