HomeAbout UsContact Us

Stack Usage Analysis and Optimization in Embedded C

By Jithin Tom
Published in Embedded C/C++
July 12, 2026
3 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
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.

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:

  • Minimum ISR frame: 32 bytes (hardware stacking, no FPU) + callee-saved if ISR calls C = 64 bytes
  • With FPU active: 104 bytes (32 + 72 for S0–S15, FPSCR, alignment padding)
  • Typical leaf function: 16–48 bytes (locals + alignment padding)
  • Non-leaf function: +4 bytes per callee-saved register pushed (32-bit words)
  • 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 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)

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

Compiler Hardening Flags

FlagEffectCostDetection
-fstack-protector-strongInserts canary on functions with local arrays/addr-taken locals; canary is checked before function return~2–4 instructions per protected functionRuntime
-fstack-usageEmits .su files with per-function frame sizesNone (no code emitted)Compile-time
-Wframe-larger-than=NWarns if any function’s frame exceeds N bytesNone (warning only)Compile-time

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)
/* 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 MSP
if (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 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
}
_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 */

Cortex-M MPU Stack Guard Regions

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.

MPU Configuration for Stack Guard (ARMv7-M: Cortex-M3/M4/M7)

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:

  • 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; 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 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 on MSP (ISRs use MSP, not task PSP on Cortex-M)
    • printf/library calls: +1.5 KB if used
    • RTOS context save: +64 bytes on task PSP (32 bytes hardware exception frame + 32 bytes software-saved R4–R11 during PendSV context switch)
  3. Round up to nearest 256 bytes (to satisfy MPU region alignment).
  4. Validate at runtime with watermark monitor for first 1000 hours.

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 (0x2001C000)
| |
| Other RAM (heap, .bss) | (grows UP from 0x20000000)
| |
+---------------------------+

Putting It Together: Stack Budget Spreadsheet

Task / ContextStatic Analysis (bytes)ISR Nesting MarginLibrary MarginMPU GuardRaw SumTotal Allocated
Main (MSP)1,7925121,5362564,0964 KB
Control Task2,61201,0242563,8924 KB ¹
Comm Task3,71202,0482566,0166 KB ¹
Idle Task12800256384512 B ¹
ISR Stack (MSP)512 (peak)N/AN/A256Shared 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

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-Wframe-larger-than to reject oversized frames at compile time; -fstack-protector-strong to detect stack buffer overruns at runtime via canary checks.
  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 Online Documentation, Developer Options (-fstack-usage). https://gcc.gnu.org/onlinedocs/gcc/Developer-Options.html
  4. AbsInt, StackAnalyzer: Static Stack Usage Analysis. https://www.absint.com/stackanalyzer/
  5. MISRA C:2012, Directive 4.1: “Run-time failures shall be minimized” (encompasses stack overflow prevention); Rule 17.2 prohibits recursion to enable static stack bounding. https://www.misra.org.uk/
  6. K. Chatterjee et al., “Stack Size Analysis for Interrupt-Driven Programs,” in Static Analysis: 10th International Symposium (SAS 2003). https://doi.org/10.1007/3-540-44898-5_7

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). On FPU-enabled targets (Cortex-M4F/M7), add 72 bytes for the extended exception frame (S0–S15 + FPSCR + alignment). 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-analysisgccclangarm

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

Link-Time Optimization (LTO) for Embedded Firmware Size Reduction
Link-Time Optimization (LTO) for Embedded Firmware Size Reduction
August 08, 2026
2 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media