The C heap is a region of memory that your program borrows from at runtime with malloc(), calloc() or realloc() and hands back with free(). It gives you memory whose size and lifetime you decide while the program runs. On a microcontroller with 20 KB of RAM and no memory protection, that flexibility becomes a liability, so most embedded code bans or tightly limits it.
Where the Heap Sits in Memory
A compiled C program is split into sections. On a typical bare-metal ARM Cortex-M part, the linker lays them out like this:
Flash: .text (code) .rodata (constants) .data initial values
RAM: .data | .bss | heap --> <-- stack
low address high address (_estack)
.data holds initialised globals. .bss holds zeroed globals. The heap starts right after .bss and grows upward. The stack starts at the top of RAM and grows down.
Nothing physically separates them. If the heap grows into the stack, or the stack grows into the heap, you get silent corruption. On a desktop OS, virtual memory and guard pages catch most of this. On a Cortex-M0 with no MPU, nothing does.
The Four Functions That Manage It
The standard library gives you four core functions, all declared in <stdlib.h>:
malloc(size)returns a pointer tosizebytes of uninitialised memory, orNULLon failure.calloc(n, size)allocatesn * sizebytes and zeroes them.realloc(ptr, size)resizes a block, possibly moving it to a new address.free(ptr)returns the block to the allocator.
C11 added aligned_alloc(), and C23 added free_sized() and free_aligned_sized(). Here is the basic pattern, with the checks people skip:
\\#include <stdlib.h>
\\#include <string.h>
int *make_buffer(size_t count)
{
int *buf = malloc(count * sizeof *buf);
if (buf == NULL) {
return NULL; /* allocation failed: handle it */
}
memset(buf, 0, count * sizeof *buf);
return buf;
}
void use_it(void)
{
int *data = make_buffer(256);
if (!data) { /* recover or fail safe */ return; }
data[0] = 42;
free(data);
data = NULL; /* avoid a dangling pointer */
}
Every malloc needs exactly one matching free. Miss one and you leak. Call it twice and you corrupt the allocator’s internal data.
What malloc Actually Does Under the Hood
malloc is a library function, not a hardware feature. It manages a pool of memory and keeps bookkeeping data next to every block you get.
On Linux, glibc uses ptmalloc2, derived from Doug Lea’s dlmalloc. Each allocation is a chunk with a size header in front of your data. On 64-bit x86, the minimum chunk is 32 bytes and alignment is 16 bytes. So malloc(1) actually consumes 32 bytes, of which 24 are usable.
Freed chunks go into bins: per-thread tcache bins (added in glibc 2.26), fastbins, small bins, large bins and an unsorted bin. The next malloc searches these lists for a fit. When the pool runs dry, glibc asks the kernel for more with brk/sbrk. Requests above M_MMAP_THRESHOLD, which defaults to 128 KiB, get their own mmap mapping instead.
On a microcontroller the story is simpler. Most GCC Arm toolchains ship newlib or newlib-nano (--specs=nano.specs). Its malloc calls a function named _sbrk() to extend the heap. In STM32CubeIDE projects, _sbrk() lives in sysmem.c. It hands out memory from the end of .bss and returns ENOMEM once it would cross into the space reserved for the stack, _estack - _Min_Stack_Size.
Engineer's note
_Min_Heap_Size in an STM32 linker script is a link-time check. It is not a cap. The linker only fails if heap plus stack minimums do not fit in RAM. At runtime, _sbrk() decides how far the heap can grow.
Stack vs Heap in C
| Property | Stack | Heap |
|---|---|---|
| Allocated by | Compiler, on function entry | You, via malloc/calloc/realloc |
| Freed by | Automatically on return | You, via free |
| Allocation time | Constant, one pointer adjust | Variable, depends on free list search |
| Size known | At compile time (except VLAs) | At runtime |
| Failure mode | Stack overflow, often silent on MCUs | NULL return, or fragmentation |
| Typical bugs | Returning pointers to locals | Leaks, double free, use-after-free |
| Embedded verdict | Preferred, but size it carefully | Avoided or restricted to init |
Why Embedded Code Avoids the Heap
Five problems stack up on small targets.
Fragmentation. Picture a 4 KB heap. You allocate 32 blocks of 64 bytes, then free every second one. Roughly half the heap is free, yet the largest contiguous block is still about 64 bytes. A request for 200 bytes fails. On a desktop that program restarts in a second. A motor controller running for six months has no such luxury.
Non-deterministic timing. A free-list search takes a different amount of time depending on the heap’s history. A 1 kHz control loop has a 1 ms budget. If malloc occasionally takes 80 ยตs instead of 2 ยตs, you have jitter you cannot bound on paper.
No safety net. Without an MMU there is no swap, no guard page and no OS to kill a misbehaving task. Heap-stack collisions overwrite live data instead of crashing cleanly.
Thread safety. newlib’s malloc needs __malloc_lock() and __malloc_unlock() hooks to be safe under an RTOS. Many projects never implement them, so two tasks calling malloc at once can corrupt the free list.
Certification. Safety standards forbid it outright:
| Standard | Rule | What it says |
|---|---|---|
| MISRA C:2012 | Rule 21.3 (Required) | Memory functions of <stdlib.h> shall not be used |
| MISRA C:2012 | Directive 4.12 (Required) | Dynamic memory allocation shall not be used |
| NASA/JPL Power of Ten | Rule 3 | No dynamic memory allocation after initialization |
| JPL C Coding Standard | LOC-3 rules | Mirrors the Power of Ten restriction |
The RAM budget makes all of this sharper. These are real on-chip SRAM figures for common parts:
| Microcontroller | Core | On-chip SRAM |
|---|---|---|
| ATmega328P (Arduino Uno) | AVR 8-bit | 2 KB |
| STM32F103C8 (“Blue Pill”) | Cortex-M3 | 20 KB |
| STM32F407VG | Cortex-M4F | 192 KB (incl. 64 KB CCM) |
| nRF52840 | Cortex-M4F | 256 KB |
| RP2040 (Raspberry Pi Pico) | Dual Cortex-M0+ | 264 KB |
| ESP32 | Xtensa LX6 | 520 KB |
On the ATmega328P, the 2 KB is shared by globals, stack and heap. One careless String concatenation in Arduino code can eat it.
What Firmware Does Instead
The fixes are boring and effective.
Static allocation. Size everything at compile time. Globals and static arrays live in .bss, and the linker tells you immediately if they don’t fit.
Fixed-block memory pools. Carve a static array into equal-sized blocks and hand them out from a free list. Allocation and release are O(1), and fragmentation cannot happen because every block is the same size. Zephyr’s k_mem_slab works this way.
Allocate once at boot. Follow the JPL rule: call malloc during initialization, never free, and never allocate again. You get runtime-sized buffers with no fragmentation risk.
Use the RTOS allocator deliberately. FreeRTOS replaces malloc with pvPortMalloc() and ships five schemes. You compile exactly one into your project:
| Scheme | Free supported | Coalescing | Best for |
|---|---|---|---|
| heap_1 | No | n/a | Create objects at boot, never delete |
| heap_2 | Yes | No | Legacy; superseded by heap_4 |
| heap_3 | Yes | Depends on libc | Wraps standard malloc/free with thread safety |
| heap_4 | Yes | Yes (first fit) | General use; the usual choice |
| heap_5 | Yes | Yes | heap_4 across non-contiguous RAM regions |
heap_1 through heap_4 take their memory from a static array sized by configTOTAL_HEAP_SIZE. heap_5 needs vPortDefineHeapRegions() called before the first allocation. Better still, set configSUPPORT_STATIC_ALLOCATION to 1 and use xTaskCreateStatic() and xQueueCreateStatic(). You supply the memory, so the kernel never touches a heap.
The same discipline shows up higher in the stack. Real-time guidance for ROS 2 tells you to preallocate messages and avoid allocation inside the control loop, and the realtime_support repository includes a TLSF-based allocator for that purpose. The joint controller inside a dog robot follows the identical rule at the firmware level.
Not the Same Thing: the Heap Data Structure
Search results mix two unrelated ideas. A binary heap is a tree stored in an array, where each parent is larger (max-heap) or smaller (min-heap) than its children. It powers priority queues and heapsort. It has nothing to do with malloc.
The index math is simple. For a node at index i, the parent sits at (i - 1) / 2, the left child at 2i + 1 and the right child at 2i + 2.
Creating a fixed-size heap
A fixed-size version fits embedded rules perfectly, since it lives in a static array:
\\#define HEAP_CAP 32
static int heap[HEAP_CAP];
static int heap_len = 0;
int heap_push(int value) /* max-heap insert */
{
if (heap_len >= HEAP_CAP) return -1;
int i = heap_len++;
heap[i] = value;
while (i > 0 && heap[(i - 1) / 2] < heap[i]) {
int p = (i - 1) / 2;
int tmp = heap[p]; heap[p] = heap[i]; heap[i] = tmp;
i = p;
}
return 0;
}
Insert and delete both run in O(log n). A pop removes the largest value at index 0, moves the last element to the root and sifts it down.
Creating a dynamically-sized heap
The desktop version grows its backing array with realloc, usually doubling capacity when full. That works well on Linux. On firmware, prefer the fixed array and treat a full queue as a design error to catch in testing.
Debugging Heap Problems on Embedded Targets
When you must use a heap, measure it:
- FreeRTOS:
xPortGetFreeHeapSize()gives current free space.xPortGetMinimumEverFreeHeapSize()gives the low-water mark, the number you actually size against. EnableconfigUSE_MALLOC_FAILED_HOOKto trap failures. - newlib:
mallinfo()reports arena size and bytes in use. - Stack painting: fill RAM between heap and stack with a pattern like
0xDEADBEEFat boot, then inspect it in the debugger to see how close the two came. - Desktop first: compile your logic for the host and run it under AddressSanitizer (
-fsanitize=address) or Valgrind to catch leaks and use-after-free before flashing.
Reality check
A heap that survives a one-hour bench test proves nothing about month six. Fragmentation depends on allocation history, so only a bounded design, or a long soak test under realistic load, gives you confidence.
FAQ
Is the C heap slower than the stack?
Yes. A stack allocation is one pointer adjustment. malloc searches free lists and updates metadata, and its time varies with heap history. For most desktop code the difference doesn’t matter. In a hard real-time loop it does.
What happens if malloc fails?
It returns NULL. If you dereference that without checking, you get undefined behavior. On a Cortex-M that usually means a HardFault, or writes to address 0 that corrupt whatever lives there.
Can I use malloc on Arduino?
You can, and the String class does internally. On a 2 KB ATmega328P, fragmentation shows up quickly. Use fixed char buffers for anything that runs indefinitely.
Does freeing memory return it to the operating system?
Usually not right away. glibc keeps freed chunks in its bins for reuse. Large mmap-backed blocks are unmapped on free, and the top of the heap can be trimmed. On bare metal there is no OS, so the memory simply goes back to the allocator’s free list.
Is any dynamic allocation acceptable in safety-critical firmware?
Allocation during initialization only is the common compromise, and it’s what the JPL rules permit. MISRA C still flags it, so you would need a documented deviation. Fixed-block pools are the other accepted pattern, since their timing and failure modes are fully predictable.









