On this page

ELF File Format

Coverage: ELF header → section header table → program header table → manual parsing → readelf/objdump usage Applicable to: x86-64 / ARM64 ELF, ET_EXEC / ET_DYN / ET_REL

Overview

ELF (Executable and Linkable Format) is the standard format for almost all binary files on Linux—executables, shared libraries (.so), object files (.o), and core dumps all use ELF. Understanding ELF is not just an academic exercise—when you need to troubleshoot "why this .so failed to load," "why LD_PRELOAD is not taking effect," or "why coredumps are unanalyzable after stripping," the details in the ELF header hold the answers.

Three Views of ELF

flowchart LR
    subgraph LINK["🔗 Linking View (Compile/Link Time)"]
        L_HDR["ELF Header"]
        L_SHDR["Section Header Table"]
        L_TEXT[".text (Code)"]
        L_RODATA[".rodata"]
        L_DATA[".data"]
        L_BSS[".bss"]
        L_SYMTAB[".symtab"]
        L_DYNSYM[".dynsym"]
        L_PLT[".plt"]
        L_GOT[".got"]
    end

    subgraph EXEC["▶️ Execution View (Runtime)"]
        E_HDR["ELF Header"]
        E_PHDR["Program Header Table"]
        E_LOAD_RX["PT_LOAD<br/>Readable + Executable"]
        E_LOAD_RW["PT_LOAD<br/>Readable + Writable"]
        E_DYNAMIC["PT_DYNAMIC<br/>Dynamic Linking Info"]
        E_INTERP["PT_INTERP<br/>ld.so Path"]
    end

    L_TEXT -.->|"Belongs to"| E_LOAD_RX
    L_RODATA -.->|"Belongs to"| E_LOAD_RX
    L_DATA -.->|"Belongs to"| E_LOAD_RW
    L_BSS -.->|"Belongs to"| E_LOAD_RW
    L_DYNSYM -.->|"Belongs to"| E_DYNAMIC

    classDef link fill:#e3f2fd,stroke:#1565c0
    classDef exec fill:#fff3e0,stroke:#ef6c00
    class LINK link
    class EXEC exec

Sections are for the linker (organized by function), while segments are for the kernel/dynamic linker (organized by permissions and loading requirements). The same .text section belongs to a PT_LOAD segment.

ELF Header

// /usr/include/elf.h
typedef struct {
    unsigned char e_ident[16];   // Magic + Class + Data + Version + ABI
    uint16_t      e_type;        // ET_EXEC, ET_DYN (PIE/.so), ET_REL (.o), ET_CORE
    uint16_t      e_machine;     // EM_X86_64, EM_AARCH64, EM_RISCV
    uint32_t      e_version;     // EV_CURRENT (1)
    uint64_t      e_entry;       // Entry point address (_start)
    uint64_t      e_phoff;       // Program Header Table offset
    uint64_t      e_shoff;       // Section Header Table offset
    uint32_t      e_flags;
    uint16_t      e_ehsize;      // ELF header size (64 bytes on x86_64)
    uint16_t      e_phentsize;   // Size of each program header
    uint16_t      e_phnum;       // Number of program headers
    uint16_t      e_shentsize;   // Size of each section header
    uint16_t      e_shnum;       // Number of section headers
    uint16_t      e_shstrndx;    // Index of the section name string table
} Elf64_Ehdr;

e_ident Magic Number

$ xxd /bin/ls | head -1
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............

# bytes 0-3: 0x7f 'E' 'L' 'F' — Magic number
# byte 4:   EI_CLASS — 1=32-bit, 2=64-bit
# byte 5:   EI_DATA  — 1=little-endian, 2=big-endian
# byte 6:   EI_VERSION — always 1
# byte 7:   EI_OSABI — 0=UNIX System V, 3=GNU/Linux
# bytes 8-15: padding

Key e_type Values

ET_EXEC (2): Traditional executable, loaded at a fixed address (non-PIE)
ET_DYN  (3): Shared library or PIE executable, loaded at a randomized base address (modern default)
ET_REL  (1): Relocatable object file (.o), input for the linker
ET_CORE (4): Core dump file

Modern Linux executables (default GCC -pie) are all ET_DYN. You can verify this with file /bin/ls.


Section Header Table

Each section describes an area in the file—code, data, symbol tables, relocation tables, etc.

typedef struct {
    uint32_t   sh_name;       // Offset of section name in .shstrtab
    uint32_t   sh_type;       // SHT_PROGBITS, SHT_SYMTAB, SHT_STRTAB, SHT_RELA, ...
    uint64_t   sh_flags;      // SHF_WRITE, SHF_ALLOC, SHF_EXECINSTR, ...
    uint64_t   sh_addr;       // Virtual address after loading into memory (in executables)
    uint64_t   sh_offset;     // Offset in the file
    uint64_t   sh_size;       // Section size
    uint32_t   sh_link;       // Index of associated section (e.g., symtab links to strtab)
    uint32_t   sh_info;       // Additional information
    uint64_t   sh_addralign;  // Alignment
    uint64_t   sh_entsize;    // Size of each entry if it is a table
} Elf64_Shdr;

Key Sections

SectionTypeContent
.textSHT_PROGBITSExecutable code
.rodataSHT_PROGBITSRead-only data (string constants, etc.)
.dataSHT_PROGBITSInitialized global/static variables
.bssSHT_NOBITSUninitialized global variables (takes space but not file size)
.pltSHT_PROGBITSProcedure Linkage Table (lazy binding)
.got / .got.pltSHT_PROGBITSGlobal Offset Table
.dynsymSHT_DYNSYMDynamic symbol table (needed at runtime)
.symtabSHT_SYMTABFull symbol table (can be removed by strip)
.dynstr / .strtabSHT_STRTABString table
.rela.dyn / .rela.pltSHT_RELARelocation table
.interpSHT_PROGBITSDynamic linker path (e.g., /lib64/ld-linux-x86-64.so.2)
.dynamicSHT_DYNAMICDynamic linking information (DT_NEEDED, DT_SONAME, ...)
.init / .finiSHT_PROGBITSInitialization/termination code (GCC -finit-array)

Program Header Table

Program headers tell the kernel "how to load this file." Each segment (PT_LOAD) specifies a block of data to be mapped to a virtual address with specific permissions.

typedef struct {
    uint32_t   p_type;    // PT_LOAD, PT_DYNAMIC, PT_INTERP, PT_NOTE, PT_GNU_STACK, ...
    uint32_t   p_flags;   // PF_R(4) | PF_W(2) | PF_X(1)
    uint64_t   p_offset;  // Segment offset in the file
    uint64_t   p_vaddr;   // Virtual address (ASLR adds a random base)
    uint64_t   p_paddr;   // Physical address (irrelevant, only for systems without MMU)
    uint64_t   p_filesz;  // Size in the file
    uint64_t   p_memsz;   // Size in memory (memsz > filesz → remaining bytes zeroed, for .bss)
    uint64_t   p_align;   // Alignment
} Elf64_Phdr;

PT_LOAD Mapping Process

// Kernel fs/binfmt_elf.c: elf_map()
// For each PT_LOAD segment:
//   1. mmap(p_vaddr, p_memsz, PROT from p_flags, MAP_PRIVATE, fd, p_offset)
//   2. If p_memsz > p_filesz → remaining bytes are mapped as 0 (anonymous MAP_ANONYMOUS)
//      This is the runtime implementation of .bss

// Example: /bin/ls has 2 PT_LOAD segments:
//   Type  Offset             VirtAddr          PhysAddr         FileSiz  MemSiz   Flg Align
//   LOAD  0x0000000000000000 0x0000000000400000 0x0000000000400000 0x002e58 0x002e58 R E 0x1000
//   LOAD  0x0000000000003000 0x0000000000403000 0x0000000000403000 0x000fb8 0x001640 RW  0x1000
//
//   First LOAD: Code + read-only data → R+X (memsz == filesz, no .bss)
//   Second LOAD: Data + .bss         → R+W (memsz > filesz, remaining bytes padded for .bss)

Manual Parsing

# ELF Header
readelf -h /bin/ls

# Section headers
readelf -S /bin/ls

# Program headers (segments)
readelf -l /bin/ls

# Dynamic symbol table
readelf --dyn-syms /bin/ls

# Relocation table
readelf -r /bin/ls

# .dynamic section (DT_NEEDED, DT_RPATH, ...)
readelf -d /bin/ls

# Full string search
strings /bin/ls | head -20

# Disassembly
objdump -d /bin/ls | head

# Check if PIE
file /bin/ls
# "ELF 64-bit LSB pie executable" → PIE
# "ELF 64-bit LSB executable" → non-PIE

References

  • Specification: ELF spec (System V ABI, Chapters 4-5), man 5 elf
  • Header files: /usr/include/elf.h, /usr/include/link.h
  • Tools: readelf, objdump, nm, size, strip, patchelf
  • LWN: "The ELF object file format" series

Keywords: ELF header, section, segment, PT_LOAD, SHT_SYMTAB, ET_DYN, ET_EXEC, PIE, readelf, objdump