The Four Rooms
An ELF file is divided into named sections, each with a specific purpose and a specific set of memory permissions. The kernel enforces these permissions at runtime using the MMU (Memory Management Unit) — the hardware component that translates virtual addresses to physical ones and checks access rights. Violating a section's permissions causes a segmentation fault (SIGSEGV).
Understanding sections is not academic. It explains why certain bugs crash programs, why some binaries are large and others tiny, and how security features like NX (No-Execute) work.
1. .text — The Code Section
This section contains the compiled CPU instructions — the actual machine code the processor executes. It is mapped into memory as Read + Execute (RX), never Writable. This is intentional: if a running program could modify its own instructions, it would be trivially exploitable. The NX bit (also called W^X — "Write XOR Execute") is a hardware-enforced rule that a memory page cannot be both writable and executable simultaneously. The .text section is the canonical example of this principle in action. (Section-inspection tools can list a binary's sections directly and confirm this layout on any compiled program.)
2. .data — Initialized Global Variables
This section holds global and static variables that have an explicit initial value in the source code. For example:
int connection_limit = 100; // lives in .data
The value 100 must be stored on disk so the loader can copy it into RAM when the program starts. Permissions are Read + Write (RW).
3. .rodata — Read-Only Data
String literals and compile-time constants live here. For example:
printf("Hello, world\n"); // "Hello, world\n" lives in .rodata
const int MAX_RETRIES = 5; // also .rodata
Permissions are Read Only (R). Attempting to modify a string literal (a classic C bug) causes a segfault because the kernel enforces this at the hardware level.
4. .bss — Uninitialized Global Variables
This is the most counterintuitive section. It holds global and static variables that are declared but not initialized, or explicitly initialized to zero:
int huge_array[1000000]; // lives in .bss
static char buffer[65536]; // also .bss
The critical property: .bss takes zero bytes on disk. The ELF header simply records "reserve 4 MB of RAM here and zero it out before the program starts." The kernel and loader handle the zeroing. This is a decades-old optimization — storing four million zeros on disk would be wasteful and pointless.
Permissions are Read + Write (RW).
The Stack and Heap
Two more memory regions exist at runtime but are not sections in the ELF file:
- Stack: Grows downward. Holds local variables, function call frames, and return addresses. Managed automatically by the CPU (
push/popinstructions). - Heap: Grows upward. Holds dynamically allocated memory (
malloc/free). Managed by the allocator library.
Interviewers frequently ask: "What's the difference between .data and .bss?" The trap is answering only in terms of initialization. The complete answer covers disk representation: .data stores its values on disk (inflating binary size), while .bss stores only a size annotation — the actual zeroed memory is allocated at load time. Follow up by mentioning that this is why int arr[1000000]; (global) costs nothing on disk but int arr[1000000] = {1}; adds ~4 MB to the binary.