Chapter 2. The ELF Format
The Executable and Linkable Format is the default binary format on Linux, used for executables, object files, shared libraries, and core dumps. Discussion centers on 64-bit executables; 32-bit differs mainly in the size and order of certain header fields. Type definitions live in /usr/include/elf.h.
An ELF binary has four component types:
| Component | Presence | Position |
|---|---|---|
| Executable header | Mandatory | Always at file offset 0 |
| Program headers | Optional | Located via e_phoff |
| Sections | — | Located via their headers |
| Section headers | Optional (one per section) | Located via e_shoff |
2.1 The Executable Header
typedef struct {
unsigned char e_ident[16]; /* Magic number and other info */
uint16_t e_type; /* Object file type */
uint16_t e_machine; /* Architecture */
uint32_t e_version; /* Object file version */
uint64_t e_entry; /* Entry point virtual address */
uint64_t e_phoff; /* Program header table file offset */
uint64_t e_shoff; /* Section header table file offset */
uint32_t e_flags; /* Processor-specific flags */
uint16_t e_ehsize; /* ELF header size in bytes */
uint16_t e_phentsize; /* Program header table entry size */
uint16_t e_phnum; /* Program header table entry count */
uint16_t e_shentsize; /* Section header table entry size */
uint16_t e_shnum; /* Section header table entry count */
uint16_t e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
Inspect with readelf -h.
e_ident
16-byte array. Starts with a 4-byte magic value: 0x7f followed by ASCII E, L, F — lets tools identify ELF files immediately. Then:
| Byte | Values | Meaning |
|---|---|---|
EI_CLASS |
ELFCLASS32 (1), ELFCLASS64 (2) |
32-bit or 64-bit architecture |
EI_DATA |
ELFDATA2LSB (1), ELFDATA2MSB (2) |
Little- or big-endian |
EI_VERSION |
EV_CURRENT (1) — only valid value |
ELF specification version |
EI_OSABI |
0 = UNIX System V ABI (default) | Nonzero signals ABI/OS-specific extensions |
EI_ABIVERSION |
Usually 0 | Version of the ABI in EI_OSABI |
EI_PAD |
Zeroed | Padding, bytes 9–15, reserved |
Remaining fields
| Field | Notes |
|---|---|
e_type |
ET_REL (relocatable object), ET_EXEC (executable), ET_DYN (shared object) |
e_machine |
EM_X86_64, EM_386, EM_ARM, ... |
e_version |
Same role as EI_VERSION; only value is 1 (EV_CURRENT) |
e_entry |
Entry point virtual address; interpreter transfers control here after loading. Useful start for recursive disassembly |
e_phoff, e_shoff |
File offsets (not virtual addresses) of the program and section header tables; either may be 0 if the table is absent |
e_flags |
Architecture-specific flags (used e.g. by embedded ARM); 0 for x86 |
e_ehsize |
Executable header size: 64 bytes (64-bit x86), 52 bytes (32-bit x86) |
e_phentsize, e_phnum |
Size and count of program header entries |
e_shentsize, e_shnum |
Size and count of section header entries |
e_shstrndx |
Section header table index of .shstrtab, the string table of section names |
2.2 Section Headers
Code and data are logically divided into contiguous, nonoverlapping sections. Sections have no predetermined structure; each is described by a section header in the section header table.
The section view exists for the linker (link time). It is optional: files that don't need linking may omit the table (e_shoff = 0). Execution uses a different organization — segments (Section 2.4).
typedef struct {
uint32_t sh_name; /* Section name (string tbl index) */
uint32_t sh_type; /* Section type */
uint64_t sh_flags; /* Section flags */
uint64_t sh_addr; /* Section virtual addr at execution */
uint64_t sh_offset; /* Section file offset */
uint64_t sh_size; /* Section size in bytes */
uint32_t sh_link; /* Link to another section */
uint32_t sh_info; /* Additional section information */
uint64_t sh_addralign; /* Section alignment */
uint64_t sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
| Field | Notes |
|---|---|
sh_name |
Index into .shstrtab; 0 = unnamed |
sh_type |
See table below |
sh_flags |
SHF_WRITE (writable at runtime), SHF_ALLOC (loaded into memory at execution — actual loading uses the segment view), SHF_EXECINSTR (contains executable instructions) |
sh_addr |
Virtual address; 0 if not loaded at runtime. Present because the linker needs runtime addresses for relocations |
sh_offset, sh_size |
File offset (bytes from file start) and size |
sh_link |
Section header table index of a related section — e.g. the string table for a symbol table, or the symbol table for a relocation section |
sh_info |
Type-dependent; for relocation sections, index of the section the relocations apply to |
sh_addralign |
Required alignment of the base address; 0 and 1 mean no requirement |
sh_entsize |
Entry size for table sections (symbol tables, relocation tables); 0 if unused |
sh_type |
Contents |
|---|---|
SHT_PROGBITS |
Program data (machine instructions, constants); no structure for the linker to parse |
SHT_SYMTAB / SHT_DYNSYM |
Static / dynamic symbol table (Elf64_Sym entries) |
SHT_STRTAB |
Array of NULL-terminated strings; first byte NULL by convention |
SHT_REL / SHT_RELA |
Relocation entries (Elf64_Rel / Elf64_Rela) for static linking: each names a location needing relocation and the symbol to resolve it to |
SHT_DYNAMIC |
Dynamic linking information (Elf64_Dyn entries) |
SHT_NOBITS |
Occupies no file bytes (.bss) |
SHT_NULL |
First entry of every section header table; fully zeroed, no associated section |
Malware
sh_name contents are not trustworthy when analyzing malware — section names may be intentionally misleading.
2.3 Sections
List with readelf --sections --wide.
.init and .fini
Executable code run before control transfers to the entry point (.init — think constructor) and after the main program completes (.fini — destructor).
.text
Main code section: SHT_PROGBITS, flags AX — executable, not writable. Executable sections should almost never be writable: it would let an attacker exploiting a vulnerability overwrite code directly.
Besides user code, gcc places standard functions here: _start, register_tm_clones, frame_dummy, etc. The binary's entry point is _start, not main: _start moves main's address into rdi (first x64 parameter register) and calls __libc_start_main, which in turn calls main.
.rodata, .data, and .bss
| Section | Contents | Type | Writable |
|---|---|---|---|
.rodata |
Constant values | SHT_PROGBITS |
No |
.data |
Default values of initialized variables | SHT_PROGBITS |
Yes |
.bss |
Space reserved for uninitialized (zero-initialized) variables | SHT_NOBITS |
Yes |
.bss ("block started by symbol") occupies no bytes on disk — it is a directive to allocate a zeroed block at load time. Compilers occasionally emit constant data in code sections (Visual Studio does; modern gcc/clang generally don't), which complicates disassembly.
Lazy Binding: .plt, .got, and .got.plt
Relocations for shared-library functions are typically deferred until a reference is first invoked — lazy binding. The dynamic linker thus performs only the relocations actually needed at runtime. Default on Linux; export LD_BIND_NOW=1 forces immediate resolution (e.g. for real-time guarantees).
Implemented via the Procedure Linkage Table (.plt, executable code) and the Global Offset Table (.got.plt, data). The PLT consists of stubs of fixed format: one default stub, then one per library function, each pushing an incremented identifier.
Resolution of a first call to puts:
.textcallsputs@plt.- Stub begins with an indirect
jmpthrough its.got.pltslot. - Before binding, the slot points back to the next instruction of the stub itself, so the jump lands on the following
push. - Stub pushes its identifier (0x0 for the first stub) and jumps to the default stub, which pushes an identifier for the executable itself (from the GOT) and jumps, indirectly through the GOT, to the dynamic linker.
- Using both identifiers, the dynamic linker resolves
putson behalf of the right module (multiple loaded libraries each have their own PLT and GOT), patches the resolved address into the.got.pltslot, and transfers control toputs. - Subsequent calls: the stub's first
jmpgoes straight toputs; the dynamic linker is no longer involved.
Why a GOT instead of patching the PLT code directly?
- Security: code sections stay non-writable. The GOT is data, so it may be writable; corrupting GOT addresses is a far weaker attack primitive than injecting code.
- Shareability: one physical copy of a library is mapped at different virtual addresses in different processes. Addresses can't be patched into shared code, but each process has a private GOT.
Data references to relocatable symbols also go through the GOT — directly, without a PLT step. Hence the split: .got for data references, .got.plt for resolved function addresses used by the PLT.
RELRO
.got.plt is runtime-writable; .got is not when RELRO (relocations read-only, ld -z relro) is enabled. RELRO keeps entries that must stay writable for lazy binding in .got.plt and all others in read-only .got.
.plt.got
An alternative PLT that uses read-only .got entries instead of .got.plt. Emitted with ld -z now ("now binding" — same effect as LD_BIND_NOW=1, but known at link time), allowing GOT entries in .got for security and 8-byte stubs instead of 16-byte .plt stubs.
.rel.* and .rela.*
Type SHT_RELA: tables of relocation entries, each giving an address to patch and how to compute the value. In a linked executable only dynamic relocations remain; static ones were resolved at link time. Common types (readelf --relocs):
| Type | Offset located in | Purpose |
|---|---|---|
R_X86_64_GLOB_DAT |
.got |
Compute a data symbol's address and plug it into .got |
R_X86_64_JUMP_SLOT |
.got.plt |
Jump slots: slots where library function addresses get plugged in; each is the indirect jump target of a PLT stub |
(readelf truncates the latter to R_X86_64_JUMP_SLO in its output.)
.dynamic
A road map for the OS and dynamic linker when loading: a table of Elf64_Dyn structures (tags), each a type plus value.
| Tag | Meaning |
|---|---|
DT_NEEDED |
A dependency, e.g. libc.so.6 — must be loaded to run the binary |
DT_VERNEED, DT_VERNEEDNUM |
Start address and entry count of the version dependency table |
DT_STRTAB, DT_SYMTAB, DT_PLTGOT, DT_RELA |
Pointers to the dynamic string table, dynamic symbol table, .got.plt, and dynamic relocation section |
.init_array and .fini_array
Arrays of function pointers: constructors called in turn before main, destructors after. Unlike .init's single startup function, these are data sections holding arbitrarily many pointers — mark C functions with __attribute__((constructor)) to register them. Easy to modify, hence a convenient hook point for inserting initialization/finalization code. Older gcc emits .ctors/.dtors instead.
String and symbol tables
| Section | Type | Contents | Strippable |
|---|---|---|---|
.shstrtab |
SHT_STRTAB |
Names of all sections; indexed by sh_name |
No |
.symtab |
SHT_SYMTAB |
Static symbol table (Elf64_Sym) |
Yes |
.strtab |
SHT_STRTAB |
Strings for .symtab symbol names |
Yes |
.dynsym |
SHT_DYNSYM |
Symbols needed for dynamic linking | No |
.dynstr |
SHT_STRTAB |
Strings for .dynsym |
No |
The distinct types SHT_SYMTAB vs. SHT_DYNSYM let strip recognize which tables are safe to remove.
2.4 Program Headers
The program header table provides the segment view: used by the OS and dynamic linker at execution time, versus the section view used at link time. A segment encompasses zero or more sections, bundled into one chunk; segments exist only in executable ELF files. readelf --wide --segments shows the table plus the section-to-segment mapping.
typedef struct {
uint32_t p_type; /* Segment type */
uint32_t p_flags; /* Segment flags */
uint64_t p_offset; /* Segment file offset */
uint64_t p_vaddr; /* Segment virtual address */
uint64_t p_paddr; /* Segment physical address */
uint64_t p_filesz; /* Segment size in file */
uint64_t p_memsz; /* Segment size in memory */
uint64_t p_align; /* Segment alignment */
} Elf64_Phdr;
| Field | Notes |
|---|---|
p_type |
PT_LOAD: loaded into memory at process setup — usually at least two, one non-writable (code) and one writable (data). PT_INTERP: contains .interp (interpreter path). PT_DYNAMIC: contains .dynamic. PT_PHDR: the program header table itself |
p_flags |
Runtime permissions: PF_X (executable; readelf shows E), PF_W (writable — data segments only, never code), PF_R (readable) |
p_offset, p_vaddr, p_filesz |
Analogous to sh_offset, sh_addr, sh_size. For loadable segments, p_vaddr ≡ p_offset (mod page size) — page size typically 4,096 bytes |
p_paddr |
Physical load address on some systems; unused (zero) on modern OSes, which execute everything in virtual memory |
p_memsz |
May exceed p_filesz (e.g. .bss: zeros aren't stored on disk); the loader appends the extra bytes and zero-initializes them |
p_align |
0 or 1 = no requirement; otherwise a power of 2, with p_vaddr ≡ p_offset (mod p_align) |