All Posts

Detecting and Preventing Stack Overflows on Microcontrollers

1. Introduction

stack overflow

The stack is a region of RAM where the microcontroller stores temporary data whenever it branches from one function to another or when an interrupt service routine is triggered. It acts like a registry of the current context—saving registers and local variables by pushing them onto the stack, and restoring them by popping them when returning. This memory region has a predefined size and boundary. When the program uses more stack space than this limit, it crosses into other memory areas, and that condition is called a stack overflow.

2. What is a Stack Overflow?

On ARM Cortex-M microcontrollers, the stack is a descending stack, meaning it starts at a high memory address and grows downward toward lower addresses as data is pushed. The CPU keeps track of the current top of the stack in a special register called the SP (Stack Pointer).

Every time a function is called, or an interrupt occurs, the CPU automatically pushes registers and local variables onto the stack and updates the SP. When the function or interrupt returns, the CPU pops the data off and restores the SP to its previous value.

stack descending

The location and size of the stack are set in the linker script. For example, on an STM32H7 you might see:

/* Entry Point */
ENTRY(Reset_Handler)

/* Highest address of the user mode stack */
_estack = ORIGIN(RAM_D1) + LENGTH(RAM_D1); /* end of "RAM_D1" Ram type memory */
_Min_Heap_Size = 0x200; /* required amount of heap  */
_Min_Stack_Size = 0x400; /* required amount of stack */

When the MCU starts, the SP register is initialized with _estack, so the stack begins at the very top of RAM_D1 and grows downwards.

If the stack keeps growing and the SP moves past its predefined lower boundary, it will overwrite other parts of RAM. This is called a stack overflow. It can corrupt variables, break return addresses, and cause crashes that are often hard to debug.

3. Detect Stack Overflow on MCU

Here are three simple, practical ways to catch stack overflows on a microcontroller:

3.1 Guard Pattern at the Stack Boundary

We add a small section in the linker script at the very end of the stack and reserve it for a guard pattern. At startup, we fill that section with a known value:

_estack = ORIGIN(RAM_D1) + LENGTH(RAM_D1);

/* Reserve 32 bytes for stack overflow guard */
.stack_guard (NOLOAD) : 
{ 
    KEEP(*(.stack_guard)) 
} > RAM_D1 

__attribute__((section(".stack_guard"))) 
volatile uint32_t stack_guard[8]; // 8 words = 32 bytes 

for (uint32_t i = 0; i < 8; i++) { 
    stack_guard[i] = 0xAA33BB33; 
}

Then, using a periodic timer interrupt, we poll this region to see if the pattern has changed. If even a single word is overwritten, it means the stack has grown past its limit—stack overflow detected.

3.2 MPU Guard Region (Hardware Fault on First Byte Past the Limit)

To avoid the CPU time spent polling a canary, you can make stack overflows event‑driven with the MPU. The idea is simple: place a no‑access guard (for privileged and unprivileged) region immediately below the stack's lower boundary (ARM stacks grow downward). If the stack ever crosses its limit, the next push lands in the guard and triggers a memory management fault right away:

uint32_t guard_size = 128;   
uint32_t stack_low  = (uint32_t)&__StackLimit;   // __StackLimit from linker script 
uint32_t guard_base = (stack_low - guard_size) & ~(guard_size - 1u); 
uint32_t p = 0; 
while ((1u << p) < guard_size) p++; 
uint32_t size_field = (p ? (p - 1) : 0); 

/* Enable MemManage faults */ 
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk; 

/* Program MPU region 0: NO ACCESS for privileged and unprivileged */ 
MPU->CTRL = 0;              
MPU->RNR  = 0;              // select region 0 
MPU->RBAR = guard_base & MPU_RBAR_ADDR_Msk; 
MPU->RASR = 
    (0u << MPU_RASR_XN_Pos)        |   
    (0u << MPU_RASR_AP_Pos)        |   
    (0u << MPU_RASR_TEX_Pos)       | 
    (1u << MPU_RASR_C_Pos)         | 
    (1u << MPU_RASR_B_Pos)         | 
    (0u << MPU_RASR_S_Pos)         | 
    (0u << MPU_RASR_SRD_Pos)       | 
    (size_field << MPU_RASR_SIZE_Pos) | 
    MPU_RASR_ENABLE_Msk; 

/* Enable MPU with default mapping for non-protected regions */ 
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk; 

/* Synchronization barriers */ 
__DSB(); 
__ISB(); 

You can refer to this article for MPU configuration on ARM Cortex M:

ARM Cortex-M MPU Explained: Memory Attributes, Access Control, and More

3.3 Monitor the Stack Pointer (SP) and Track Usage

At runtime, sample the SP register and compare it to the configured stack base/limit. Log the minimum SP seen to estimate worst‑case depth, and raise an alert if SP moves within a safety margin (e.g., 128 bytes) of the boundary. This doesn't guarantee catching the exact overflow moment, but it's lightweight and excellent for early warning and sizing the stack correctly.

If you use FreeRTOS, it offers several built-in features to detect and monitor stack overflows.

4. Avoid Stack Overflow

The easiest way to avoid stack overflow is to write code that doesn't put unpredictable pressure on the stack. Avoid recursion unless the maximum depth is small and well-tested, since each call adds another frame. Don't put huge local buffers on the stack because they can quickly consume all available space. Instead, move large data to static/global memory.

At build time, GCC can help estimate usage with -fstack-usage, which creates .su files showing each function's stack cost, and -Wstack-usage=<bytes> to warn if any function exceeds a chosen limit. These tools let you find "heavy" functions early and resize or refactor them before they cause trouble in the field.

5. Conclusion

Stack overflows can crash your program and corrupt data. Avoid them by not using deep recursion or large buffers on the stack, and check stack size with compiler tools.

Adding simple runtime checks helps catch problems early and keeps your system running safely.