5.1.15 · D3C Programming

Worked examples — Memory layout — text, data, BSS, heap, stack segments

3,623 words16 min readBack to topic

This page is a drill: we hunt down every kind of question the memory-layout topic can throw at you and solve one of each — completely. If you have not yet met the segments, read the parent first: Memory layout — text, data, BSS, heap, stack segments.


The scenario matrix

Read each row as a case class — a distinct kind of situation. The rightmost column names the example that nails it.

# Case class Concrete trigger Correct segment Example
1 Global, non-zero init int g = 5; Data Ex 1
2 Global, zero / no init int g; or int g = 0; BSS Ex 1
3 static inside a function static int s = 0; BSS/Data Ex 1
4 Local (auto) variable int x; in a function Stack Ex 1, Ex 3
5 Pointer vs its target int *p = malloc(...) p→Stack, *p→Heap Ex 2
6 String literal char *s = "hi"; Text (RO) Ex 4
7 Local array on the stack char buf[10]; Stack Ex 4
8 Code / a function itself void f(void){...} Text (RO) Ex 4b
9 Disk-size degenerate (Data↔BSS) huge array, zero vs non-zero Data grows file / BSS doesn't Ex 5
10 Limiting case — collision deep recursion / giant malloc Stack overflow / NULL Ex 6
11 Word problem (real world) server session buffers Heap sizing Ex 7
12 Exam twist — freed memory use pointer after free dangling (Heap) Ex 8
13 Growth direction proof compare addresses numerically Stack↓, Heap↑ Ex 3

We now walk every row.


Ex 1 — Rows 1–4: classify every declaration

The decision procedure — apply to each name. Ask three yes/no questions in order:

  1. Is it a function's local without static?Stack. (Its lifetime is the function call.)
  2. Otherwise it has static storage. Is its initial value non-zero?Data (the value must be stored on disk).
  3. Static storage, value zero or absent?BSS (only a "reserve & zero" note is stored).

Now grind through:

Var Q1 local-auto? Q2 non-zero? Segment Why this step?
a =5 no (global) yes Data value 5 must survive on disk
b =0 no no BSS zero carries no info → no disk bytes
c (none) no no (defaults to 0) BSS uninit global = guaranteed 0
d static=7 no (static) yes Data static storage + non-zero
e =3 yes Stack plain local → dies when run returns
f static=0 no (static) no BSS static beats "inside function": static storage, zero
g static=9 no (static) yes Data static storage, non-zero

Verify (Why it's consistent): exactly the two non-zero static things (a, d, g) hit Data; the two zero static things (b, f) plus the uninitialized global (c) hit BSS; only the plain local e hits Stack. Count: Data = 3, BSS = 3, Stack = 1 — matches the matrix rows 1–4.


Ex 2 — Row 5: the pointer vs the block it points to

  1. Separate the two objects. Why this step? malloc produces two distinct things: (i) the variable p, a pointer, and (ii) the 40-byte block it will point at. Never merge them.
  2. Locate p. p is a plain local of demo with no static → question 1 = yes → Stack. Why: it lives only while demo runs.
  3. Locate the block. malloc asks the heap allocator for 40 bytes → Heap. Why: the heap holds runtime requests that must outlive the function if you return the pointer.
Figure — Memory layout — text, data, BSS, heap, stack segments

Verify (sanity check): if you return p; from demo, the returned address still points at valid heap memory (the block survives), but you could never return &(a local) safely — that address dies. This asymmetry is the whole reason heap exists.


Ex 3 — Rows 4 & 13: prove the growth directions with real numbers

  1. Compare the two stack addresses. &x = ...a1c8, &y = ...a1c4. The later-declared y sits at the lower address (). Why this step? consecutive locals are laid out as the frame is built, so their order reveals direction.
  2. Read the difference. bytes — exactly one int. New locals go down by their size → stack grows toward lower addresses.
  3. Compare the two heap addresses. q = ...880010, p = ...880000. The second malloc sat higher (). Why: the program break moves up as you request more.
  4. Read the difference. bytes. The heap handed out 16 bytes for a request the allocator rounded up. New blocks go upheap grows toward higher addresses.
Figure — Memory layout — text, data, BSS, heap, stack segments

Verify (units & magnitude): stack step = 4 bytes = sizeof(int) ✓. Heap addresses () are astronomically lower than stack addresses () — confirming heap-below, stack-above, exactly as the layout formula predicts. Difference of the two malloc addresses = .


Ex 4 — Rows 6 & 7: string literal vs local buffer (the classic confusion)

  1. Case (1): the pointer and the literal. lit is a local pointer → Stack. The string literal "hello" it points to is a compile-time constant baked into the read-only Text area. Why: literals never change, so the compiler stores one shared, read-only copy — writing through lit is undefined behaviour.
  2. Case (2): the array. buf is a local array of 6 chars. All 6 bytes live inside greet's stack frame; the initializer "hello" is copied into them at runtime. Why: an array is storage, not a pointer — so its bytes sit where the variable sits (Stack), and you can modify buf[0].

Verify: sizeof(lit) = 8 (a pointer on 64-bit); sizeof(buf) = 6 (six chars). Different sizes ⇒ different objects ⇒ different homes. ✓


Ex 4b — Row 8: where does the code itself live?

  1. Locate the instructions of greet. Why this step? a function is not a variable; it is a block of machine instructions produced by the compiler. Those instructions must sit somewhere executable and unchanging → the Text segment (read-only, shared). Why Text and not Data: code never mutates while running, so making it read-only stops both bugs and viruses from rewriting it.
  2. Locate fp. fp is a plain local of main → question 1 = yes → Stack. It merely holds the address of greet, exactly like Ex 2's p held a heap address. Why: the pointer and the thing it points at live in different segments.
  3. Predict the printed address. Because greet lives in Text (low addresses, near the bottom of the layout), fp prints a value far below stack addresses like 0x7ffe... — usually in the 0x...55... range on Linux.

Verify: sizeof(fp) = 8 (function pointer on 64-bit). Calling fp() runs greet — proof the address is a valid, executable Text address. The address is much smaller than any 0x7ffe... stack address. ✓


Ex 5 — Row 9: the disk-size degenerate case (Data ↔ BSS swap)

  1. Version B (all zero) → BSS. Why: every element is zero, so the file stores only "reserve bytes, zero them." Disk cost ≈ a few bytes of bookkeeping, essentially payload.
  2. Version A (one non-zero) → Data. Why: the array is initialized, so the compiler must be able to reproduce all integers — even though of them are zero, the array's declared value is no longer "all zero," so the whole block goes to Data and is written out.
  3. Compute the Data payload. Why this step? the file must physically hold every one of the integers, and each int occupies sizeof(int) = 4 bytes — so total bytes = (number of elements) × (bytes per element); multiplying count by element size is just "how many boxes × size of each box."

Verify (runtime vs disk): at runtime both versions occupy the same of RAM (BSS is zeroed into memory too). The saving is purely on disk: version B's file is ~4 MB smaller. Units check: bytes × count = bytes ✓. (.)


Ex 6 — Row 10: the limiting case (collision → overflow)

  1. (a) Stack budget ÷ frame size. Why this step? the stack pointer sinks by exactly one frame per call, so the number of calls that fit is "total room ÷ room-per-call" — the same reason you divide a shelf's length by one book's thickness to count how many books fit. bytes. Divide by frame size : So around the 4096th deep call the stack pointer crosses its guard page → stack overflow (a segment fault, not a malloc failure).
  2. (b) Heap budget ÷ block size. Why this step? each successful malloc(1<<20) consumes one fixed 1 MiB chunk of free memory, so the count that fits before exhaustion is again "total free ÷ per-chunk" — identical logic to (a), just a different budget and chunk. Each block = bytes. With bytes available: Around block 8192, malloc returns NULL — a different failure mode (heap exhaustion), which you can test and recover from.

Verify: ✓. ✓. Both divisions are exact.


Ex 7 — Row 11: real-world word problem

  1. Real bytes per user. Why this step? the heap must store your data plus the allocator's own metadata, so the true footprint of one user is data + overhead, not just the 512 you asked for. Per block: bytes.
  2. Multiply by users. Why this step? every user is an independent heap block of the same size, so total heap = (bytes per block) × (number of blocks) — the same "count × size-of-each" reasoning as Ex 5. bytes.
  3. Convert to MiB. Why this step? the question asks in MiB, and bytes by definition, so we divide the raw byte total by to change units. .

Verify (which segment & sanity): these records live on the Heap (runtime malloc), not Data/BSS — their count is unknown at compile time. The pointers indexing them (an array of 20,000 pointers) would themselves be bytes elsewhere. Peak heap ✓ (the 16-byte overhead pushed us just over).


Ex 8 — Row 12: the exam twist (use-after-free)

  1. Where is p? p is a plain local pointer of make → question 1 = yes → Stack, both before and after the free. Why this step: free acts on the heap block, never on the stack slot that stores the address — separating the two objects (as in Ex 2) is exactly what tells us p's home is untouched.
  2. What did free change? It returned the 4-byte heap block to the allocator. The address value sitting inside p is unchanged, but it now points at memory you no longer ownp is a dangling pointer. Why this matters: the allocator is free to hand those same bytes to the next malloc, so the block's contents are no longer yours to trust.
  3. Is *p safe to read? No. Why: reading through a dangling pointer is undefined behaviour — the block may hold 42, garbage, or another object's data, and even the read itself may fault. Nothing is guaranteed.
  4. The fix. Immediately do p = NULL; after free, and never return p for a freed block (returning it invites the caller to dereference dead memory). See Pointers and dangling pointers.

Verify: the value *p was 42 before free; after free its value is undefined (not guaranteed 42, not guaranteed anything). The only guaranteed fact is: p remained on the Stack throughout, and the freed block itself was on the Heap. ✓


Active recall

Recall Which cell does each land in?
  • static int k = 4; inside a function ::: Data (static storage, non-zero)
  • char *s = "abc"; — where is "abc"? ::: Text (read-only literal)
  • char buf[4] = "abc"; — where is buf? ::: Stack (local array)
  • The compiled body of a function f ::: Text (read-only code)
  • int *p = malloc(8) — where is p, where is the block? ::: p on Stack, block on Heap
  • Reading *p after free(p) ::: undefined behaviour; p is a dangling Stack pointer
  • int big[1000000]; vs = {1} — what differs? ::: on-disk file size (BSS vs Data), not runtime RAM

Connections

  • malloc, calloc, realloc and free — the engine behind Ex 2, 6, 7.
  • Stack frames and the calling convention — why Ex 3's locals sink downward.
  • Static and global variables — storage duration — the rule powering Ex 1.
  • Pointers and dangling pointers — Ex 8's use-after-free.
  • Buffer overflow and stack smashing — Ex 6's stack-limit collision.
  • Virtual memory and paging — why the middle gap between heap and stack exists at all.