Free — no signup required

Anatomy of ELF Sections (.text, .data, .bss, .rodata)

3 min read

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/pop instructions).
  • Heap: Grows upward. Holds dynamically allocated memory (malloc/free). Managed by the allocator library.
Interview Tip

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.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free