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
- Восемь областей (0…7) — MPU_TYPE в опыте прочитался как 0x00000800: поле числа областей = 8.
- У области — база, размер и права. Размер — степень двойки от 32 байт до 4 ГБ, база выровнена по размеру: область в 1 КБ начинается с адреса, кратного 1 КБ.
- Области могут перекрываться — побеждает область с большим номером.
- Нарушение — отказ MemManage на той самой команде.
- Фоновая область (PRIVDEFENA): где областей нет, привилегированный код видит обычную карту памяти. Удобно — не надо описывать периферию.
Размер кодируется в поле SIZE: размер = 2^(SIZE+1). Руководство в одном месте называет минимум SIZE = 3, а в таблице рядом — 4 (32 байта) «минимальный допустимый»; считать надо по таблице: наименьшая область — 32 байта.
Права (AP):
| AP | Привилегированный код | Непривилегированный |
|---|---|---|
| 000 | нет доступа | нет доступа |
| 001 | чтение и запись | нет доступа |
| 010 | чтение и запись | только чтение |
| 011 | чтение и запись | чтение и запись |
| 101 | только чтение | нет доступа |
| 110 | только чтение | только чтение |
Бит XN запрещает выполнять команды из области.
11а.2 Опыт: пять областей
| № | Где | Размер | Права | XN | RASR |
|---|---|---|---|---|---|
| 0 | флеш-память, 0x08000000 | 1 МБ | только чтение | нет | 0x06020027 |
| 1 | ОЗУ, 0x20000000 | 128 КБ | чтение-запись | да | 0x13080021 |
| 2 | таблица, 0x20000400 | 1 КБ | только чтение | да | 0x16080013 |
| 3 | «сторож», 0x20000800 | 32 байта | нет доступа | да | 0x10080009 |
| 4 | адреса с нуля | 256 байт | нет доступа | да | 0x1000000F |
Область 2 и 3 лежат внутри области 1 и перекрывают её — у них номер больше.
Значение 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 |
- При нарушении данных в MMFAR — точный адрес, а PC в кадре указывает на
сбойную команду. Дальше
addr2lineпо PC — и найдена строка. - При нарушении выборки команды адреса в MMFAR нет (бит MMARVALID = 0 — там осталось прошлое значение 0x2000040C), зато PC в кадре — это и есть запретный адрес.
- Запись в таблицу не прошла: после ловушки
ro_table[3]= 0.
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 Итоги
- MPU Cortex-M4: 8 областей, размер 2^(SIZE+1) от 32 байт, база выровнена по размеру, больший номер побеждает.
- Права AP и запрет исполнения XN; фоновая карта — PRIVDEFENA.
- Опыт: чтение нуля и запись в защищённую таблицу и «сторож» — DACCVIOL (MMFSR 0x82) с точным адресом в MMFAR; вызов кода из ОЗУ — IACCVIOL (0x01) с адресом в PC кадра.
- Главные применения: ловушка нулевого указателя, сторож стека, запрет исполнения данных.
- GCC без
-fno-delete-null-pointer-checksвыбрасывает чтение по нулю.
Полные исходники
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, когда у ядра появляются кэши.