
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.
+--------------------------+ <-- Stack Base (high address)| Previous Frame || (caller's locals, etc) |+--------------------------+| Callee-saved Registers | r4-r11 (8 x 4 = 32 bytes)| (if function calls C) |+--------------------------+| Local Variables / | Variable size, aligned to 8 bytes| Spilled Temporaries | (ABI requires 8-byte stack alignment)+--------------------------+| Outgoing Args / | Space for args beyond r0-r3| Call Preservation |+--------------------------+ <-- Stack Pointer (SP) at function entry| Pushed LR / Return Addr | (on BL/BLX, LR holds return addr)+--------------------------+| Exception Frame | On ISR entry: r0-r3, r12, LR, PC, xPSR| (hardware pushed) | (32 bytes, 8-word aligned)+--------------------------+ <-- Stack Limit (low address)
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 (requires Graphviz)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 with gcc-stack-usage (open-source Python tool):
pip install gcc-stack-usagegcc-stack-usage build/*.su --entry main --entry ISR_Handler1 --entry Task1_Entry# Output: max stack per entry point, call chain achieving it
| Flag | Effect | Cost |
|---|---|---|
-fstack-protector-strong | Inserts canary on functions with local arrays/addr-taken locals | ~2-4 instructions per protected function |
-fstack-clash-protection | Probes stack on large frame allocation (>1 page) | Probe loop on large allocations only |
-fstack-usage | Emits .su files (no runtime cost) | Compile-time only |
-Wframe-larger-than=N | Warns if frame exceeds N bytes | Compile-time check |
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)static void stack_fill_pattern(void){uint32_t *p = &_stack_bottom;uint32_t *end = &_estack;while (p < end) {*p++ = STACK_PATTERN;}}uint32_t stack_get_watermark_bytes(void){uint32_t *p = &_stack_bottom;uint32_t *end = (uint32_t *)__get_MSP(); // current MSPif (end > (uint32_t *)&_estack) end = (uint32_t *)&_estack;while (p < end && *p == STACK_PATTERN) {p++;}return (uint32_t)((uintptr_t)end - (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}_stack_bottom = ORIGIN(RAM); /* Bottom of RAM */_stack_size = 16K; /* Main stack (MSP) size */_estack = _stack_bottom + _stack_size; /* Initial MSP *//* Task stacks allocated from heap or static pools */
The Memory Protection Unit (MPU) can place a guarded region at the stack boundary. Any access (read/write) to the guard region triggers a MemManage fault — catching overflow before it corrupts adjacent memory.
#include "core_cm4.h" // or cm7/cm33#define MPU_RASR_VALID (1U << 0)#define MPU_RASR_ENABLE (1U << 1)#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_PRIV_RO (5U << 24) /* Priv RO, User no access */#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 */uint32_t guard_size = 256; /* 256-byte red zone */MPU->RNR = 0; /* Region 0 */MPU->RBAR = guard_base | MPU_RASR_VALID | 0; /* Region 0, valid */MPU->RASR = MPU_RASR_SIZE_256B | MPU_RASR_AP_PRIV_RO | 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 mmfar = SCB->MMFAR; /* Fault address */uint32_t mmfsr = SCB->CFSR & 0xFF; /* MemManage Fault Status *//* Log mmfar, mmfsr, LR, PSP, MSP *//* Check if mmfar is within stack guard region */if (mmfar >= _stack_bottom && mmfar < _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, checks pattern on switch (slower but catches deeper corruption)
// 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 (0x20000000)| || Other RAM (heap, .bss) || |+---------------------------+
| Task / Context | Static Analysis (bytes) | ISR Nesting Margin | Library Margin | MPU Guard | Total Allocated |
|---|---|---|---|---|---|
| Main (MSP) | 1,840 | 512 | 1,536 | 256 | 4 KB |
| Control Task | 2,100 | 512 | 512 | 256 | 4 KB |
| Comm Task | 3,200 | 512 | 2,048 | 256 | 6 KB |
| Idle Task | 128 | 0 | 0 | 256 | 512 B |
| ISR Stack (MSP) | 512 (peak) | N/A | N/A | 256 | Shared with Main |
Total RAM for 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.-fstack-protector-strong, -Wframe-larger-than) to catch oversized frames and buffer overruns at compile time.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/wiki/StackUsageAnalysisQuick Links
Legal Stuff





