SCROLL TO PARSE →
A field guide for curious systems programmers

Anatomy of
an ELF file.

Every Linux binary — every .o, shared library, and executable you've ever run — is a plain sequence of bytes arranged to a spec. No magic, just a header pointing at some tables, pointing at your code. We're going to take one apart.

0x0000
0x0010
0x0020
The blueprint

One file, three ways to read it.

The same bytes look different depending on who's reading them. The compiler wrote it linearly. The loader reads it as a set of segments to map into memory. The linker reads it as a set of sections for symbols and relocations. This strip is the file's actual byte order — everything below refers back to it.

ELF Hdr
Program Hdrs
.text
.rodata
.data
.symtab / .strtab
Section Hdrs
ELF header — 64 bytes, fixed size Program headers — what to load Code & data — the payload Symbols & strings — names Section headers — the linker's index
Bytes 0x00 – 0x40

The ELF header, field by field.

Exactly 64 bytes on a 64-bit system, always at offset zero. It's the file's table of contents — everything else is found by following an offset written in here. Hover a row to see which bytes belong to it.

↳ e_phoff and e_shoff are the two pointers that make the rest of this page possible.
Pointed to by e_phoff

Program headers: instructions for the loader.

When the kernel execve()s a binary, it doesn't care about functions or variables — it cares about segments: contiguous chunks to map into virtual memory, each with an offset, a virtual address, and a permission set. A handful of PT_LOAD entries is usually all it takes.

On disk (the file)
R··PT_LOAD #1offset 0x0000 · size 0x0c40
R·XPT_LOAD #2 — .textoffset 0x1000 · size 0x2a10
RW·PT_LOAD #3 — .dataoffset 0x3e00 · size 0x0210
RW·PT_DYNAMICoffset 0x3c10 · size 0x01f0
In memory (the process)
Read-onlyvaddr 0x400000
Text (executable)vaddr 0x401000
Data (writable) vaddr 0x404000 · filesz 0x0210, memsz 0x0480
zero-filled — this is .bss, it isn't in the file at all
.dynamic (RW)vaddr 0x403c10
Pointed to by e_shoff

Section headers: the linker's index card.

Segments are for running the program; sections are for building and inspecting it. The linker uses them to place code, and tools like objdump and gdb use them to make sense of a binary long after it left the compiler.

NameTypeFlagsRelative size
.textSHT_PROGBITSALLOC · EXEC
.rodataSHT_PROGBITSALLOC
.dataSHT_PROGBITSALLOC · WRITE
.bssSHT_NOBITSALLOC · WRITE
.symtabSHT_SYMTAB
.strtabSHT_STRTAB
.rela.textSHT_RELA
.shstrtabSHT_STRTAB
From disk to a running process

Every layer of this page, in the order the kernel reads it.

Run ./a.out and this is roughly what execve() does before your first line of main() ever runs.

1

Check the magic bytes

Read the first 4 bytes. If they aren't 7F 45 4C 46, this isn't an ELF file — refuse immediately.

2

Read the ELF header

Pull e_entry, e_phoff, and e_phnum from the fixed 64-byte header.

3

Walk the program headers

Jump to e_phoff and read each Elf64_Phdr entry to find every PT_LOAD segment.

4

mmap() each segment

Map every segment at its p_vaddr with its permission bits; zero-fill the gap where memsz exceeds filesz — that's your .bss.

5

Set up the stack

Write argv, envp, and the auxiliary vector onto a fresh stack for the new process.

6

Jump to e_entry

Set the instruction pointer to the entry address from the header. The kernel's job is done — your code is now running.