Ground Embedded KO Subscribe

Notes / Computer Architecture

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.

GROUND EMBEDDED · NOTEComputer Architecture 07Stack and heapMCU

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

0x20000000.data .bssglobal variablesaboveheap ↑mallocfree spacemargincollision when this reaches 00x20002000stack ↓locals, return addresses
A typical Cortex-M RAM layout. The stack grows downwards from a high address and the heap upwards from a low one. If they meet, memory is corrupted.

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".

RoadmapSee where this note sits in Computer Architecture→

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 malloc takes 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.

FREE PDFGet the 3 free ebooksIf you want to keep reading this topic as one book: BLDC motor control, serial communication and sensors at book length. PDFs are in Korean.Get them free →