Memory protection units (MPU) — preventing stack overflow, access faults
What is a Memory Protection Unit?
Key differences from an MMU (Memory Management Unit):
- No virtual memory: MPU works with physical addresses only — no page tables, no address translation.
- Simpler, lower latency: perfect for hard real-time systems where deterministic fault handling is critical.
- Typical use: protecting RTOS task stacks, isolating peripheral registers, catching buffer overflows at runtime.
How the MPU Works: Region Configuration
Step 1: Define Regions
The MPU divides memory into overlapping or non-overlapping regions (typically 8–16 regions on ARM Cortex-M). Each region is configured with:
- Base Address (must align to region size: = bytes, )
- Size (32 bytes to 4 GB in powers of two)
- Access Permissions:
APbits: None, RO (read-only), RW (read/write), Execute-Never (XN)- Privilege: accessible only in privileged mode (kernel) or also in user mode (tasks)
- Subregion disable (8 subregions per region; can disable1/8ths to carve holes)
Why alignment matters: Hardware decodes the address by masking low bits. A 1KB region () must start at address XX...0_0000_0000 (low 10 bits zero). Misaligned base → unpredictable behavior.
Step 2: Enable the MPU
After configuring regions, set the MPU CTRL register:
ENABLEbit: turn on protection.PRIVDEFENA: if no region matches, allow privileged access to background (optional).- On fault, CPU vectors to
MemManage_Handler()(ARM) where you log the fault address (MMFARregister) and decide: kill the task, log & continue, or reset.
Preventing Stack Overflow with the MPU
The Problem
Each RTOS task has a private stack in RAM. A deep recursion, large local array, or missing volatile can cause the stack pointer to grow past the allocated boundary into another task's memory or into heap data → silent corruption, crash hours later.
The Solution: Guard Regions
Configure an MPU region as a no-access guard band at the bottom of each stack. Crucial: every region base must be aligned to its own size. A 1 KB region must start on a 1 KB boundary (low 10 bits zero), a 32-byte guard must start on a 32-byte boundary (low 5 bits zero).
// Task A stack layout (ALL bases aligned to region size):
// 0x2000_0000 – 0x2000_0400: Actual stack (1 KB, RW) -> base aligned to 1 KB
// 0x2000_0000 – 0x2000_0020: Guard region (32 bytes, no access) overlaps stack bottom
// The guard is a HIGHER-numbered region so it wins over the stack region.
void setup_task_stackprotection(void) {
// Region 0: Stack (1 KB), base aligned to 0x400
MPU->RBAR = 0x20000000 | REGION_VALID | 0; // low 10 bits = 0 -> aligned to 1 KB
MPU->RASR = (9 << 1) // Size: 2^10 = 1024 bytes
| (3 << 24) // AP = 011 -> RW in privileged+user
| (1 << 28) // XN = 1
| REGION_ENABLE;
// Region 1: Guard (bottom 32 bytes), base aligned to 0x20
// Higher region number -> its "no access" wins over Region 0's RW.
MPU->RBAR = 0x20000000 | REGION_VALID | 1; // low 5 bits = 0 -> aligned to 32 B
MPU->RASR = (4 << 1) // Size: 2^5 = 32 bytes
| (0 << 24) // AP = 000 -> no access
| (1 << 28) // XN = 1 (execute never)
| REGION_ENABLE;
MPU->CTRL = MPU_CTRL_ENABLE | MPU_CTRL_PRIVDEFENA;
}Why this works: The stack region (Region 0) covers the whole 1 KB, but the guard region (Region 1) overlaps its bottom 32 bytes with no access. Because the highest-numbered matching region wins, the bottom 32 bytes are protected. If taskA overflows downward into 0x2000_0000–0x2000_0020, the very first write triggers a MemManage fault. You catch it immediately, log the task name and fault address, and handle it (kill task, reboot, alert).
Why 32 bytes? Minimum ARM MPU region size is 32 bytes (). Larger guards waste RAM but catch overflows earlier.
Preventing Access Faults: Isolating Peripherals & Code
Use Case 1: Protect Peripheral Registers
Make peripheral memory (e.g., 0x4000_0000–0x5FFF_FFFF on ARM) privileged-only. User tasks cannot accidentally write to UART, SPI, or system clock registers.
// Region 2: Peripherals (512 MB), privileged RW only
MPU->RBAR = 0x40000000 | REGION_VALID | 2; // base aligned to 512 MB (0x20000000)
MPU->RASR = (28 << 1) // 2^29 = 512 MB
| (1 << 24) // AP = 001 → RW privileged, no user access
| (1 << 28) // XN
| REGION_ENABLE;A user-mode task calling *UART_TXREG = data; → instant MemManage fault.
Use Case 2: Execute-Never (XN) on Data Regions
Mark stack and heap as XN (execute-never). If a buffer overflow overwrites a return address with shellcode on the stack, the CPU faults when it tries to execute stack bytes.
// Heap: RW but XN
MPU->RASR = ... | (1 << 28); // XN bit setWhy this matters: Classic code-injection attacks fail. Even if an attacker controls stack data, they cannot redirect execution there.
Common Mistakes & Fixes
Diagram: MPU Region Layout

Each task's stack gets a guard region at the bottom (no access). Heap and peripherals are isolated. Code is read-only + executable.
Worked Example: Configuring MPU for 3 Tasks
System: ARM Cortex-M4, 128 KB SRAM at 0x2000_0000, 8 MPU regions.
Memory map (note every base is aligned to its region size):
- Region 0 (background): 0x2000_0000–0x2002_0000 (128 KB, ), RW privileged (low priority). Base aligned to 128 KB.
- Region 1: Task A stack, 0x2000_0000–0x2000_0400 (1 KB, ), RW user. Base aligned to 1 KB.
- Region 2: Task A stack guard, 0x2000_0000–0x2000_0020 (32 B, ), no access, higher number than Region 1 so it wins.
- Region 3: Task B stack, 0x2000_0800–0x2000_0C00 (1 KB), RW user. Base 0x2000_0800 aligned to 1 KB.
- Region 4: Task B stack guard, 0x2000_0800–0x2000_0820 (32 B), no access, higher number than Region 3.
- Region 5: Shared heap, 0x2000_1000–0x2001_1000 (64 KB, ), RW user, XN. Base 0x2000_1000 aligned to 64 KB.
- Region 6: Peripherals, 0x4000_0000–0x6000_0000 (512 MB, ), RW privileged only. Base aligned to 512 MB.
- Region 7: Flash code, 0x0000_0000–0x0008_0000 (512 KB, ), RO user+privileged, executable. Base aligned.
Code:
void configure_mpu(void) {
MPU->CTRL = 0; // Disable during config
// Region 0: Background (entire SRAM), privileged RW (base aligned to 128 KB)
MPU->RBAR = 0x20000000 | VALID | 0;
MPU->RASR = (16 << 1) | (1 << 24) | ENABLE; // 2^17 = 128 KB, AP=001
// Region 1: Task A stack (1 KB), base aligned to 0x400
MPU->RBAR = 0x20000000 | VALID | 1;
MPU->RASR = (9 << 1) | (3 << 24) | (1 << 28) | ENABLE; // 1 KB, RW user, XN
// Region 2: Task A guard (32 B) — higher number wins over Region 1
MPU->RBAR = 0x20000000 | VALID | 2;
MPU->RASR = (4 << 1) | (0 << 24) | (1 << 28) | ENABLE; // 32 B, no access
// Region 3: Task B stack (1 KB), base aligned to 0x400
MPU->RBAR = 0x20000800 | VALID | 3;
MPU->RASR = (9 << 1) | (3 << 24) | (1 << 28) | ENABLE;
// Region 4: Task B guard (32 B) — higher number wins over Region 3
MPU->RBAR = 0x20000800 | VALID | 4;
MPU->RASR = (4 << 1) | (0 << 24) | (1 << 28) | ENABLE;
// Region 5: Heap (64 KB), base aligned to 0x10000
MPU->RBAR = 0x20001000 | VALID | 5;
MPU->RASR = (15 << 1) | (3 << 24) | (1 << 28) | ENABLE; // 2^16 = 64 KB
// Region 6: Peripherals (512 MB), base aligned to 0x20000000
MPU->RBAR = 0x40000000 | VALID | 6;
MPU->RASR = (28 << 1) | (1 << 24) | (1 << 28) | ENABLE; // 2^29 = 512 MB
// Region 7: Flash (512 KB), RO user+privileged, executable
MPU->RBAR = 0x00000000 | VALID | 7;
MPU->RASR = (18 << 1) | (5 << 24) | (0 << 28) | ENABLE; // 2^19 = 512 KB, AP=101 (RO both), exec OK
MPU->CTRL = MPU_CTRL_ENABLE | MPU_CTRL_PRIVDEFENA;
__DSB(); // Data sync barrier
__ISB(); // Instruction sync barrier (flush pipeline)
}Note on AP encoding:
AP = 0b101(5 << 24) means read-only for both privileged and unprivileged.AP = 0b110(6 << 24) is reserved on Cortex-M — do not use it.AP = 0b001= RW privileged only;AP = 0b011= RW both;AP = 0b000= no access.
Why the barriers? ARM is weakly ordered. __DSB() ensures all MPU register writes complete before enabling. __ISB() flushes the instruction pipeline so the next instruction fetch respects new MPU rules.
Testing: In Task A, declare char overflow[2048]; and write to it. The program faults at the guard region, MemManage_Handler logs MMFAR = 0x2000_0010, you see Task A's name → immediate root cause.
Active Recall Flashcards
#flashcards/coding
What is the key difference between an MPU and an MMU? :: MPU works with physical addresses only (no virtual memory, no page tables). MMU translates virtual to physical addresses. MPU is simpler, deterministic, ideal for real-time embedded systems.
How does an MPU prevent stack overflow in an RTOS?
Why must MPU region base addresses be aligned to region size?
What happens if two MPU regions overlap on ARM Cortex-M?
What AP value gives read-only access for both privileged and unprivileged code, and which nearby value must you avoid?
AP = 0b101 (5 << 24) = RO for both. Avoid AP = 0b110 (6 << 24), which is reserved on Cortex-M.What is the XN (execute-never) bit used for?
Why call __DSB() and __ISB() after enabling the MPU?
__DSB() (data sync barrier) ensures MPU config writes complete. __ISB() (instruction sync barrier) flushes the pipeline so the next instruction respects new MPU rules. Without them, a weakly-ordered ARM core might fetch instructions before the MPU is active.Mnemonic
Feynman Explanation (Explain to a 12-Year-Old)
Recall Explain MPU like I'm twelve
Imagine your computer's memory is a big neighborhood with houses (memory addresses). Each program (task) gets a house to live in. Without rules, a bugy program could walk into your house and mess up your stuff (write to your memory).
The MPU is like a security guard at every street corner. You tell the guard: "Program A can only go in house #3, read and write. Program B can only go in house #5, but only read the living room." If Program A tries to sneak into house #5, the guard blows a whistle (fault interrupt) and stops it before it breaks anything.
Stack overflow is when a program's pile of papers (the call stack) grows so tall it falls over into the neighbor's yard. The guard puts a tripwire (guard region with no-access) at the edge of the yard. The instant a paper crosses the line, the guard catches it. No mess, no silent corruption, just a clear alarm: "Program A overflowed!"
Why is this better than no guard? Without the MPU, Program A could trash Program B's house, and you wouldn't know until Program B tries to use its broken furniture hours later. With the MPU, you catch the troublemaker red-handed, immediately.
Connections
- Stack memory layout — how the stack grows and why overflow happens
- ARM Cortex-M privilege levels — user vs privileged mode, how MPU enforces it
- RTOS task switching — each task needs isolated memory; MPU reconfigures on context switch
- Memory faults and exception handling — what happens when the MPU triggers a fault
- Memory-mapped IO — peripherals live at fixed addresses; MPU protects them from user code
- Buffer overflow attacks — MPU (XN bit) mitigates code injection
- Real-time system determinism — MPU faults are deterministic (no paging, no TLB miss), critical for hard real-time
Summary
The Memory Protection Unit is hardware-enforced memory access control for embedded systems without virtual memory. You divide the address space into regions with base, size, and permissions (RW, RO, no-access, XN, privileged/user). The MPU checks every access; violations trigger a fault before the bad write completes.
Stack overflow protection: Place a no-access guard region at the stack bottom (as a higher-numbered overlapping region). Overflow writes hit the guard → instant fault with the fault address in MMFAR → you know which task and exactly where.
Access fault prevention: Mark peripherals as privileged-only, mark data as XN (execute-never), isolate task memory. Bugs and attacks are caught at the instruction that violates the rule, not after silent corruption.
Critical details: Align base addresses to region size (hardware requirement). Higher region number wins on overlap. Use the correct AP encoding (0b101 for RO both; avoid reserved 0b110). Use __DSB and __ISB after config. Test by deliberately overflowing in a debug build — you should see the fault handler, not a random crash.
The MPU turns "my system crashed somewhere, sometime" into "Task A overflowed at 0x2000_0010, here's the call stack." It's the difference between debugging for days and fixing in minutes.
Concept Map
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Dekho, yaha basic idea bahut simple hai — embedded system mein multiple tasks ek hi memory share karte hain, aur agar ek task ka "buggy" code galti se kisi doosre task ki memory mein likh de, toh poora system crash ho sakta hai. Yahi problem solve karta hai Memory Protection Unit (MPU). Isko aise samjho jaise har task ko apna ek "fenced yard" mil gaya hai — MPU ek hardware block hai jo har memory access se pehle check karta hai ki task apni boundary ke andar hi kaam kar raha hai ya nahi. Agar koi task apni limit cross karke doosre ki jagah likhne ki koshish kare (jaise stack overflow ya wild pointer), toh MPU turant ek fault raise kar deta hai — data corrupt hone se pehle hi. Yeh MMU se alag hai kyunki isme virtual memory ya page tables nahi hote, sirf physical addresses par kaam karta hai, isliye yeh fast aur deterministic hota hai — real-time systems ke liye perfect.
MPU ka kaam samajhne ka core concept hai regions. Aap memory ko chote-chote regions mein divide karte ho, aur har region ko permissions dete ho — jaise read-only, read-write, ya execute-never, aur privileged (kernel) ya unprivileged (user task). Ek important baat yaad rakhna: har region ka base address uske size ke hisaab se aligned hona chahiye — matlab 1KB ka region 1KB boundary par hi start hoga (low bits zero). Yeh isliye zaroori hai kyunki hardware address ko bits mask karke decode karta hai; agar alignment galat ho toh behavior unpredictable ho jaata hai. Jab access hota hai, toh MPU check karta hai ki address kis region mein aata hai, aur agar multiple match ho jaayein toh highest-numbered region jeet jaata hai (ARM convention).
Ab practically iska sabse bada fayda hai stack overflow ko pakadna. Socho har task ka apna stack RAM mein hai, aur agar deep recursion ya bada local array ki wajah se stack apni limit se aage badh jaaye, toh bina MPU ke woh silently doosre task ki memory kha jaata hai — aur sabse buri baat, bug Task A mein hota hai par crash Task B mein dikhta hai, jo debugging ko nightmare bana deta hai. MPU se aap har stack ke neeche ek guard region laga dete ho jise "no-access" bana diya jaata hai — jaise hi stack overflow hoke us guard band ko touch kare, MPU turant fault de deta hai aur aapko exact jagah pata chal jaati hai. Yeh baat kyun matter karti hai? Kyunki medical devices, car ECUs jaise safety-critical systems mein ek chhoti si memory corruption bhi jaan-lewa ho sakti hai — MPU aapko yeh reliability aur predictable fault handling deta hai jo aap software checks se kabhi utne efficiently nahi kar sakte.