Embedded HandbookApps
All sections › II. Core and system mechanisms
STM32F407, STM32F405 · level advanced · 11 min read
Open in the interactive handbook

11а. MPU: protecting memory from your own mistakes

A microcontroller has no "operating system" to stop a program that writes to the wrong place. A write through a null pointer, a stack overflow into neighbouring variables, a jump into data — and the program keeps running with corrupted memory until something strange happens far away. The MPU (Memory Protection Unit) in Cortex-M3/M4/M7 cores lets you turn such errors into an immediate stop right on the guilty instruction — together with the address that was accessed.

The MPU is not only in M3/M4/M7. On Cortex-M0+ (G0, C0, L0 from L05x) it is built the same way — RBAR, RASR, eight regions — but without the TEX field, and the smallest region is 256 bytes: a 32-byte guard is not possible there. On Cortex-M33 (L5, U5, H5) the MPU is different, ARMv8-M: a region is given by a base and a limit (RBAR and RLAR) in steps of 32 bytes, with no power of two and no subregions, attributes are an index into the MAIR registers, and overlapping regions do not mean the higher number wins but a fault. The rasr() function of this section does not apply there. The M33 catches stack overflow even without the MPU — with the limit registers MSPLIM and PSPLIM (a UsageFault, flag STKOF).

The section is an experiment in QEMU (netduinoplus2, Cortex-M4): five MPU regions and four traps. The rules follow the Cortex-M4 core manual from the pack. Program _verify/mpu11a/mpu11a.c, check _verify/mpu11a_check.py.

Terms used in the documentation

Term Meaning
Memory Protection Unit (MPU) the memory protection block
region, background region a region and the background region
MPU_RBAR, MPU_RASR region base address register and region attribute and size register
access permission (AP) access rights
execute never (XN) execution forbidden
subregion disable (SRD) switching off subregions
MemManage fault, MMFSR, MMFAR memory protection fault, its flags and its address
PRIVDEFENA default memory map enabled for privileged code

11а.1 How the MPU works

The size is encoded in the SIZE field: size = 2^(SIZE+1). In one place the manual gives the minimum as SIZE = 3, while the table next to it gives 4 (32 bytes) as the "minimum permitted"; go by the table: the smallest region is 32 bytes.

Permissions (AP):

AP Privileged code Unprivileged
000 no access no access
001 read and write no access
010 read and write read only
011 read and write read and write
101 read only no access
110 read only read only

The XN bit forbids executing instructions from the region.


11а.2 Experiment: five regions

Memory map with five MPU regions and four traps: addresses and MMFSR flags

No. Where Size Permissions XN RASR
0 Flash memory, 0x08000000 1 MB read only no 0x06020027
1 RAM, 0x20000000 128 KB read-write yes 0x13080021
2 table, 0x20000400 1 KB read only yes 0x16080013
3 "guard", 0x20000800 32 bytes no access yes 0x10080009
4 addresses from zero 256 bytes no access yes 0x1000000F

Regions 2 and 3 lie inside region 1 and override it — their numbers are higher.

The MPU_RASR word by fields: XN in bit 28, AP in bits 26–24, TEX, S, C, B in 21–16, SIZE in 5–1 and ENABLE in bit 0; three values from the experiment decoded — 0x06020027 is Flash without XN with AP 6 read only and SIZE 19 that is 1 MB, 0x13080021 is RAM with XN, AP 3 read-write and 128 KB, 0x10080009 is the no-access guard of 32 bytes

The RASR value is assembled from the fields:

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;
}

Enabling: region by region through MPU_RNR, MPU_RBAR, MPU_RASR; then enable the MemManage fault (SHCSR.MEMFAULTENA, bit 16) and the MPU itself with the background map: MPU_CTRL = PRIVDEFENA | ENABLE; and the dsb, isb barriers.


11а.3 Four traps

The MemManage handler records the flags (MMFSR), the address (MMFAR) and the instruction address from the stack frame (section 7а) — and returns control to the caller so that the next trap can be checked:

What we did MMFSR What it means MMFAR
read *(uint32_t *)0 0x82 DACCVIOL + address valid 0x00000000
wrote to the read-only table 0x82 DACCVIOL + address valid 0x2000040C — ro_table[3]
called code in RAM 0x01 IACCVIOL: instruction fetch not written (PC in frame = 0x20000000)
wrote to the "guard" below the stack 0x82 DACCVIOL + address valid 0x20000800

11а.4 Three uses the MPU is switched on for

Null pointer. On STM32 address 0 is not empty: the boot memory is mapped there, and a read through a null pointer silently returns something (for example, the initial stack value). A "no access" region over the first 256 bytes turns such an error into a fault on the spot.

Stack overflow. The stack grows downwards and, when it overflows, corrupts .bss below it — the error shows up anywhere. A 32-byte "no access" "guard" below the stack catches the overflow on the very first write past the boundary. RTOSes with MPU support reprogram the regions on every task switch — this protects the stack of each task as well.

The guard has two weaknesses, and both are visible in the experiment _verify/fixw2a3/guard11a. The first is a large frame. A function with a 4 KB array (as in section 11.4) moves SP down by 4 KB at once, past the 32 bytes of the guard, and its write lands below it: no fault, the memory below the guard is corrupted. So the guard is made no smaller than the largest stack frame — GCC prints frame sizes with the -fstack-usage option. The second is the handler's own stack. When the task stack (PSP) overflows, the MemManage handler runs on MSP and calmly reports the address; in the experiment MMFSR = 0x92 — MSTKERR joined DACCVIOL, a failure to write the frame on exception entry. But when MSP overflows, the handler gets the same corrupted stack: its first stack write escalated the fault to HardFault (HFSR = 0x40000000, FORCED). Such a case is dealt with in HardFault, and its handler first of all puts MSP on an intact area.

Code in data. XN on RAM forbids executing what came in from outside as data — the first defence against code injection through a buffer overflow.


11а.5 Pitfalls

The compiler threw away the read of address zero. In the experiment, without the -fno-delete-null-pointer-checks option GCC removed the function reading *(uint32_t *)0 entirely: for it, dereferencing zero is undefined behaviour, so it "never happens". On a microcontroller, where there is memory at zero, this option is needed.

The region "does not work". The base is not aligned to the size: a 1 KB region at address 0x20000100 is impossible — the low bits of the base are ignored.

Peripherals stopped responding after the MPU was enabled. No background map: without PRIVDEFENA everything not described by regions is forbidden — including USART.

HardFault instead of MemManage. The fault itself is not enabled (MEMFAULTENA), and it escalates to HardFault.

A "foreign" address in MMFAR. An instruction fetch violation does not write an address — check MMARVALID; the address is the PC from the frame.

MPU settings did not take effect immediately. dsb and isb were missing after the write.

A region smaller than 32 bytes. Does not exist: the smallest size is 32 bytes (SIZE = 4).


11а.6 Summary

Full sources

mpu11a.c — experiment firmware in QEMU: five MPU regions and MemManage traps.

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

Next: section 11б "Cache, TCM and memory domains on F7 and H7" — what changes in memory and in working with DMA when the core gets caches.

Open in the interactive handbook