Skip to content

Lesson 1 of 9 · 20m

How C Lays Out Memory: Stack, Heap, and Addresses

Map a real process: text, data, bss, heap, stack, and how to read /proc/<pid>/maps and ASLR's effect.

You inherit a daemon that crashes once a week under load, and the core dump is full of hex addresses nobody explains. Before you can chase a memory bug — or write C that doesn't ship one — you need a working model of where things actually live in a process. C gives you almost no abstraction here, which is exactly why the model matters.

A Linux process's address space is a handful of regions, each with its own lifetime and permissions:

  • .text — your compiled machine code, read-only and executable.
  • .rodata — string literals and const data.
  • .data / .bss — globals and static locals; .bss is the zero-initialized part.
  • The heap — what malloc hands you, growing upward via brk/mmap.
  • Memory-mapped regions — shared libraries, and any malloc chunk big enough that glibc served it with mmap instead of brk.
  • The stack — one per thread, holding each call's frame: locals, saved registers, and the return address. It typically grows downward on x86-64.

You can see all of it without a debugger:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static int counter;               /* .bss — zeroed */
const char banner[] = "sudo>";    /* .rodata */
int verbose = 1;                  /* .data */

int main(void) {
    int local = 42;                       /* stack */
    void *heap = malloc(64);              /* heap */
    printf("main    %p\n", (void *)main);
    printf("banner  %p\n", (void *)banner);
    printf("verbose %p\n", (void *)&verbose);
    printf("counter %p\n", (void *)&counter);
    printf("heap    %p\n", heap);
    printf("local   %p\n", (void *)&local);
    printf("pid %d — check /proc/%d/maps\n", getpid(), getpid());
    sleep(60);
    free(heap);
    return 0;
}
gcc -O0 -g layout.c -o layout
./layout &
cat /proc/$(pgrep layout)/maps

Match each printed address to a region in maps. The heap pointer lands inside [heap] for small allocations; local lands in [stack]; banner sits in a read-only page of the executable's own mapping.

Try this

Run ./layout twice and diff the addresses. They move — that's ASLR (address space layout randomization), the mitigation that makes exploit addresses unstable. Run it once with setarch -R ./layout to disable ASLR and watch the addresses stabilize. Then bump the malloc(64) to malloc(1 << 24) and look at maps again: the allocation moved out of [heap] into an anonymous mmap region.

Field notes

  • "The stack grows down" is an architecture convention, not a C guarantee — don't write code that depends on direction, only on lifetime.
  • Print pointers with %p and cast to void *. Using %x on a pointer is a classic warning-silencer that truncates on 64-bit.
  • The two stack traces that matter later — where memory was allocated and where it was touched — both become readable once you can name these regions. Keep this lesson's program; you'll rebuild it under ASan.