Visual walkthrough — Memory layout — text, data, BSS, heap, stack segments
Step 0 — What is "an address" anyway?
The whole rest of this page is about which boxes hold what. That's it. Every symbol we use — Text, Data, BSS, Heap, Stack — is just a named stretch of this street.
Look at the figure: the horizontal bar is the street. The number under each tick is the address. We will now fill it, left (low) to right (high).
Step 1 — The code has to live somewhere: the Text segment
WHAT. The first stretch, at the lowest addresses, holds the compiled machine instructions — the actual add, jump, load operations your C turned into. We call this the Text segment (confusingly, "text" here means code, not words).
WHY. The CPU needs to read instructions to run them. Two facts force Text to be its own segment:
- Instructions never change while the program runs, so this stretch is marked read-only. If a bug (or virus) tries to write here, the OS kills the program. Safety for free.
- If 100 copies of the same program run, the code is identical — so the OS stores one read-only copy and lets all 100 share it. Memory saved.
PICTURE. In the figure, the blue block at the far left is Text. Notice the little lock icon — read-only. The arrow shows the CPU's instruction pointer reading from it, never writing.
Step 2 — Globals with a real value: the Data segment
WHAT. Right after Text sits the Data segment. It holds global and static variables that you initialized to a non-zero value.
WHY. Consider:
Here the symbol g_init is a global variable, and = 5 is its initial value. That literal is information — the program cannot invent it at runtime, so it must be written into the executable file on disk and copied into RAM at load time. That "carry a real value from disk" job is exactly what Data does.
PICTURE. The orange block, just above Text. Inside it we've drawn the box for g_init already containing the number , loaded straight from the file.
Step 3 — The clever trick: why zeros get their own segment (BSS)
WHAT. Next stretch: the BSS segment (the name is a historical abbreviation — just treat it as a label). It holds globals/statics that are uninitialized or explicitly set to zero.
WHY — this is the elegant bit. Compare two globals:
a's value is information → it must be stored on disk (that's Data, Step 2).b's value is no information at all. A million zeros are still just... zero.
So instead of wasting disk writing zeros, BSS stores only a sticky note:
Here is the total byte-count of every zero global. At load time the OS reads this note and zero-fills that RAM. Result: a global int big[1000000]; (4 MB of zeros) adds ~0 bytes to your .exe — only the number grows.
PICTURE. The green block. On disk it's drawn thin (just the note " bytes"); at runtime an arrow inflates it to its full RAM size, all boxes showing .
Step 4 — Runtime requests grow upward: the Heap
WHAT. After BSS begins a large empty region. From its bottom, the Heap grows upward toward higher addresses. The heap is the memory you grab at runtime with malloc/calloc/realloc (see malloc, calloc, realloc and free).
WHY grow up, and why a separate segment? The compiler cannot know in advance how much runtime memory you'll want — that depends on input. So it can't reserve a fixed box like Data/BSS. Instead there's a movable frontier:
Each malloc pushes the program break to a higher address, claiming more of the empty region. The symbol here is just a marker; the OS calls brk/sbrk to move it. Growing up (away from Text) means the heap expands into the middle emptiness.
PICTURE. From the bottom of the empty region, an orange arrow labelled "program break" pushes upward as three malloc blocks stack on top of each other, addresses increasing.
Step 5 — Function calls grow downward: the Stack
WHAT. At the very top (highest addresses) sits the Stack. Every time you call a function, a new stack frame (its parameters, return address, and local variables) is placed on the stack, and the stack grows downward toward lower addresses (see Stack frames and the calling convention).
WHY down, toward the heap? The stack and heap share the same empty middle region. If the heap grows up from the bottom and the stack grows down from the top, then either one can use as much of the free gap as it needs — no fixed wall decided in advance. The frontier here is a CPU register:
Calling a function subtracts from rsp (grows down, "push"); returning adds back (shrinks, "pop"). Automatic — that's why locals vanish when a function returns.
PICTURE. From the top, a blue arrow labelled "stack pointer" pushes downward; each function call adds a frame below the previous one. Between the descending stack and the rising heap lies the free gap.
Step 6 — The collision case (degenerate / edge behaviour)
WHAT. What if the heap keeps rising and the stack keeps sinking until they meet?
WHY it matters. They share the gap, so in extreme cases the frontiers collide:
- Stack overflow — too-deep recursion or a huge local array drives the stack pointer past its limit (down into forbidden territory). The OS kills the program.
- Heap exhaustion —
malloccan't move the program break any further, so it returns the special "failed" pointer:
These are different failures in different segments — never confuse them.
PICTURE. The gap shrinks to zero; a red flash marks the collision, with the two culprit arrows labelled.
Step 7 — Putting a real program onto the map
WHAT. Now place every variable from a small program onto the street we just built.
int g_init = 5; // (A)
int g_zero = 0; // (B)
int g_uninit; // (C)
int main(void) {
int local = 3; // (D)
static int s = 0; // (E)
int *p = malloc(40);// (F) pointer p and its 40-byte block
}WHY each lands where it does (apply the rules from Steps 2–5):
| Var | Segment | The one deciding fact |
|---|---|---|
A g_init=5 |
Data | global and non-zero init → value stored on disk |
B g_zero=0 |
BSS | global, zero → just a "reserve, zero-fill" note |
C g_uninit |
BSS | uninitialized global = treated as zero |
D local=3 |
Stack | auto variable inside main's frame |
E s=0 |
BSS | static = static storage, zero → BSS (not Stack!) |
F p |
Stack | the pointer variable is a local |
F *p (40 B) |
Heap | malloc returns heap memory |
The subtle ones — (E) a static local is not on the stack (it lives the whole program, so it obeys the global rules → BSS since it's zero); and (F) the pointer p and the block it points to live in two different segments. See Static and global variables — storage duration.
PICTURE. Each variable drawn as a labelled box sitting in its correct segment, with (F)'s pointer on the stack and an arrow reaching down to its heap block.
The one-picture summary
This single figure compresses everything: low→high addresses left to right; Text (read-only code), Data (non-zero globals, real values on disk), BSS (zero globals, just a size note), then the shared empty region with Heap rising from the bottom (program break, malloc) and Stack sinking from the top (stack pointer, function calls), colliding only in the extreme.
Recall Feynman retelling — the whole walkthrough in plain words
Your program gets a long street of numbered boxes. At the low-number end we bolt down the instruction manual — Text — and lock it so nobody scribbles on it. Just above it we put the toys that came pre-filled — Data — because a 5 is real stuff we had to carry from disk. Next come empty boxes with a sticky note "reserve N, fill with zeros" — BSS — so we never haul a million zeros in the moving truck. The whole middle is empty floor: from the bottom, the toy pile grows up every time you say malloc (that top edge is the program break), and from the ceiling, a stack of plates grows down every time you start a chore/function (that edge is the stack pointer, and finishing the chore takes the plate back automatically). They only crash if the pile and the plates meet — stack overflow or malloc returning NULL. Finally, to place any variable: is it global/static? non-zero → Data, zero → BSS. Is it a plain local? → Stack. Did malloc make it? → Heap (but the pointer to it is still a local on the Stack).
Recall Rapid self-test
Read-only segment? ::: Text.
int x = 5; global lives where? ::: Data.
int x = 0; global lives where? ::: BSS.
static int s = 0; inside a function? ::: BSS (static storage, zero).
The pointer p in int *p = malloc(40);? ::: Stack.
The 40-byte block p points to? ::: Heap.
Which frontier moves the heap? ::: The program break (grows up).
Which frontier moves the stack? ::: The stack pointer / rsp (grows down).
malloc failed — what does it return? ::: NULL.
Connections
- malloc, calloc, realloc and free — how the heap frontier is moved.
- Stack frames and the calling convention — what each downward-growing frame contains.
- Static and global variables — storage duration — the Data-vs-BSS deciding rule.
- Pointers and dangling pointers — the pointer-on-stack / block-on-heap split gone wrong.
- Virtual memory and paging — how these segments map to physical RAM.
- Buffer overflow and stack smashing — what happens when the stack is written past its frame.