Sections vs Segments: Two Views of One File
Type this right now
# Compile a simple C program
gcc -o hello hello.c
# Two different views of the same binary:
echo "=== SECTIONS (compiler/linker view) ==="
readelf -S hello | head -40
echo ""
echo "=== SEGMENTS (loader/kernel view) ==="
readelf -l hello
Run both commands. You'll see two completely different lists describing the same file. By the end of this chapter, you'll understand why both exist and how they relate.
The key insight
An ELF file has two parallel indexing systems:
- Sections — the compiler's and linker's view. Fine-grained. Named. Used during compilation and linking.
- Segments — the loader's and kernel's view. Coarse-grained. Permission-based. Used when mapping the binary into memory.
Compile time Run time
─────────── ────────
┌─────────────────────┐ ┌──────────────────┐
│ Section Header │ │ Program Header │
│ Table │ │ Table │
│ │ │ │
│ .text │ │ LOAD (r-x) │
│ .rodata │──────────► │ (code + rodata) │
│ .data │ │ │
│ .bss │──────────► │ LOAD (rw-) │
│ .symtab │ │ (data + bss) │
│ .strtab │ │ │
│ .debug_* │ │ (not loaded) │
└─────────────────────┘ └──────────────────┘
Many small pieces Few big chunks
The linker needs sections so it can merge .text from file A with .text from file B. The
kernel doesn't care about any of that — it just needs to know which bytes go to which addresses
with which permissions.
Sections: the full catalog
Here are the sections you'll encounter most often:
Section Contents Permissions
───────── ────────────────────────────── ───────────
.text Machine code (your functions) r-x
.rodata Read-only data (string literals) r--
.data Initialized global variables rw-
.bss Uninitialized globals (zeroed) rw-
.symtab Symbol table (function names) --- (not loaded)
.strtab String table (section names) --- (not loaded)
.dynsym Dynamic symbol table r--
.dynstr Dynamic string table r--
.plt Procedure Linkage Table r-x
.got Global Offset Table rw-
.debug_* DWARF debug information --- (not loaded)
.rel.text Relocations for .text --- (not loaded)
.init Startup code r-x
.fini Cleanup code r-x
Not all sections get loaded into memory. .symtab, .strtab, and .debug_* exist only in the
file — the kernel ignores them entirely. They're for the linker, debugger, and tools like nm.
🧠 What do you think happens? If
.bssholds uninitialized globals that are all zero, how many bytes does it occupy in the file? Answer: zero. The section header records its size, but no actual bytes are stored. The kernel allocates and zeroes the memory at load time. This is why.bsssaves disk space.
Segments: what the kernel actually maps
readelf -l hello
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R 0x8
INTERP 0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R 0x1
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x000628 0x000628 R 0x1000
LOAD 0x001000 0x0000000000001000 0x0000000000001000 0x000185 0x000185 R E 0x1000
LOAD 0x002000 0x0000000000002000 0x0000000000002000 0x000114 0x000114 R 0x1000
LOAD 0x002db8 0x0000000000003db8 0x0000000000003db8 0x000258 0x000260 RW 0x1000
DYNAMIC 0x002dc8 0x0000000000003dc8 0x0000000000003dc8 0x0001f0 0x0001f0 RW 0x8
NOTE 0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R 0x8
NOTE 0x000368 0x0000000000000368 0x0000000000000368 0x000044 0x000044 R 0x4
GNU_EH_FRAME 0x00200c 0x000000000000200c 0x000000000000200c 0x00003c 0x00003c R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x002db8 0x0000000000003db8 0x0000000000003db8 0x000248 0x000248 R 0x1
The key segment types:
| Type | Purpose |
|---|---|
LOAD | Map these bytes into memory. This is the main event. |
INTERP | Path to the dynamic linker (/lib64/ld-linux-x86-64.so.2) |
DYNAMIC | Dynamic linking information (needed libraries, symbol tables) |
NOTE | Metadata (build ID, ABI tag) |
GNU_STACK | Stack permissions (notably: no execute) |
GNU_RELRO | Mark GOT as read-only after relocation (security) |
The mapping: sections merge into segments
This is where the two views connect. readelf -l also shows you which sections fall into each
segment:
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag
.gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .eh_frame_hdr
10 (empty — GNU_STACK has no sections)
11 .init_array .fini_array .dynamic .got
Now the picture becomes clear:
SECTIONS (file) SEGMENTS (memory)
───────────────── ─────────────────
┌─────────────┐
│ .init │───┐
├─────────────┤ │
│ .plt │───┤
├─────────────┤ ├──► LOAD segment 03 (R E)
│ .text │───┤ Executable code
├─────────────┤ │
│ .fini │───┘
├─────────────┤
│ .rodata │───┬──► LOAD segment 04 (R)
├─────────────┤ │ Read-only data
│ .eh_frame │───┘
├─────────────┤
│ .data │───┐
├─────────────┤ ├──► LOAD segment 05 (RW)
│ .bss │───┘ Read-write data
├─────────────┤
│ .symtab │ NOT LOADED
├─────────────┤ (debug/linker use only)
│ .strtab │ NOT LOADED
├─────────────┤
│ .debug_* │ NOT LOADED
└─────────────┘
Multiple sections with the same permissions merge into one segment. The kernel doesn't need
to know the difference between .init and .text — both are executable code, so they get one
mmap() call with PROT_READ | PROT_EXEC.
Why two views?
The split exists because compilation and execution have different needs.
The linker needs to:
- Merge
.textfrom dozens of object files into one.text - Merge
.datafrom dozens of object files into one.data - Apply relocations section by section
- Keep debug info separate from code
The kernel needs to:
- Map as few memory regions as possible (fewer page table entries)
- Set permissions per region (read, write, execute)
- Know the entry point
- Know where the dynamic linker is
Sections are the how (fine-grained building blocks). Segments are the what (what goes where in memory with what permissions).
💡 Fun Fact: A fully stripped, statically linked binary can have zero sections and still run. The kernel only reads program headers. You can literally delete the section header table with a hex editor and the binary works fine. Tools like
readelf -Swill complain, but the kernel won't even notice.
Seeing your code in .text
objdump -d hello | grep -A 15 '<main>'
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push rbp
114e: 48 89 e5 mov rbp,rsp
1151: 48 8d 05 ac 0e 00 00 lea rax,[rip+0xeac] # 2004 <_IO_stdin_used+0x4>
1158: 48 89 c7 mov rdi,rax
115b: e8 f0 fe ff ff call 1050 <puts@plt>
1160: b8 00 00 00 00 mov eax,0x0
1165: 5d pop rbp
1166: c3 ret
Address 0x1149 — that's in the .text section, which is inside the LOAD segment with
R E permissions. The lea instruction at 0x1151 references address 0x2004 — that's the
"Hello, ELF!\n" string in .rodata, inside the read-only LOAD segment.
Everything maps. Addresses in the code point to addresses in specific sections, which live in specific segments, which get specific permissions.
Rust comparison
// save as hello.rs, compile: rustc hello.rs fn main() { println!("Hello, ELF!"); }
readelf -l hello_rs | grep LOAD
LOAD 0x000000 0x0000000000000000 ... R 0x1000
LOAD 0x009000 0x0000000000009000 ... R E 0x1000
LOAD 0x05b000 0x000000000005b000 ... R 0x1000
LOAD 0x07e458 0x000000000007f458 ... RW 0x1000
Same pattern: read-only, executable, read-only data, read-write. The Rust binary just has more of everything because it includes the standard library.
🔧 Task: Map sections to segments by hand
- Compile:
gcc -o hello hello.c- Run
readelf -S hello— note the address and flags of each section- Run
readelf -l hello— note the address range and flags of each segment- For each section, determine which segment contains it:
- Does the section's address fall within the segment's [VirtAddr, VirtAddr+MemSiz) range?
- Do the permissions match? (A writable section should be in a writable segment)
- Verify your mapping against the "Section to Segment mapping" output at the bottom of
readelf -l- Find a section that is NOT in any segment. Why isn't it loaded?
This exercise makes the two-view model concrete. Once you can do this mapping by hand, the linker and loader will never be mysterious again.