The Four Segments

Every C program you compile ends up as a process image with a small number of memory regions. You’ll hear “stack, heap, BSS, text” a hundred times — that’s the four-segment model, and it’s the mental picture you should carry into every debugging session. In practice on modern Linux/macOS there are more regions (rodata, shared libraries via mmap, TLS blocks, the guard page below the stack), but the four-segment view is the one you draw on the whiteboard and it’s not wrong — it’s compressed.

The Picture

 High memory
 +---------------------------+
 | Command-line args & env   |
 +---------------------------+
 | Stack       (grows down)  |  <-- auto locals, return addrs, saved regs
 |         v v v             |
 |                           |
 |         ^ ^ ^             |
 | Heap        (grows up)    |  <-- malloc/calloc/realloc live here
 +---------------------------+
 | BSS   (zero-initialized)  |  <-- static/global vars with no initializer
 +---------------------------+
 | Data  (initialized)       |  <-- static/global with an initializer
 +---------------------------+
 | Rodata                    |  <-- string literals, const globals
 +---------------------------+
 | Text  (code)              |  <-- your compiled instructions
 +---------------------------+
 Low memory (near 0x400000 on Linux; ASLR randomizes)

Text is read-only executable; write to it and you get SIGSEGV. Rodata is read-only data. Data is initialized static storage. BSS is zero-initialized static storage (the OS gives you zero-filled pages on demand, so BSS costs nothing on disk in the ELF file — only its size is recorded). Heap grows toward high addresses through brk/sbrk or mmap. Stack grows toward low addresses, one frame per function call.

Where Does Each Variable Live?

This is the single most useful table in this month. Memorize it.

Declaration

Segment

Lifetime

int x; inside a function

Stack

Until function returns

int x = 5; inside a function

Stack

Until function returns

static int x; inside a function

BSS

Entire program

static int x = 5; inside a function

Data

Entire program

int x; at file scope

BSS

Entire program

int x = 5; at file scope

Data

Entire program

const char *s = "hi";

Pointer on stack, "hi" in rodata

See both

char s[] = "hi"; inside a function

Stack (array is a copy)

Until function returns

malloc(n) return value

Heap

Until you free it

int arr[8126311]; inside main

Stack

Boom (stack overflow)

The last row is the classic. On most Linux systems ulimit -s reports 8192 (KB) — that’s your entire stack budget for main, everything it calls, and every recursion frame. A int[8126311] is 4 MB, technically fits, but two of them or one deeper call chain and you’re dead. On macOS the default main-thread stack is also ~8 MB; non-main threads default to 512 KB unless you set pthread_attr_setstacksize. Big arrays go on the heap, always.

Verifying With Real Tools

Don’t take this on faith — you have Unix, so prove it. Take a tiny program with one of each kind of variable and inspect it:

$ cat > seg.c <<'EOF'
#include <stdio.h>
#include <stdlib.h>
int g_bss;                       // BSS
int g_data = 42;                 // Data
const char *rodata_ptr = "hi";   // pointer in Data, "hi" in rodata
int main(void) {
    static int s_bss;            // BSS
    static int s_data = 7;       // Data
    int local = 1;               // Stack
    int *h = malloc(sizeof *h);  // Heap
    printf("g_bss  %p\n", (void*)&g_bss);
    printf("g_data %p\n", (void*)&g_data);
    printf("s_bss  %p\n", (void*)&s_bss);
    printf("local  %p\n", (void*)&local);
    printf("heap   %p\n", (void*)h);
    printf("code   %p\n", (void*)main);
    free(h);
}
EOF
$ cc -o seg seg.c && ./seg
$ size seg      # shows text/data/bss sizes
$ nm seg | grep -E ' [BbDdTtRr] '   # symbols by section

When you run this, addresses will cluster: code will be lowest, g_data and g_bss next, heap further up, local way up top (stack). Because of ASLR the absolute numbers change every run; the relative ordering doesn’t. On macOS use otool -l seg instead of size.

Stack Growth In Detail

Each function call pushes a frame onto the stack: return address, saved base pointer, callee-saved registers the function will clobber, then space for its locals (rounded up for alignment). On x86-64 the ABI aligns %rsp to 16 bytes at every call. Recursion is stack growth in disguise — a naive recursive Fibonacci hits ~1 KB per frame if you’re unlucky with local layout, so ~8000 recursive calls before overflow. Iterative or tail-call-friendly code is not just about speed; it’s about not falling off the cliff.

Below the stack sits a guard page mapped PROT_NONE. When your stack grows into it, the CPU faults and the kernel delivers SIGSEGV. That’s what “stack overflow” feels like in C — not an exception, a crash.

What Most People Get Wrong About This

They conflate “static” the keyword with “BSS” the segment. static in C has two orthogonal meanings: storage duration (lives for the whole program) and linkage (name not visible outside the translation unit at file scope). A static int x = 5; lives in Data, not BSS, because it has a nonzero initializer. A static int x; lives in BSS. Both have static duration; that’s what unifies them. Get this wrong on a whiteboard and a senior engineer will smile the wrong kind of smile.

Practical Rules You’ll Use Weekly

  • Anything more than a few KB — heap. Don’t argue with the stack.

  • String literals are const char* — writing through them is undefined behavior even though the type doesn’t say const. Compile with -Wwrite-strings.

  • static locals are your friend for lookup tables and cached state, but they make functions non-reentrant — don’t use them in code that might run on multiple threads.

  • Zero-initialization of BSS is free. static int table[<phone_number_or_numberic_id_or_random_id_24>]; costs zero disk space and gets zeroed by the loader. static int table[<phone_number_or_numberic_id_or_random_id_24>] = {1}; costs 4 MB in the binary. This matters.


Return to README.md · Next: 02_pointer_arithmetic.md