Stack and heap: where function calls pile up
The stack, which grows with every function call, and the heap, borrowed at run time. Why embedded code is careful about stack overflow and heap use.
Stack
When a function is called, the return address, the registers to preserve and the local variables are pushed onto the stack, and they are removed when the function ends. The Cortex-M stack grows from high addresses to low.
- A local array
uint8_t buf[512];uses 512 bytes of stack. - When an interrupt occurs, eight registers (32 bytes, more with the FPU in use) are pushed automatically.
- Deeper call chains and nested interrupts make the stack deeper.
Stack overflow
When the stack goes past its region, it overwrites the global variables or heap below it. The symptoms appear far from the cause: "a variable sometimes has a strange value", "a hard fault when a certain feature is on". Fill the stack region with a pattern such as 0xA5 and later see how much was overwritten to measure the maximum usage.
Heap
The region borrowed at run time with malloc. In embedded work, because of fragmentation, failure handling and unpredictable execution time, it is used only at initialisation or not at all, with static arrays or memory pools used instead.
| Region | Size set by | Risk |
|---|---|---|
| Stack | Linker script | Overflow |
| Heap | Linker script | Fragmentation, allocation failure |
| Static (.data/.bss) | At compile time | None (check the map file) |
In a picture: how RAM is divided
What one function call pushes
void b(void) { uint8_t buf[64]; /* ... */ } // 64 bytes + registers to save
void a(void) { uint32_t x; b(); }
int main(void) { a(); }
Each step of main → a → b pushes a return address, registers to preserve and local variables, and they disappear on the way back. If an interrupt arrives on top of that, the handler's share is pushed on top of the stack at that moment. So the stack size needed is "the deepest call path + every interrupt that can nest".
How to measure stack usage
The simplest way is to fill the stack region with a fixed value at start-up and later count how far that value is still intact.
extern uint32_t _sstack, _estack; // symbols from the linker script
void stack_paint(void) {
for (uint32_t *p = &_sstack; p < (uint32_t *)__get_MSP() - 8; p++) *p = 0xDEADBEEF;
}
uint32_t stack_free_bytes(void) {
uint32_t *p = &_sstack;
while (*p == 0xDEADBEEF) p++;
return (uint32_t)((uint8_t *)p - (uint8_t *)&_sstack);
}
After a long run, increase the stack if less than 20% of the total is left. GCC's -fstack-usage option writes the stack size used by each function to a file.
Why the heap is avoided
- Fragmentation: repeated allocation and freeing chops the free space into small pieces, until the total is enough but no large block can be found. Fatal in a device left on for months.
- Time is not constant: how long
malloctakes varies from call to call. - Failure handling: there is little you can sensibly do when NULL comes back.
So embedded code mostly uses global or static buffers and fixed-size memory pools, and takes heap memory only once at initialisation or not at all.
Symptoms of stack overflow
A global variable changes for no reason, a hard fault occurs on entering a particular function, or the system dies only when interrupts are frequent. Large local arrays and deep recursion are the main culprits.