Diagnosing and Fixing Stack Overflow in Resource-Constrained Embedded C
Unexpected resets and hard faults in embedded C often trace to stack overflow. Learn how to paint the stack, measure the high-water mark, and fix the root cause without corrupting adjacent memory.
26 Jul 2026, 15:15 UTC

Unexpected resets, hard faults, and silent memory corruption in embedded C often share one hidden cause: the call stack has grown past the RAM region reserved for it. On a microcontroller with tens of kilobytes of RAM, a recursive parser or a single 2 KB local buffer can push the stack into the heap or global variables. The useful takeaway is that you can confirm this without guessing by painting the stack with a known pattern at boot, running your heaviest code path, and then measuring how much of that pattern survives. This guide walks through the recognizable symptoms, ordered checks, and fixes tied to each finding.
Recognizable Condition
The failure usually appears when the application performs deep function-call nesting or allocates large local arrays. Typical signs include:
- Hard fault or unexpected reset that occurs only after a complex operation, not during idle loops.
- Global variables or heap structures that change value without any code writing to them.
- Behavior that improves temporarily when a function is inlined or a buffer size is reduced.
These symptoms point to the stack overwriting adjacent memory because the statically allocated stack region is too small for the worst-case call depth.
Cause and Diagnostic Table
| Condition | Likely Cause | Diagnostic Signal |
|---|---|---|
| Hard fault after recursive descent | Stack exceeds its RAM allocation | Painted pattern partially erased near the stack limit |
| Corrupted globals after large local array | Local buffer spills past stack boundary | Intact magic pattern ends well above the nominal stack base |
| Intermittent reset under peak load | Transient deep call chain or interrupt nesting | High-water mark varies between runs; pattern erasure moves lower |
Ordered Checks
1. Inspect the Linker Map File
Before changing code, confirm how much stack the linker reserved and where it sits relative to other sections. Generate a map file with your toolchain (for GNU tools, add -Map=output.map to the linker flags). Open the map and locate the .stack or _stack symbol. Cross-reference its address range against the physical RAM limits from the microcontroller datasheet. If the stack region touches or overlaps the heap or .bss/.data, the layout itself is the problem.
Expected check: The stack start and end addresses fall entirely inside RAM, with a visible gap to the next section.
2. Paint the Stack at Boot
Fill the entire reserved stack area with a known magic word before main() runs. A common choice is 0xDEADBEEF. The exact symbols depend on your linker script; the example below uses placeholder names.
extern uint32_t _stack_limit, _stack_top;
void paint_stack(void)
{
uint32_t *p = &_stack_limit;
while (p < &_stack_top) {
*p++ = 0xDEADBEEF;
}
}
Call paint_stack() from startup code before C runtime initialization completes. On architectures where the stack grows downward, iterate from the initial stack pointer toward the lower limit.
Risk: This is a destructive test. Do not call it after the application has started using the stack, or you will corrupt live frames.
3. Measure the High-Water Mark
Run the most complex code path your device supports—deepest recursion, largest buffers, highest interrupt load. Then halt the processor and inspect the painted region. In a debugger such as GDB, dump the stack area (example command, adjust the address and length to your map file):
x/256wx 0x20000000
Expected check: Most words should still read 0xDEADBEEF. The first address that no longer matches the pattern approximates the deepest stack usage. Compare that address to the stack limit from the map file.
4. Correlate with Physical RAM
Take the high-water mark from the previous step and the section addresses from the map file. If the deepest used address is within a few hundred bytes of the stack limit—or if it has already crossed into the heap or globals—you have confirmed a stack overflow.
Fixes Tied to Their Findings
Fix A: Enlarge the Stack Region
If the map file shows unused RAM below the stack and the high-water mark is close to the limit, increase the reserved stack size in the linker script. An illustrative GNU ld snippet:
SECTIONS
{
.stack (NOLOAD) :
{
. = . + 0x2000; /* 8 KB stack */
} > RAM
}
Caution: On many bare-metal linkers the stack and heap grow toward each other. Raising the stack size without moving the heap start can create a silent collision that corrupts data before any crash occurs. After changing the size, regenerate the map file and verify the gap between .stack and the heap or .bss remains positive.
Fix B: Replace Recursion with Iteration
If the high-water mark tracks with recursion depth, convert the recursive function to an explicit loop with a manually managed state or work queue. This bounds stack usage to a constant regardless of input size.
/* Before: recursive tree walk */
void walk(node_t *n) {
if (!n) return;
walk(n->left);
process(n);
walk(n->right);
}
/* After: iterative walk with fixed-size static buffer */
static node_t *stack[MAX_DEPTH];
void walk_iterative(node_t *root) {
int top = 0;
/* push/pop logic here; MAX_DEPTH is a compile-time constant */
}
Choose MAX_DEPTH from the measured high-water mark plus a safety margin, not from a guess.
Fix C: Move Large Buffers Off the Stack
If a single local array consumes most of the stack, declare it static or allocate it from a fixed-size pool at startup. Static placement shifts the cost from the stack to global RAM, which the linker accounts for explicitly.
/* Risky on small stacks */
void parse_packet(void) {
uint8_t buf[2048];
/* ... */
}
/* Safer: static or pool-allocated */
static uint8_t packet_buf[2048];
void parse_packet(void) {
/* use packet_buf */
}
If you use a heap allocator, ensure the allocator itself is bounded and that the linker map shows the heap cannot grow into the stack.
Escalation Criteria
Stop debugging locally and escalate to a platform or toolchain expert when any of the following apply:
- The high-water mark sits within roughly 10 percent of the physical RAM limit even after applying Fix A, leaving no room for growth.
- Memory corruption persists after the painted pattern shows ample unused stack, suggesting a DMA overrun, pointer bug, or cache coherency issue instead.
- The linker script or startup code is generated by a vendor IDE and you cannot obtain a map file or modify section placement.
- An MPU (memory protection unit) fault points to a stack access outside the defined region, indicating the stack pointer itself is corrupted by a separate bug.
Limitations and Verification
Stack painting is destructive and only reveals the deepest point reached between manual inspections. A transient interrupt storm or a rare call path may escape detection. To reduce that risk, run the painted test after the most complex sequence your field devices encounter, not just a synthetic benchmark.
To verify a fix, repeat the full cycle: regenerate the map file, repaint the stack, execute the same peak-load path, and dump the region again. The practical check is simple—the first non-0xDEADBEEF word should now sit comfortably above the stack limit, and the map file should show a non-overlapping gap between the stack and every other RAM section. If both conditions hold, the overflow condition is resolved for the tested path.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.