Embedded

Stack vs heap in embedded C

By CourseTron Team ·  9 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: In embedded C the stack holds short-lived, automatically-managed data and grows toward the heap; the heap serves dynamic allocation but in a memory-constrained MCU it invites fragmentation, non-deterministic timing, and silent collisions with the stack, which is exactly why most safety-critical embedded code bans malloc and lives entirely on a bounded stack and static storage.

    Every embedded engineer eventually meets a bug where a device runs fine for hours, then reboots or corrupts a variable with no obvious cause. Nine times out of ten the root cause is memory: a stack that quietly outgrew its reserved region, or a heap that fragmented until an allocation failed at the worst moment. Understanding where your variables actually live, byte by byte, is not academic — it is the difference between firmware that ships and firmware that gets recalled.

    The memory map of a running embedded program

    When you compile C for a microcontroller, the linker partitions SRAM into well-defined regions. From low address to high you typically find .data (initialized globals copied from flash at startup), .bss (zero-initialized globals), then the heap growing upward, and finally the stack growing downward from the top of RAM. The gap in the middle is shared: the heap climbs up and the stack descends into the same free space.

    SRAM layout (low to high address) .data (initialized globals) .bss (zeroed globals) HEAP grows toward high address free gap (danger zone) STACK grows toward low address heap up stack down low address high address CourseTron · coursetron.com
    SRAM on a typical Cortex-M part: the heap grows up, the stack grows down, and they share one free gap. A collision corrupts whichever one reached the middle first.

    On an STM32F103 with 20 KB of SRAM, a careless 4 KB recursive buffer or a leaking allocator can erase that free gap in milliseconds. There is no MMU to catch it — the write simply lands in live data.

    What the stack actually does

    The stack is a LIFO region managed automatically by the compiler and the CPU's stack pointer (SP). Every function call pushes a stack frame: the return address, saved registers, function arguments that didn't fit in registers, and all local (automatic) variables. When the function returns, SP rewinds and that memory is instantly reclaimed — no bookkeeping, no fragmentation, constant-time.

    That determinism is why embedded code favors the stack. Consider:

    • uint8_t buf[64]; inside a function costs exactly 64 bytes while that function is active and zero afterward.
    • Allocation and deallocation are a single SP adjustment — effectively free, with cycle-exact timing you can reason about.
    • Interrupt service routines push their own frames onto the same stack (or a dedicated one via the ARM MSP/PSP split), so ISR depth must be budgeted into your worst-case stack usage.
    Anatomy of one stack frame higher address Return address Saved registers Arguments that did not fit in registers Local variables SP uint8_t buf[64] = 64 bytes while active On return, SP rewinds and the whole frame is reclaimed at once. CourseTron · coursetron.com
    One stack frame holds the return address, saved registers, spilled arguments and locals; returning rewinds SP and reclaims all of it.

    Why the heap is risky on an MCU

    Dynamic allocation (malloc/free, new/delete) is the norm on a Linux server with gigabytes of virtual memory and a sophisticated allocator. On a bare-metal MCU the picture inverts, for several concrete reasons:

    • Fragmentation. Repeatedly allocating and freeing blocks of varying sizes leaves the heap pockmarked with free holes. You can have 8 KB free in total yet fail to allocate a 2 KB contiguous block. There is no compacting garbage collector to fix this.
    • Non-deterministic timing. A call to malloc may walk a free list of unknown length. In a hard-real-time control loop running at 10 kHz, a variable-latency allocation is unacceptable — you cannot guarantee the 100 µs deadline.
    • Silent failure. When malloc returns NULL and the code forgets to check (it usually does), the next dereference writes to address 0 — often valid RAM or a peripheral register on an MCU, so instead of a clean crash you get mysterious corruption.
    • Heap/stack collision. Because both live in the same free gap, an over-grown heap and a deep call chain can overwrite each other with no hardware trap on parts without an MPU.

    This is precisely why standards such as MISRA C (Rule 21.3) prohibit the use of malloc, calloc, realloc and free in critical code, and why automotive, avionics (DO-178C), and medical firmware are overwhelmingly built on static and stack allocation with fixed-size memory pools where dynamic behavior is unavoidable.

    PropertyStackHeap
    Managed byCompiler / SP registerAllocator library (malloc)
    Allocation costOne SP adjustment, O(1)Variable, free-list walk
    FragmentationImpossible (LIFO)Common and permanent
    LifetimeUntil function returnsUntil explicit free
    Typical failureOverflow → corruptionLeak, fragmentation, NULL deref
    Deterministic?YesNo

    Stack overflow: the classic silent killer

    A stack overflow happens when the cumulative depth of nested calls plus their locals plus ISR frames pushes SP below the stack's reserved floor. On a desktop you'd get a segmentation fault; on most MCUs you get nothing — SP simply wanders into .bss or the heap and starts overwriting live variables. The watch pattern below shows the stack pointer marching into the danger zone.

    Stack pointer descending on deep recursion heap / .bss (corrupted) stack limit top of stack SP Each nested call lowers SP; the bar turns red once it crosses the limit. CourseTron · coursetron.com
    Each recursive frame pushes SP lower. Once it crosses the red limit the stack silently corrupts the heap or globals below it. The bar turns red at the moment of overflow.

    Defending against this is a core embedded skill:

    • Stack painting. Fill the stack region with a known pattern (e.g. 0xDEADBEEF) at boot, then periodically scan how far the pattern has been overwritten to measure high-water mark. FreeRTOS does this with uxTaskGetStackHighWaterMark().
    • MPU guard regions. On Cortex-M3/M4/M7 parts, configure the Memory Protection Unit so a write past the stack boundary raises a MemManage fault instead of corrupting data.
    • Static analysis. Tools in the Synopsys Coverity and worst-case-stack-depth analyzers (and Siemens/Mentor and Cadence toolchains for larger SoC firmware) compute the maximum call-tree depth so you can size the stack with margin.
    • Avoid recursion and large automatic buffers. Prefer iteration; move big buffers to static storage where their size is visible to the linker map.

    A practical rule of thumb: size your stack for the worst-case call path including the deepest ISR nesting, then add at least 25–50% margin. Verify with the high-water mark on real hardware — not just in simulation.

    Practical guidance for writing memory-safe embedded C

    The disciplined approach most professional firmware teams converge on:

    • Allocate everything you can statically at compile time so the linker map tells you your exact RAM budget before you flash a single byte.
    • When you genuinely need dynamic behavior, use fixed-size block pools (a pre-allocated array of equal blocks with a free list). This gives O(1) allocation, zero fragmentation, and deterministic timing — the safe middle ground.
    • Keep automatic variables small; large or long-lived data belongs in static storage, not deep in a call frame.
    • Remember that qualifiers interact with memory placement — for instance, how you declare hardware-touched variables matters, which is why the volatile keyword in embedded C deserves its own study. Pair that with a solid grasp of pointers and memory in embedded C and most memory bugs become visible before they bite.

    If you are building production firmware and want an external design review or a hands-on training pathway, the design-services team at Avecas VLSI and embedded design services works with exactly these constraints on real silicon.

    Want to master embedded memory, RTOS stack sizing, and real debugging on hardware? Explore the full Embedded Systems track on our CourseTron courses page and build firmware that never mysteriously reboots.

    Frequently Asked Questions

    Can I ever use malloc in embedded systems?

    Yes, but carefully. It is acceptable in non-critical code that allocates once at startup and never frees, or on resource-rich MCUs/Linux-class embedded devices. For hard-real-time or safety-critical loops, avoid it; use static allocation or fixed-size memory pools instead. MISRA C explicitly forbids standard dynamic allocation in critical code.

    How do I detect a stack overflow on a microcontroller without an OS?

    Paint the stack with a sentinel pattern at boot and periodically scan how much has been overwritten to find the high-water mark. On Cortex-M parts, configure the MPU to place a guard region below the stack so an overflow triggers a MemManage fault you can trap and log instead of silently corrupting memory.

    Why does the stack grow downward and the heap upward?

    It is a long-standing convention that maximizes the shared free space between them: placing them at opposite ends of the free RAM lets each grow toward the other until they meet. Neither needs to know the other's size in advance, giving flexible use of a single contiguous gap.

    What is heap fragmentation and why can't the MCU fix it automatically?

    Fragmentation is when freed blocks of varying sizes leave non-contiguous holes, so a large request fails even though enough total memory is free. A desktop can use a compacting garbage collector to relocate blocks, but bare-metal C uses raw pointers that cannot be moved safely, so the holes are permanent. Fixed-size pools sidestep the problem entirely.

    Advertisement