5.1.15 · D5C Programming

Question bank — Memory layout — text, data, BSS, heap, stack segments

2,307 words10 min readBack to topic

Vocabulary you need before the traps

Some traps below lean on low-level Unix terms. Define them once here so nothing on the page is used before it is built.


The picture you are drilling against

Before the traps, fix the layout in your eye. This is the virtual address space one process sees — a tall column of bytes, lowest address at the bottom, highest at the top. Figure 1 is the full map, including the read-only rodata section and the kernel region at the very top.

Figure — Memory layout — text, data, BSS, heap, stack segments

Figure 1 — Full user address space (low → high). Text, rodata, Data, BSS, the brk heap, the shared mmap/library region, the stack, and the kernel band at the very top that user code cannot touch.

Figure 2 captures the one dynamic idea the whole page hinges on: heap and stack expand into a shared gap from opposite ends, where the small brk heap sits, and where large mmap blocks actually land.

Figure — Memory layout — text, data, BSS, heap, stack segments

Figure 2 — Heap vs stack growing into one shared gap. The program break rises from below, the stack pointer falls from above, mmap blocks land off to the side, and the guard page marks the point where a collision becomes a crash.


True or false — justify

An uninitialized global int g; contains garbage until you assign it.
False — globals and statics live in BSS and the C standard guarantees they start at zero; only automatic (stack) variables hold garbage.
A global int x = 0; lives in the Data segment because you wrote an initializer.
False — an explicit = 0 is still all-zero information, so the compiler places it in BSS, not Data; only non-zero initializers force Data.
BSS uses less RAM at runtime than Data.
False — the saving is on disk only; at load time the OS reserves and zero-fills BSS in RAM exactly like Data, so runtime memory is the same.
The Text segment is read-only so viruses cannot inject code into a running program.
Mostly true — under W^X (write XOR execute; see vocabulary box) Text pages are executable-not-writable, so overwriting your own instructions faults; but attackers can still hijack control flow to redirect execution elsewhere (see Buffer overflow and stack smashing).
The heap and stack each have a fixed, pre-assigned size boundary between them.
False — they grow toward each other through a shared gap so either can consume most of the free space; the "boundary" is dynamic and only reached in a collision.
static int s = 0; inside a function lives on the stack because it is declared locally.
False — static gives static storage duration, so s lives in BSS (zero init) and persists across calls; only its scope is local.
Two processes running the same program share one copy of the Text segment.
True — instructions never change while running, so the OS maps one read-only physical copy into both, saving memory (see Virtual memory and paging); shared libraries are mmapped and shared the same way.
Successive malloc calls always return higher addresses than the previous one.
False — the trend is upward as the program break rises, but freed blocks, allocator bins, and mmap-backed large allocations can hand back lower or scattered addresses.
A pointer returned by malloc is destroyed when the function that called malloc returns.
False — the pointer variable on the stack dies, but the heap block it pointed to survives; if no other pointer holds it, you have leaked memory.
Stack overflow and running out of heap are the same "out of memory" condition.
False — stack overflow is the stack pointer crossing its guard page (deep recursion / huge locals); heap exhaustion is malloc returning NULL. Different segments, different symptoms.
The kernel lives somewhere in the middle of your program's address space.
False — the kernel is mapped into the highest addresses, walled off from user code; a user read/write there faults. Your code, heap and stack all sit below it.

Spot the error

"Uninitialized globals are unpredictable, so I always initialize them to zero to be safe."
The zero-init is redundant — the standard already zeroes BSS globals — and worse, = 0 doesn't move it to Data anyway. The habit isn't harmful, but the reasoning (that it was unsafe) is wrong.
"I made a huge int buf[1000000]; global, so my executable file grew by 4 MB."
Wrong — an all-zero (or uninitialized) global goes to BSS, which stores only a size record; the file barely grows. Add a non-zero initializer and then it bloats via the Data segment.
"To keep memory after the function returns, I declared the array static inside the function."
This does keep the memory (static storage), but the array is a single fixed instance shared across all calls — not a fresh block each time. For per-call surviving memory use the heap.
"My program crashed with stack overflow, so I called free() to release stack space."
free only releases heap blocks and does nothing to the stack. Stack frames are reclaimed automatically on return; the fix is less recursion depth or smaller locals, not free.
"I returned &local from a function and the caller used it fine, so returning stack addresses works."
It appeared to work by luck — local is on the stack and its frame is gone after return, so the pointer is dangling (see Pointers and dangling pointers); the memory may be overwritten anytime.
"char *msg = "hello"; — I can safely do msg[0] = 'H'."
The string literal lives in the read-only rodata section, so writing to it is undefined behavior (often a crash). Copy it into a stack or heap buffer first if you need to modify it.

Why questions

Why split globals into two segments (Data and BSS) instead of one?
Because zero-valued data carries no information to store on disk — BSS records only "reserve N bytes, fill with 0", keeping the executable small while Data stores actual non-zero bytes.
Why does the stack grow down (toward lower addresses) while the heap grows up?
So both can share one big middle gap and expand toward each other; neither needs a hard pre-set size, and they only collide in extreme cases.
Why is stack memory freed automatically but heap memory is not?
The stack is strictly LIFO — a return simply moves the stack pointer back (see Stack frames and the calling convention). The heap has no such order; only you know when you are done with a block, so you must call free.
Why is the Text segment kept separate and marked read-only?
Separating code lets many processes share one immutable copy, and under W^X the MMU marks those pages non-writable so accidental or malicious writes to instructions fault (see Virtual memory and paging).
Why do local variables often show much higher addresses than malloced blocks?
The stack sits near the high end of user space (just below the kernel band) and the brk heap grows up from low addresses above BSS, so a snapshot usually shows stack addresses far above heap ones.
Why does the C standard guarantee zero for statics but not for locals?
Statics/globals are set up once at load by zero-filling BSS — cheap and deterministic. Zeroing every stack frame on every call would cost time, so locals are left as whatever bytes were there.
Why are shared libraries mmapped rather than copied into each program?
mmapping a library read-only lets every process map the same physical pages, so one copy of libc serves them all — the same sharing trick as the Text segment.

Edge cases

A global array declared but never used: does it still consume RAM at runtime?
Yes if the compiler/linker keeps it — BSS still reserves and zeroes its bytes at load time; aggressive dead-code elimination may drop truly unreferenced ones.
int *p = malloc(0); — where does anything live, and is p usable?
The pointer variable p itself lives on the stack, but any bytes malloc reserves come from the heap; malloc(0) may return NULL or a unique non-dereferenceable pointer, so reading/writing through it is undefined — only free(p) is safe.
A recursion with no base case: which segment fills up and what happens?
Each call pushes a new frame onto the stack, driving the stack pointer down until it hits the guard page → stack overflow (a crash), independent of how much heap is free.
Can the heap ever occupy a lower address than part of the Data/BSS region?
In the classic model no — the brk heap begins just above BSS as the program break rises; but large allocations via mmap land elsewhere in the middle, so the "always above BSS" rule holds only for the brk-based heap.
You free(p) then read *p. Which segment is involved and why is it dangerous?
The block is still physically on the heap, but ownership returned to the allocator, so p is dangling; the bytes may be reused or scrambled at any moment (see Pointers and dangling pointers).
A static const int table[] = {1,2,3}; — Data or BSS or something else?
It has non-zero initializers, so it is not BSS; because it is const it typically lands in the read-only rodata section rather than writable Data.
Can user code read the kernel-space addresses at the top of the map?
No — those pages are marked kernel-only; a user-mode read or write there triggers a protection fault, which is exactly what keeps the kernel safe from stray pointers.

Connections

  • Static and global variables — storage duration — the Data-vs-BSS decision rules.
  • malloc, calloc, realloc and free — heap lifetime and the traps above.
  • Pointers and dangling pointers — the returning-stack-address and use-after-free traps.
  • Stack frames and the calling convention — why the stack self-cleans.
  • Virtual memory and paging — pages, permissions, mmap and the kernel split.
  • Buffer overflow and stack smashing — why read-only Text isn't full protection.