HomeAbout UsContact Us

Stack Usage Analysis and Optimization in Embedded C

By Jithin Tom
Published in Embedded C/C++
July 12, 2026
2 min read
Stack Usage Analysis and Optimization in Embedded C

Table Of Contents

01
The Stack Layout on ARM Cortex-M
02
Static Stack Analysis with Compiler Tooling
03
Compiler Hardening Flags
04
Runtime Stack Watermarking
05
Cortex-M MPU Stack Guard Regions
06
Per-Task Stack Sizing in FreeRTOS
07
ASCII Art: Stack Growth and Guard Region
08
Putting It Together: Stack Budget Spreadsheet
09
Summary
10
Related Reading
11
References
12
Frequently Asked Questions

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.

The Stack Layout on ARM Cortex-M

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:

  • Minimum ISR frame: 32 bytes (hardware) + 32 bytes (callee-saved if calling C) = 64 bytes
  • Typical leaf function: 16–48 bytes (locals + alignment padding)
  • Non-leaf function: +8 bytes per callee-saved register used
  • printf/snprintf family: 1–2 KB stack (heavy internal buffers)

Static Stack Analysis with Compiler Tooling

Modern toolchains emit per-function stack usage metadata. Enable it and feed the output to a call-graph analyzer.

GCC / Clang: -fstack-usage

arm-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).

Building the Call Graph

# Generate call graph (requires Graphviz)
arm-none-eabi-gcc -fcallgraph-info -c module.c
# Or use: cflow, cgx, or commercial tools (StackAnalyzer, Bound-T)

Worst-Case Stack Depth Calculation

# 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-usage
gcc-stack-usage build/*.su --entry main --entry ISR_Handler1 --entry Task1_Entry
# Output: max stack per entry point, call chain achieving it

Compiler Hardening Flags

FlagEffectCost
-fstack-protector-strongInserts canary on functions with local arrays/addr-taken locals~2-4 instructions per protected function
-fstack-clash-protectionProbes stack on large frame allocation (>1 page)Probe loop on large allocations only
-fstack-usageEmits .su files (no runtime cost)Compile-time only
-Wframe-larger-than=NWarns if frame exceeds N bytesCompile-time check

Recommended baseline for embedded:

CFLAGS += -fstack-usage -fstack-protector-strong -Wframe-larger-than=512

Runtime Stack Watermarking

Fill the stack with a known pattern at startup; scan periodically to find the high-water mark.

Cortex-M Watermark Implementation

#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 MSP
if (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 task
void 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 limit
uint32_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));
}
}

Linker Script Symbols

MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (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 */

Cortex-M MPU Stack Guard Regions

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.

MPU Configuration for Stack Guard

#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:

  • 32 bytes: Minimal, catches most overflows but tight
  • 256 bytes: Comfortable margin, 0.25 KB RAM cost
  • 1 KB: Paranoid, catches large frame overruns

Per-Task Stack Sizing in FreeRTOS

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 hook
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
(void)xTask;
log_error("Stack overflow in task: %s", pcTaskName);
NVIC_SystemReset();
}

Sizing Methodology

  1. Static analysis: Run gcc-stack-usage on task entry function + all reachable callees.
  2. Add margins:
    • ISR nesting: +512 bytes (or max ISR depth × frame)
    • printf/library calls: +1.5 KB if used
    • RTOS context save: +64 bytes (already in task stack)
  3. Round up to nearest 256 bytes.
  4. Validate at runtime with watermark monitor for first 1000 hours.

ASCII Art: Stack Growth and Guard Region

+---------------------------+ 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) |
| |
+---------------------------+

Putting It Together: Stack Budget Spreadsheet

Task / ContextStatic Analysis (bytes)ISR Nesting MarginLibrary MarginMPU GuardTotal Allocated
Main (MSP)1,8405121,5362564 KB
Control Task2,1005125122564 KB
Comm Task3,2005122,0482566 KB
Idle Task12800256512 B
ISR Stack (MSP)512 (peak)N/AN/A256Shared with Main

Total RAM for this MCU: 14.5 KB of 128 KB RAM

Summary

Stack overflow is a silent killer in embedded systems. The defense-in-depth strategy combines:

  1. Static analysis (-fstack-usage + call graph) to establish a baseline budget per task/ISR.
  2. Compiler hardening (-fstack-protector-strong, -Wframe-larger-than) to catch oversized frames and buffer overruns at compile time.
  3. Runtime watermarking (pattern fill + periodic scan) to observe actual peak usage in the field.
  4. MPU guard regions (256-byte red zone at stack bottom) to trap overflow immediately via MemManage fault.
  5. RTOS stack checking (FreeRTOS 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.

References

  1. ARM Limited, Cortex-M4/M7/M33 Devices Generic User Guide, “Memory Protection Unit (MPU)” chapter. https://developer.arm.com/documentation/dui0553/latest
  2. FreeRTOS Kernel Documentation, Stack Overflow Checking. https://www.freertos.org/Stacks-and-stack-overflow-checking.html
  3. GCC Wiki, Stack Usage Analysis (-fstack-usage). https://gcc.gnu.org/wiki/StackUsageAnalysis
  4. AbsInt, StackAnalyzer: Static Stack Usage Analysis. https://www.absint.com/stackanalyzer/
  5. MISRA C:2012, Directive 4.6: “Stack usage shall be bounded and measured.” https://www.misra.org.uk/
  6. B. Schwarz et al., “Stack Size Analysis for Interrupt-Driven Programs,” EMSOFT 2008. https://doi.org/10.1145/1450058.1450067

Frequently Asked Questions

What is the difference between stack overflow and stack overrun?

Stack overflow occurs when the stack pointer exceeds the allocated stack memory region, typically due to deep call chains or large local arrays. Stack overrun (or buffer overrun on stack) is writing past the bounds of a local array, corrupting adjacent stack frames — a security vulnerability even if the stack pointer remains valid.

How does the compiler estimate maximum stack usage per function?

Compilers like GCC (-fstack-usage) and Clang (-fstack-usage) emit a .su file per translation unit listing each function's frame size. Static analysis tools (e.g., StackAnalyzer, Bound-T) then build a call graph and compute worst-case stack depth by summing frame sizes along the deepest call path, accounting for recursion and interrupt nesting.

Can stack canaries prevent all stack overflow exploits?

Stack canaries (gcc -fstack-protector-strong) detect stack buffer overruns that overwrite the canary before the return address. They do not prevent stack overflow from deep recursion or large VLAs that exhaust the stack region without corrupting the canary. Combine with MPU stack guard regions and static analysis for defense-in-depth.

What is the typical stack watermark pattern for runtime monitoring?

Fill the stack with a known pattern (e.g., 0xA5A5A5A5) at startup. A periodic monitor task or idle hook scans from the stack bottom upward until it finds a non-pattern word; the distance to the stack top is the high-water mark. On Cortex-M, the PSP/MSP value at the scan time gives current usage.

How much stack should I reserve for an ISR on Cortex-M?

Reserve at least 256–512 bytes for a minimal ISR (register save area: 32 bytes for r0–r3, r12, LR, PC, xPSR; plus callee-saved registers if the ISR calls C functions). For ISRs calling RTOS APIs or printf, budget 1–2 KB. Use the MPU to place a guard region below the ISR stack.

Tags

embedded-cstackmemoryoptimizationcortex-mstatic-analysiswatcomgccclang

Share


Previous Article
Static Analysis Tools for Embedded C: Cppcheck, MISRA, and Clang Static Analyzer
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Atomic Operations in Embedded C: Lock-Free Synchronization for Cortex-M
Atomic Operations in Embedded C: Lock-Free Synchronization for Cortex-M
July 03, 2026
2 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media