High-level languages spoil you. Type new or call malloc() and a chunk of memory just appears, ready to use, with someone else’s code quietly handling all the bookkeeping. Drop down to assembly and that convenience vanishes — you either build the bookkeeping yourself or call into a library that does it for you, one instruction at a time. Understanding how memory allocation actually works at this level changed how I think about every higher-level language I use, because it turns out “memory management” in assembly is really just a handful of well-understood strategies applied directly to registers, stack pointers, and system calls.
The Memory Landscape First
Before allocation strategies make sense, you need the map. A typical process’s address space is divided into regions:
flowchart TB
subgraph AddressSpace["Process Address Space (low to high addresses)"]
direction TB
A["Text/Code Segment (.text)"]
B["Initialized Data (.data)"]
C["Uninitialized Data (.bss)"]
D["Heap (grows upward via brk/sbrk or VirtualAlloc)"]
E["Memory-mapped region (mmap, shared libs)"]
F["Stack (grows downward)"]
end
A --> B --> C --> D --> E --> F
- Stack: automatic, LIFO, managed almost entirely by the CPU’s stack pointer register and
PUSH/POP/CALL/RETinstructions. - Heap: dynamic, managed explicitly — either by hand-rolled allocator code or by calling the C runtime’s
malloc/free(or the OS directly). - Static/global storage: fixed at link time, sizes reserved via assembler directives.
Static Allocation: Reserving Memory at Assembly Time
The simplest form of “allocation” isn’t runtime allocation at all — it’s telling the assembler to reserve space that exists for the entire life of the program.
x86-64 NASM Example
section .data
counter dd 0 ; 4-byte initialized value
greeting db "Hello", 0 ; initialized byte string
section .bss
buffer resb 256 ; reserve 256 uninitialized bytes
big_array resq 100 ; reserve 100 quadwords (800 bytes)
This memory’s address is fixed once the program is linked; there’s no “allocation failure” possible at runtime because the OS reserved this space when the process was loaded.
ARM Assembly Equivalent (GNU syntax)
.data
counter: .word 0
greeting: .asciz "Hello"
.bss
buffer: .space 256
Stack Allocation: Fast, Automatic, Scoped
Stack allocation is essentially “free” in terms of bookkeeping — you just move the stack pointer.
; x86-64 System V calling convention
push rbp
mov rbp, rsp
sub rsp, 64 ; "allocate" 64 bytes of local stack space
; ... use [rbp-8], [rbp-16], etc. as local variables ...
mov rsp, rbp ; deallocate — instantly
pop rbp
ret
On ARM:
PUSH {R4-R7, LR}
SUB SP, SP, #64 ; reserve 64 bytes on the stack
; ... use [SP, #offset] for locals ...
ADD SP, SP, #64 ; release it
POP {R4-R7, PC}
Stack allocation is extremely fast (just an arithmetic adjustment to RSP/SP) but strictly scoped — memory disappears the moment the function returns, and there’s a hard size limit (the stack’s reserved region, often a few MB by default).
Dynamic Allocation: The Heap
When you need memory whose size or lifetime isn’t known until runtime, or that must outlive the function that created it, you need the heap. In pure assembly, you have two real options.
Option 1: Call the OS Directly
Linux x86-64 example using brk:
; Extend the heap using the brk syscall
mov rax, 12 ; syscall number for brk
mov rdi, 0 ; pass 0 to query current break
syscall
mov r12, rax ; r12 = current break (start of new region)
mov rax, 12
lea rdi, [r12+4096] ; request 4096 more bytes
syscall ; rax now holds the new break address
Linux x86-64 example using mmap (preferred for larger/anonymous allocations):
mov rax, 9 ; syscall number for mmap
xor rdi, rdi ; addr = NULL, let kernel choose
mov rsi, 4096 ; length
mov rdx, 3 ; PROT_READ | PROT_WRITE
mov r10, 0x22 ; MAP_PRIVATE | MAP_ANONYMOUS
mov r8, -1 ; fd = -1 (no backing file)
xor r9, r9 ; offset = 0
syscall ; rax = pointer to new memory, or negative errno
Windows x86-64 equivalent (conceptually): call VirtualAlloc through the standard calling convention rather than a raw syscall, since Windows doesn’t expose stable raw syscall numbers to user code the way Linux does.
Option 2: Call an Existing Allocator (malloc/free)
Most assembly code that needs heap memory just calls the C library’s allocator rather than reinventing it:
; x86-64 System V: call malloc(64)
mov rdi, 64
call malloc
test rax, rax
jz alloc_failed ; malloc returns NULL on failure
mov [my_ptr], rax
; ... later ...
mov rdi, [my_ptr]
call free
Building Your Own Simple Allocator
Understanding allocator internals matters even if you rarely write one from scratch. A classic simple design is a free list allocator:
flowchart LR
subgraph Heap["Heap Memory"]
H1["Header: size=32, used"] --> B1["32 bytes data"]
B1 --> H2["Header: size=64, free"] --> B2["64 bytes data"]
B2 --> H3["Header: size=16, used"] --> B3["16 bytes data"]
end
FL["Free List Head"] -.-> H2
Each allocation is preceded by a small header recording its size and status. A free_list pointer chains together available blocks. Allocating means walking this list for a big-enough free block (first-fit, best-fit, or worst-fit strategies), splitting it if it’s larger than needed. Freeing means marking the block free and ideally coalescing it with adjacent free blocks to fight fragmentation.
Comparison of Allocation Strategies
| Strategy | Speed | Lifetime Control | Fragmentation Risk | Typical Use |
|---|---|---|---|---|
| Static (.data/.bss) | Instant (link-time) | Whole program | None | Globals, lookup tables |
Stack (sub rsp) | Extremely fast | Function scope | None | Local variables, small buffers |
| Heap via OS syscall | Slower (kernel transition) | Manual, unbounded | Managed by kernel/page tables | Large or page-aligned allocations |
| Heap via malloc/free | Moderate (library overhead) | Manual, unbounded | Managed by allocator (fragmentation possible) | General dynamic data |
| Custom allocator | Tunable | Manual, unbounded | Depends entirely on design | Performance-critical or embedded systems |
Static/stack advantages: predictable, essentially free, no fragmentation. Static/stack disadvantages: fixed size known at compile time (static) or scope-bound (stack); can’t outlive their context. Heap advantages: flexible size and lifetime. Heap disadvantages: slower, must be manually freed (or leaks), subject to fragmentation.
Operating System Interaction
Every dynamic allocator, no matter how clever, eventually rests on OS-provided primitives: brk/sbrk and mmap on Linux, VirtualAlloc/HeapAlloc on Windows. The OS manages actual physical memory and page tables; the allocator you call from assembly just manages how the address space it received gets subdivided among your program’s requests. This is why a page fault can occur even in seemingly “already allocated” memory — the OS may use demand paging, only mapping physical pages in when they’re first touched.
Debugging and Optimization Considerations
- Common mistake: forgetting to check for a NULL/negative return from
malloc,brk, ormmap— writing to an invalid pointer from a failed allocation is a classic segfault source. - Common mistake: stack overflow from allocating large buffers with
sub rspinside deeply recursive functions — the stack has a hard, often surprisingly small, limit. - Debugging tools: Valgrind and AddressSanitizer catch heap corruption, use-after-free, and leaks even in hand-written assembly (as long as it calls standard
malloc/free). GDB’sxcommand lets you inspect raw memory directly around your allocations. - Optimization tip: batch small allocations into a single larger one (a custom pool/arena allocator) when you control the pattern — this avoids per-call syscall/library overhead and improves cache locality.
- Optimization tip: align allocations to cache-line or page boundaries (
AND-masking addresses, or using aligned mmap/VirtualAlloc calls) when performance-sensitive data structures are involved.
Practical Use Cases
- Writing a custom memory allocator for an embedded system with no OS and no libc.
- Implementing a bootloader that must manage its own memory map before any runtime exists.
- Hand-optimizing hot paths in performance-critical libraries where library-call overhead from
mallocis unacceptable.
Best Practices
- Prefer static or stack allocation whenever the size and lifetime are known — it’s simpler and faster.
- Always check the return value of any allocation call before using the returned pointer.
- Match every allocation with exactly one deallocation; track ownership carefully in an environment with no garbage collector.
- If you write a custom allocator, implement coalescing of freed blocks early — fragmentation problems compound over the life of a long-running program.
- Use OS-provided allocation (
mmap/VirtualAlloc) for large or page-aligned chunks; usemalloc/freeor your own allocator for smaller, frequent requests.
FAQs
Does assembly have a built-in malloc instruction? No — there’s no CPU instruction for heap allocation. It’s always implemented in software, either via OS system calls or a runtime library function you call using the standard calling convention.
Why use mmap instead of brk for large allocations? mmap allocations are independent, page-granular regions that can be released individually back to the OS; brk only manages a single contiguous heap that can only shrink from its topmost end, making large temporary allocations awkward to release.
Can stack memory be “allocated” dynamically based on a runtime size? Yes, via alloca()-style stack adjustment (subtracting a runtime-computed value from RSP/SP), but this is risky — there’s no safety net if it exceeds the stack’s bounds.
What happens if I forget to free heap memory in assembly? Exactly what happens in any other language without a garbage collector: a memory leak. The process’s resident memory grows until it’s terminated, since nothing in the OS reclaims memory it doesn’t know is unused.
Summary and Key Takeaways
Memory allocation in assembly comes down to three layers: static allocation (compile/link-time, fixed for the whole program), stack allocation (fast, scope-bound, just a pointer adjustment), and heap allocation (dynamic, manually managed, built either on raw OS system calls like mmap/brk or on a library allocator like malloc/free). There is no automatic garbage collection — every byte you take, you’re responsible for giving back.
Key takeaways:
- Static and stack allocation are essentially free but inflexible in size/lifetime.
- Heap allocation via
mmap/brk(Linux) orVirtualAlloc(Windows) is how all higher-level allocators eventually get their memory. - Custom allocators (free-list based) trade implementation complexity for control over performance and fragmentation.
- Always validate allocation results and match every allocation with a corresponding free.
References
- Intel® 64 and IA-32 Architectures Software Developer’s Manuals, Volume 1 — memory organization chapters.
- AMD64 Architecture Programmer’s Manual, Volume 2 — System Programming, virtual memory chapters.
- ARM Architecture Reference Manual — Virtual Memory System Architecture (VMSA) chapter.
- GNU C Library (glibc) manual — Memory Allocation chapter, and GNU Assembler (GAS) documentation for
.data/.bss/.spacedirectives. - Linux man pages:
brk(2),mmap(2).
