
Stack overflow remains one of the most insidious failure modes in embedded firmware — silent, non-deterministic, and often surfacing only under specific interrupt nesting or worst-case call-depth conditions. Unlike heap exhaustion, which typically yields a clean allocation failure, stack overflow corrupts adjacent memory (often the heap, globals, or peripheral registers) and manifests as inexplicable crashes hours or days later. This article covers static stack analysis, compiler-assisted instrumentation, runtime watermarking, and Cortex-M hardware guards to bound and monitor stack usage in production firmware.
On Cortex-M, each thread/task and the main stack (MSP) have dedicated stack pointers. The PSP (Process Stack Pointer) serves the active task; the MSP (Main Stack Pointer) serves the kernel, exception handlers, and the reset/startup code. Understanding the frame layout is essential for sizing stacks correctly.
Function Call Frame (normal C function):
+--------------------------+ <-- Caller's SP (high address)| Pushed LR | Saved by prologue (PUSH {r4-r7, lr})+--------------------------+| Callee-saved Registers | r4-r11 (up to 8 x 4 = 32 bytes)| (only those used) |+--------------------------+| Local Variables / | Variable size, aligned to 8 bytes| Spilled Temporaries | (AAPCS requires 8-byte SP alignment)+--------------------------+| Outgoing Args | Space for args beyond r0-r3+--------------------------+ <-- SP during function body
Exception/ISR Entry Frame (hardware-pushed):
+--------------------------+ <-- SP before exception (high address)| xPSR | SP + 28| PC (return address) | SP + 24| LR | SP + 20| R12 | SP + 16| R3 | SP + 12| R2 | SP + 8| R1 | SP + 4| R0 | SP + 0+--------------------------+ <-- SP after stacking (32 bytes)| (+ S0-S15, FPSCR if | Extended frame: +72 bytes on| FPU context is active) | Cortex-M4F/M7 with FPU enabled+--------------------------+
Key dimensions for sizing:
printf/snprintf family: 1–2 KB stack (heavy internal buffers)Modern toolchains emit per-function stack usage metadata. Enable it and feed the output to a call-graph analyzer.
-fstack-usagearm-none-eabi-gcc -fstack-usage -c module.c -o module.o# Produces module.su with lines like:# module.c:42:12:foo 48 static# module.c:55:5:bar 112 dynamic
The second column is the function’s frame size in bytes. static means fixed size; dynamic means VLAs or alloca (avoid both in embedded).
# Generate call graph info (.ci file in VCG format)arm-none-eabi-gcc -fcallgraph-info -c module.c# Or use: cflow, cgx, or commercial tools (StackAnalyzer, Bound-T)
# Pseudocode: compute max stack depth from .su files + call graph# Nodes = functions, weight = frame size# Edges = caller -> callee# Max depth = max weight path from entry points (main, ISR handlers, task entry)# Account for:# - Interrupt nesting: sum of max ISR stack along nesting chain# - Recursion: flag as unbounded (error) or bound by config# - RTOS task switch: MSP/PSP swap adds no stack, but each task needs its own budget
Practical workflow (combining .su files with call graph):
# Several open-source tools can parse .su files and compute worst-case depth:# - avstack.pl: Perl script combining objdump + .su data (Daniel Beer)# - puncover: Python tool with web UI for stack/code analysis# - Commercially: StackAnalyzer (AbsInt), Bound-T (Tidorum)
| Flag | Effect | Cost | Detection |
|---|---|---|---|
-fstack-protector-strong | Inserts canary on functions with local arrays/addr-taken locals; canary is checked before function return | ~2–4 instructions per protected function | Runtime |
-fstack-usage | Emits .su files with per-function frame sizes | None (no code emitted) | Compile-time |
-Wframe-larger-than=N | Warns if any function’s frame exceeds N bytes | None (warning only) | Compile-time |
Recommended baseline for embedded:
CFLAGS += -fstack-usage -fstack-protector-strong -Wframe-larger-than=512
Fill the stack with a known pattern at startup; scan periodically to find the high-water mark.
#include <stdint.h>#include <string.h>#define STACK_PATTERN 0xA5A5A5A5U#define WATERMARK_MARGIN_BYTES 128 // safety margin below observed max// Linker symbols (define in .ld script)extern uint32_t _estack; // Initial MSP (top of stack region)extern uint32_t _stack_bottom; // End of stack region (lowest addr)/* IMPORTANT: Call before enabling interrupts (e.g., from Reset_Handler or* early main) to avoid racing with ISRs that would push onto the MSP stack* below the captured 'end' boundary. */static void stack_fill_pattern(void){volatile uint32_t *p = (volatile uint32_t *)&_stack_bottom;/* Stop well below the current stack pointer to avoid corrupting active frames */volatile uint32_t *end = (volatile uint32_t *)((uint32_t *)__get_MSP() - 8);while (p < end) {*p++ = STACK_PATTERN;}}uint32_t stack_get_watermark_bytes(void){volatile uint32_t *p = (volatile uint32_t *)&_stack_bottom;volatile uint32_t *end = (volatile uint32_t *)__get_MSP(); // current MSPif (end > (volatile uint32_t *)&_estack) end = (volatile uint32_t *)&_estack;while (p < end && *p == STACK_PATTERN) {p++;}return (uint32_t)((uintptr_t)&_estack - (uintptr_t)p);}// Call from idle task or 1 Hz monitor taskvoid stack_monitor_task(void *arg){(void)arg;uint32_t peak = 0;for (;;) {uint32_t used = stack_get_watermark_bytes();if (used > peak) peak = used;// Log or assert if within margin of stack limituint32_t stack_size = (uint32_t)&_estack - (uint32_t)&_stack_bottom;if (stack_size - used < WATERMARK_MARGIN_BYTES) {// Trigger fault, log, or reboot__BKPT(0);}vTaskDelay(pdMS_TO_TICKS(1000));}}
MEMORY{FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512KRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K}_estack = ORIGIN(RAM) + LENGTH(RAM); /* Initial MSP (top of RAM) */_stack_size = 16K; /* Main stack (MSP) size */_stack_bottom = ALIGN(_estack - _stack_size, 256); /* Bottom of stack region *//* ALIGN to 256 ensures MPU guard region base address meets power-of-two alignment *//* Task stacks allocated from heap or static pools */
The Memory Protection Unit (MPU) can place a guarded region at the stack boundary. Any access to the guard region triggers a MemManage fault — catching overflow before it corrupts adjacent memory.
Note: This register model (RBAR/RASR with VALID bit) applies to ARMv7-M only. Cortex-M33/M23 (ARMv8-M) use a different MPU programming model (RBAR/RLAR) and require separate configuration code.
#include "core_cm4.h" // or core_cm7.h (ARMv7-M only)#define MPU_RBAR_VALID (1U << 4) /* Valid bit in RBAR */#define MPU_RASR_ENABLE (1U << 0) /* Enable bit in RASR */#define MPU_RASR_SIZE_32B (4U << 1) /* 2^(4+1) = 32 bytes min */#define MPU_RASR_SIZE_64B (5U << 1)#define MPU_RASR_SIZE_128B (6U << 1)#define MPU_RASR_SIZE_256B (7U << 1)#define MPU_RASR_SIZE_512B (8U << 1)#define MPU_RASR_SIZE_1K (9U << 1)#define MPU_RASR_AP_NO_ACCESS (0U << 24) /* No access at any privilege level */#define MPU_RASR_XN (1U << 28)static void mpu_setup_stack_guard(uint32_t stack_base, uint32_t stack_size){/* Guard region at bottom of stack (lowest addresses) */uint32_t guard_base = stack_base; /* _stack_bottom *//* Base address must be aligned to region size (256 bytes for ARMv7-M MPU) */assert((guard_base & 0xFF) == 0);/* Guard: 256-byte red zone — No Access triggers MemManage on any read or write */MPU->RBAR = guard_base | MPU_RBAR_VALID | 0; /* Region 0, valid (also sets RNR) */MPU->RASR = MPU_RASR_SIZE_256B | MPU_RASR_AP_NO_ACCESS | MPU_RASR_XN | MPU_RASR_ENABLE;MPU->CTRL = MPU_CTRL_PRIVDEFENA_Msk | MPU_CTRL_ENABLE_Msk;__DSB();__ISB();}
MemManage Handler:
void MemManage_Handler(void){uint32_t mmfsr = SCB->CFSR & 0xFF; /* MemManage Fault Status */uint32_t mmfar = SCB->MMFAR; /* Fault address *//* Log mmfar, mmfsr, LR, PSP, MSP *//* Check MMARVALID (bit 7) before using MMFAR */if ((mmfsr & (1U << 7)) &&mmfar >= (uint32_t)&_stack_bottom && mmfar < (uint32_t)&_stack_bottom + 256) {/* Stack overflow detected */log_fault("STACK_OVERFLOW", mmfar, __get_PSP(), __get_MSP());}NVIC_SystemReset(); // Or enter safe state}
Guard size trade-off:
Each FreeRTOS task gets its own stack (allocated from heap or static). Oversizing wastes RAM; undersizing crashes.
// Static allocation (preferred for determinism)static StackType_t task1_stack[1024] __attribute__((aligned(8)));static StaticTask_t task1_tcb;TaskHandle_t task1 = xTaskCreateStatic(task1_fn, "Task1", 1024, NULL, tskIDLE_PRIORITY + 1,task1_stack, &task1_tcb);// Enable stack overflow checking (configCHECK_FOR_STACK_OVERFLOW = 1 or 2)// Mode 1: checks stack pointer against stack limit on context switch// Mode 2: fills stack with 0xA5 at create; on each context switch, checks the// last 20 bytes (5 words) at the stack bottom for the sentinel pattern// (slower but catches overflows that occurred between context switches)
// configCHECK_FOR_STACK_OVERFLOW = 2 hookvoid vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName){(void)xTask;log_error("Stack overflow in task: %s", pcTaskName);NVIC_SystemReset();}
gcc-stack-usage on task entry function + all reachable callees.printf/library calls: +1.5 KB if used+---------------------------+ HIGH ADDRESS (0x20020000)| || MSP / PSP | <-- Stack Pointer (grows DOWN)| Current Position || |+---------------------------+| || Active Stack Frames | Call chain: main -> foo -> bar -> ISR| (grows toward zero) || |+---------------------------+| || WATERMARK (high water) | <-- Furthest SP reached so far| (0xA5A5A5A5 pattern) || |+---------------------------+| || UNUSED / PATTERN FILL | 0xA5A5A5A5 sentinel values| |+---------------------------+|===========================| <-- MPU GUARD REGION (256 bytes)| RED ZONE (no access) | MemManage fault on any access|===========================| LOW ADDRESS (0x2001C000)| || Other RAM (heap, .bss) | (grows UP from 0x20000000)| |+---------------------------+
| Task / Context | Static Analysis (bytes) | ISR Nesting Margin | Library Margin | MPU Guard | Raw Sum | Total Allocated |
|---|---|---|---|---|---|---|
| Main (MSP) | 1,792 | 512 | 1,536 | 256 | 4,096 | 4 KB |
| Control Task | 2,612 | 0 | 1,024 | 256 | 3,892 | 4 KB ¹ |
| Comm Task | 3,712 | 0 | 2,048 | 256 | 6,016 | 6 KB ¹ |
| Idle Task | 128 | 0 | 0 | 256 | 384 | 512 B ¹ |
| ISR Stack (MSP) | 512 (peak) | N/A | N/A | 256 | — | Shared with Main |
¹ Rounded up to the nearest 256-byte boundary per the sizing methodology above.
Total RAM for stacks on this MCU: 14.5 KB of 128 KB RAM
Stack overflow is a silent killer in embedded systems. The defense-in-depth strategy combines:
-fstack-usage + call graph) to establish a baseline budget per task/ISR.-Wframe-larger-than to reject oversized frames at compile time; -fstack-protector-strong to detect stack buffer overruns at runtime via canary checks.configCHECK_FOR_STACK_OVERFLOW=2) as a final safety net on context switch.Apply this stack budget to every task and interrupt context during architecture review — not after the first field crash. The RAM cost (a few KB) is negligible compared to the debugging cost of a latent stack overflow.
-fstack-usage). https://gcc.gnu.org/onlinedocs/gcc/Developer-Options.htmlQuick Links
Legal Stuff





