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

11а. MPU: защита памяти от своих же ошибок

У микроконтроллера нет «операционной системы», которая остановит программу, записавшую не туда. Запись по нулевому указателю, переполнение стека в соседние переменные, прыжок в данные — и программа продолжает работать с испорченной памятью, пока где-то далеко не случится странное. Блок MPU (Memory Protection Unit) в ядрах Cortex-M3/M4/M7 позволяет превратить такие ошибки в немедленную остановку прямо на виноватой команде — с адресом, по которому пытались обратиться.

MPU есть не только у M3/M4/M7. У Cortex-M0+ (G0, C0, L0 начиная с L05x) он устроен так же — RBAR, RASR, восемь областей, — но без поля TEX, а наименьшая область — 256 байт: сторож в 32 байта там не выйдет. У Cortex-M33 (L5, U5, H5) MPU другой, ARMv8-M: область задают базой и пределом (RBAR и RLAR) с шагом 32 байта, без степени двойки и подобластей, атрибуты — номером в регистрах MAIR, а перекрытие областей даёт не победу большего номера, а отказ. Функция rasr() этого раздела там неприменима. Переполнение стека M33 ловит и без MPU — регистрами предела MSPLIM и PSPLIM (отказ UsageFault, флаг STKOF).

Раздел — опыт в QEMU (netduinoplus2, Cortex-M4): пять областей MPU и четыре ловушки. Правила — по руководству на ядро Cortex-M4 из пакета. Программа _verify/mpu11a/mpu11a.c, проверка _verify/mpu11a_check.py.

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

Оригинал Перевод
Memory Protection Unit (MPU) блок защиты памяти
region, background region область и фоновая область
MPU_RBAR, MPU_RASR регистр базового адреса и регистр атрибутов и размера области
access permission (AP) права доступа
execute never (XN) запрет исполнения
subregion disable (SRD) отключение подобластей
MemManage fault, MMFSR, MMFAR отказ защиты памяти, его флаги и адрес
PRIVDEFENA разрешение карты по умолчанию для привилегированного кода

11а.1 Как устроен MPU

Размер кодируется в поле SIZE: размер = 2^(SIZE+1). Руководство в одном месте называет минимум SIZE = 3, а в таблице рядом — 4 (32 байта) «минимальный допустимый»; считать надо по таблице: наименьшая область — 32 байта.

Права (AP):

AP Привилегированный код Непривилегированный
000 нет доступа нет доступа
001 чтение и запись нет доступа
010 чтение и запись только чтение
011 чтение и запись чтение и запись
101 только чтение нет доступа
110 только чтение только чтение

Бит XN запрещает выполнять команды из области.


11а.2 Опыт: пять областей

Карта памяти с пятью областями MPU и четыре ловушки: адреса и флаги MMFSR

№ Где Размер Права XN RASR
0 флеш-память, 0x08000000 1 МБ только чтение нет 0x06020027
1 ОЗУ, 0x20000000 128 КБ чтение-запись да 0x13080021
2 таблица, 0x20000400 1 КБ только чтение да 0x16080013
3 «сторож», 0x20000800 32 байта нет доступа да 0x10080009
4 адреса с нуля 256 байт нет доступа да 0x1000000F

Область 2 и 3 лежат внутри области 1 и перекрывают её — у них номер больше.

Слово MPU_RASR по полям: XN в бите 28, AP в битах 26–24, TEX, S, C, B в 21–16, SIZE в 5–1 и ENABLE в бите 0; три значения из опыта разобраны — 0x06020027 это флеш без XN с AP 6 только чтение и SIZE 19 то есть 1 МБ, 0x13080021 это ОЗУ с XN, AP 3 чтение-запись и 128 КБ, 0x10080009 это сторож без доступа на 32 байта

Значение RASR собирается из полей:

static uint32_t rasr(int xn, int ap, int tex, int s, int c, int b, int size_log2)
{
    return (uint32_t)xn << 28 | (uint32_t)ap << 24 | (uint32_t)tex << 19 | (uint32_t)s << 18 | (uint32_t)c << 17 |
           (uint32_t)b << 16 | (uint32_t)(size_log2 - 1) << 1 | 1u;
}

Включение: область за областью через MPU_RNR, MPU_RBAR, MPU_RASR; затем разрешить отказ MemManage (SHCSR.MEMFAULTENA, бит 16) и сам MPU с фоновой картой: MPU_CTRL = PRIVDEFENA | ENABLE; и барьеры dsb, isb.


11а.3 Четыре ловушки

Обработчик MemManage запоминает флаги (MMFSR), адрес (MMFAR) и адрес команды из кадра стека (раздел 7а) — и возвращает управление вызывающему, чтобы проверить следующую ловушку:

Что сделали MMFSR Что значит MMFAR
прочитали *(uint32_t *)0 0x82 DACCVIOL + адрес верен 0x00000000
записали в таблицу только для чтения 0x82 DACCVIOL + адрес верен 0x2000040C — ro_table[3]
вызвали код из ОЗУ 0x01 IACCVIOL: выборка команды не записан (PC в кадре = 0x20000000)
записали в «сторож» под стеком 0x82 DACCVIOL + адрес верен 0x20000800

11а.4 Три применения, ради которых MPU включают

Нулевой указатель. На STM32 адрес 0 — не пустота: там отображение загрузочной памяти, и чтение по нулевому указателю молча вернёт что-то (например, начальное значение стека). Область «нет доступа» на первые 256 байт превращает такую ошибку в отказ на месте.

Переполнение стека. Стек растёт вниз и, переполнившись, портит .bss под собой — ошибка проявится где угодно. «Сторож» в 32 байта «нет доступа» под стеком ловит переполнение на первой же записи за границу. ОСРВ с поддержкой MPU перенастраивают области при каждом переключении задачи — так защищается и стек каждой задачи.

У сторожа две слабости, и обе видны в опыте _verify/fixw2a3/guard11a. Первая — большой кадр. Функция с массивом в 4 КБ (как в разделе 11.4) опускает SP сразу на 4 КБ, мимо 32 байт сторожа, и её запись ложится ниже него: отказа нет, память под сторожем испорчена. Поэтому сторож берут не меньше самого большого кадра стека — размеры кадров GCC печатает ключом -fstack-usage. Вторая — стек самого обработчика. Когда переполняется стек задачи (PSP), обработчик MemManage работает на MSP и спокойно сообщает адрес; в опыте MMFSR = 0x92 — к DACCVIOL добавился MSTKERR, сбой записи кадра при входе в исключение. Когда же переполняется MSP, обработчик получает тот же испорченный стек: его первая запись в стек подняла отказ до HardFault (HFSR = 0x40000000, FORCED). Такой случай разбирают в HardFault, и его обработчик первым делом ставит MSP на целое место.

Код в данных. XN на ОЗУ запрещает исполнять то, что пришло извне как данные, — первая защита от внедрения кода через переполнение буфера.


11а.5 Грабли

Компилятор выбросил чтение нулевого адреса. В опыте без ключа -fno-delete-null-pointer-checks GCC удалил функцию, читающую *(uint32_t *)0, целиком: для него разыменование нуля — неопределённое поведение, значит «не бывает». На микроконтроллере, где по нулю есть память, этот ключ нужен.

Область «не работает». База не выровнена по размеру: область в 1 КБ с адреса 0x20000100 не получится — младшие биты базы не учитываются.

Периферия перестала отвечать после включения MPU. Нет фоновой карты: без PRIVDEFENA всё, что не описано областями, запрещено — включая USART.

Вместо MemManage — HardFault. Не разрешён сам отказ (MEMFAULTENA), и он поднимается до HardFault.

В MMFAR «чужой» адрес. Нарушение выборки команды адрес не пишет — смотреть MMARVALID; адрес — PC из кадра.

Настройки MPU не подействовали сразу. Не хватило dsb и isb после записи.

Область меньше 32 байт. Не бывает: наименьший размер — 32 байта (SIZE = 4).


11а.6 Итоги

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

mpu11a.c — прошивка опыта в QEMU: пять областей MPU и ловушки MemManage.

/* Раздел 11а: MPU — защита памяти. Прогон в QEMU netduinoplus2 (STM32F405, Cortex-M4).
 * Области: 0 — флеш-память только для чтения; 1 — ОЗУ без исполнения; 2 — таблица только для чтения;
 * 3 — «сторож» под стеком; 4 — первые 256 байт адресов (ловушка нулевого указателя).
 * Обработчик MemManage запоминает MMFSR, MMFAR и адрес команды и возвращает управление вызывающему
 * (PC в кадре := LR в кадре), чтобы проверить следующую ловушку. */
#include <stdint.h>

#define RCC_APB2ENR  (*(volatile uint32_t *)0x40023844)
#define USART1_SR    (*(volatile uint32_t *)0x40011000)
#define USART1_DR    (*(volatile uint32_t *)0x40011004)
#define USART1_BRR   (*(volatile uint32_t *)0x40011008)
#define USART1_CR1   (*(volatile uint32_t *)0x4001100C)
#define SCB_SHCSR    (*(volatile uint32_t *)0xE000ED24)
#define SCB_MMFSR    (*(volatile uint8_t  *)0xE000ED28)
#define SCB_MMFAR    (*(volatile uint32_t *)0xE000ED34)
#define MPU_TYPE     (*(volatile uint32_t *)0xE000ED90)
#define MPU_CTRL     (*(volatile uint32_t *)0xE000ED94)
#define MPU_RNR      (*(volatile uint32_t *)0xE000ED98)
#define MPU_RBAR     (*(volatile uint32_t *)0xE000ED9C)
#define MPU_RASR     (*(volatile uint32_t *)0xE000EDA0)

static void putc1(char c)
{
    while (!(USART1_SR & (1u << 7))) {
    }
    USART1_DR = (uint32_t)c;
}

static void puts1(const char *s)
{
    while (*s)
        putc1(*s++);
}

static void puthex(uint32_t v)
{
    static const char hex[] = "0123456789abcdef";
    puts1("0x");
    for (int i = 28; i >= 0; i -= 4)
        putc1(hex[(v >> i) & 0xF]);
}

/* ── обработчик MemManage ── */
volatile uint32_t f_mmfsr, f_mmfar, f_pc, f_count;

void MemManage_C(uint32_t *frame)
{
    f_mmfsr = SCB_MMFSR;
    f_mmfar = SCB_MMFAR;
    f_pc = frame[6];
    f_count++;
    SCB_MMFSR = 0xFF;              /* флаги сбрасываются записью единиц */
    frame[6] = frame[5] & ~1u;     /* вернуться туда, откуда вызвали сбойную функцию */
}

__attribute__((naked)) void MemManage_Handler(void)
{
    __asm volatile("tst lr, #4 \n ite eq \n mrseq r0, msp \n mrsne r0, psp \n b MemManage_C");
}

/* ── области ── */
/* RASR: XN[28] AP[26:24] TEX[21:19] S[18] C[17] B[16] SRD[15:8] SIZE[5:1] ENABLE[0]; размер = 2^(SIZE+1) */
static uint32_t rasr(int xn, int ap, int tex, int s, int c, int b, int size_log2)
{
    return (uint32_t)xn << 28 | (uint32_t)ap << 24 | (uint32_t)tex << 19 | (uint32_t)s << 18 | (uint32_t)c << 17 |
           (uint32_t)b << 16 | (uint32_t)(size_log2 - 1) << 1 | 1u;
}

static void region(int n, uint32_t base, uint32_t attr)
{
    MPU_RNR = (uint32_t)n;
    MPU_RBAR = base;
    MPU_RASR = attr;
    puts1("region ");
    putc1((char)('0' + n));
    puts1(" base=");
    puthex(base);
    puts1(" rasr=");
    puthex(attr);
    puts1("\r\n");
}

__attribute__((aligned(1024), section(".bss"))) static volatile uint32_t ro_table[256];
__attribute__((aligned(32))) static volatile uint32_t guard[8];      /* «сторож»: сюда стек упрётся первым */
__attribute__((aligned(4))) static uint16_t ram_code[2] = {0x4770, 0x4770};   /* bx lr */

/* сбойные действия — отдельные функции без собственного стека: в кадре LR = адрес возврата */
__attribute__((noinline)) uint32_t t_read_null(void) { return *(volatile uint32_t *)0; }
__attribute__((noinline)) void t_write_ro(void) { ro_table[3] = 0x12345678u; }
__attribute__((noinline)) void t_write_guard(void) { guard[0] = 0xDEADu; }

static void report(const char *name)
{
    puts1(name);
    puts1(" faults=");
    puthex(f_count);
    puts1(" mmfsr=");
    puthex(f_mmfsr);
    puts1(" mmfar=");
    puthex(f_mmfar);
    puts1(" pc=");
    puthex(f_pc);
    puts1("\r\n");
    f_mmfsr = f_mmfar = f_pc = 0;
}

int main(void)
{
    RCC_APB2ENR |= 1u << 4;
    USART1_BRR = 0x008B;
    USART1_CR1 = (1u << 13) | (1u << 3);

    puts1("mpu_type=");
    puthex(MPU_TYPE);
    puts1("\r\n");

    region(0, 0x08000000u, rasr(0, 6, 0, 0, 1, 0, 20));                        /* флеш 1 МБ: только чтение */
    region(1, 0x20000000u, rasr(1, 3, 1, 0, 0, 0, 17));                        /* ОЗУ 128 КБ: чтение-запись, без исполнения */
    region(2, (uint32_t)ro_table, rasr(1, 6, 1, 0, 0, 0, 10));                  /* таблица 1 КБ: только чтение */
    region(3, (uint32_t)guard, rasr(1, 0, 1, 0, 0, 0, 5));                      /* сторож 32 байта: нет доступа */
    region(4, 0x00000000u, rasr(1, 0, 0, 0, 0, 0, 8));                          /* 256 байт с нуля: нет доступа */
    SCB_SHCSR |= 1u << 16;                                                      /* MEMFAULTENA */
    MPU_CTRL = (1u << 2) | 1u;                                                  /* PRIVDEFENA + ENABLE */
    __asm volatile("dsb \n isb");

    (void)t_read_null();
    report("read_null");
    t_write_ro();
    report("write_ro");
    ((void (*)(void))((uint32_t)ram_code | 1u))();
    report("exec_ram");
    t_write_guard();
    report("write_guard");
    puts1("ro_table[3]=");
    puthex(ro_table[3]);                         /* читать таблицу можно: запись не прошла */
    puts1("\r\nro_table=");
    puthex((uint32_t)ro_table);
    puts1(" guard=");
    puthex((uint32_t)guard);
    puts1(" ram_code=");
    puthex((uint32_t)ram_code);
    puts1("\r\ndone\r\n");
    for (;;) {
    }
}

Дальше: раздел 11б «Кэш, TCM и домены памяти у F7 и H7» — что меняется в памяти и в работе с DMA, когда у ядра появляются кэши.

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