Skip to main content
Be Bitwise

The difference between a section and a segment

ELF has both, they overlap, and the distinction trips up everyone reading the docs for the first time. Here's the short version, with readelf output.

#elf#linker#loader#binary-formats

Open the ELF spec, scroll for ten minutes, and you'll find both sections and segments. They describe the same file, often the same bytes, and they sound almost like synonyms in casual speech. They aren't. Each exists because two different programs need to read the file for two different reasons.

Sections are the linker's view

A section is a fine-grained chunk of the file: .text for executable code, .rodata for read-only constants, .data for initialised globals, .bss for zero-initialised globals, .symtab for symbols, .rela.dyn for relocations, and a long tail of more obscure ones. Sections are listed in the section header table, which sits at an offset given by e_shoff in the ELF header.

Why so granular? Because the linker needs that granularity. When ld combines two object files, it concatenates each named section from each input into the corresponding section in the output. .text from a.o plus .text from b.o becomes .text in a.out. The linker needs to know which bytes are code, which are read-only data, which are uninitialised, and which are bookkeeping that exists only to help the linker do its job.

bash
$ readelf -S /bin/ls
There are 31 section headers, starting at offset 0x...:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.proper NOTE 0000000000000338 00000338
0000000000000020 0000000000000000 A 0 0 8
...
[11] .text PROGBITS 0000000000005000 00005000
0000000000012000 0000000000000000 AX 0 0 16
[12] .rodata PROGBITS 0000000000017000 00017000
0000000000004000 0000000000000000 A 0 0 32
...

Thirty-one sections in a binary that, from the user's point of view, just lists files. Most of them are linker artefacts.

Segments are the loader's view

A segment is a region the kernel (and ld.so) needs to map into memory before the program can run. Segments are listed in the program header table, located at offset e_phoff. The relevant types are:

  • PT_LOAD — bytes to copy into memory at a given virtual address with given permissions
  • PT_DYNAMIC — points at the dynamic table for the runtime linker
  • PT_INTERP — names the dynamic linker (/lib64/ld-linux-x86-64.so.2)
  • PT_GNU_RELRO — region the loader should mark read-only after relocations

The loader doesn't care about .text versus .init versus .fini. It cares about: "give me 0x13000 bytes mapped at virtual address 0x5000, read+execute, sourced from offset 0x5000 of the file." That's a PT_LOAD segment. Whether the section header table calls those bytes .text and .init is the linker's problem.

bash
$ readelf -l /bin/ls
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x6ab0
There are 13 program headers, starting at offset 64:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x000040 0x000040 0x0002d8 0x0002d8 R 0x8
INTERP 0x000318 0x000318 0x000318 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x000000 0x000000 0x002a98 0x002a98 R 0x1000
LOAD 0x003000 0x003000 0x003000 0x014191 0x014191 R E 0x1000
LOAD 0x017000 0x017000 0x017000 0x008514 0x008514 R 0x1000
LOAD 0x01ff20 0x020f20 0x020f20 0x001218 0x002488 RW 0x1000
...

Four PT_LOAD segments. One per permission category, basically: pure read, read+execute, more read (split for RELRO), and read+write. Thirty-one sections collapse into four loadable chunks.

The mapping

The bottom of readelf -l prints a section-to-segment mapping. Every loadable section ends up inside exactly one PT_LOAD segment, chosen by permission. Sections that exist purely for the linker (.symtab, .strtab, .rela.dyn, etc.) belong to no segment and are simply not loaded into memory at runtime.

Why both exist

The linker and the loader solve different problems. The linker assembles a binary from many inputs, needing to identify code, data, debugging info, and metadata down to the byte. The loader puts that finished binary into a process's address space, needing only to know which bytes go where with which permissions. Conflating the two would either bloat the program header table with linker minutiae or strip the section table out and leave the linker (and tools like objdump) blind.

You can in fact strip the section table entirely with strip --strip-section-headers, and the binary will still run — the loader doesn't need it. Try the same with the program header table and the loader has nothing to work from.

A useful test

If you ever forget which is which, ask: would the kernel, doing nothing but mapping pages, care about this? If yes, it's a segment. If only the linker or a debugger or objdump cares, it's a section. The two views overlap on the bytes they describe, but the questions they answer are unrelated.

Where this fits

The full structure of ELF — header, section header table, program header table, the dynamic table, relocations, symbols — lives in Module 103: Linking, Loading & Binary Formats, which builds the file format up from scratch and shows where every field gets used during linking and loading.