Skip to content

ELF

Level: 201 · for anyone with a hex editor open

One line: Byte 4 of an ELF file says how wide its offsets are and byte 5 says which end comes first, so the header is the one format in this chapter that tells its reader how to read the rest of it — a struct format string can be built from those two bytes, and the same seven fields are 52 bytes in one file and 64 in another.

What the loader reads

The Executable and Linking Format is the object format of Linux, the BSDs, Solaris and most embedded toolchains, defined in the System V ABI ↗. Every file opens with sixteen bytes called e_ident, and the first multi-byte number is not until byte 16. That ordering is the design: everything a reader needs in order to read a number is in the single bytes before the first number.

offset field width what it says
0 EI_MAG0..3 4 7f 45 4c 46 — a byte no text file begins with, then the letters ELF
4 EI_CLASS 1 1 = 32-bit, 2 = 64-bit: the width of every address and offset field
5 EI_DATA 1 1 = little-endian, 2 = big-endian: the byte order of every field after this one
6 EI_VERSION 1 1
7 EI_OSABI 1 0 for System V, 3 for Linux, and so on
8 EI_ABIVERSION 1 usually 0
9 pad 7 zero
16 e_type 2 1 REL, 2 EXEC, 3 DYN, 4 CORE
18 e_machine 2 3 i386, 0x28 ARM, 0x3E x86-64, 0xB7 AArch64, 0xF3 RISC-V
20 e_version 4 1
24 e_entry 4 or 8 where execution starts
e_phoff, e_shoff 4 or 8 each file offsets of the program header table and the section header table
e_flags 4 processor-specific
e_ehsize 2 this header's size: 52 or 64
e_phentsize, e_phnum 2 each size and count of program headers: 32 × n or 56 × n
e_shentsize, e_shnum 2 each size and count of section headers: 40 × n or 64 × n
e_shstrndx 2 which section holds the section names

Three fields grow when EI_CLASS is 2, and that is the whole of the 52-to-64 difference. Ghidra's ElfHeader reads them in exactly that way — if (is32Bit()) readNextUnsignedInt() else readNextLong() three times, after reading the class byte — and the Python program below does the same with a format string.

In Python

Verified output of elf_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. THE FIRST SIXTEEN BYTES SAY HOW TO READ THE REST
------------------------------------------------------------------------
   64-bit, little-endian  7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
   32-bit, big-endian     7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00

   byte 0..3   7f 45 4c 46   a byte no text file starts with, then 'ELF'
   byte 4      EI_CLASS      1 = 32-bit offsets, 2 = 64-bit offsets
   byte 5      EI_DATA       1 = little-endian, 2 = big-endian
   byte 6      EI_VERSION    1, the only value there has ever been
   byte 7      EI_OSABI      0 = System V; 9..15 the pad

2. THE SAME SEVEN FIELDS, 52 OR 64 BYTES
------------------------------------------------------------------------
   64-bit LSB   64 bytes   format string '<HHIQQQIHHHHHH'
      0010  02 00 3e 00 01 00 00 00 00 10 40 00 00 00 00 00
      0020  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
      0030  00 00 00 00 40 00 38 00 00 00 40 00 00 00 00 00
   32-bit MSB   52 bytes   format string '>HHIIIIIHHHHHH'
      0010  00 02 00 3e 00 00 00 01 00 40 10 00 00 00 00 00
      0020  00 00 00 00 00 00 00 00 00 34 00 20 00 00 00 28
      0030  00 00 00 00

   e_entry, e_phoff and e_shoff are the three fields that grew from
   4 bytes to 8; everything else is the same width in both. 52 + 12
   is 64, and e_ehsize in each header says which it is.

3. READ EACH ONE WITH ITS OWN IDENT
------------------------------------------------------------------------
   field          64-bit LSB   32-bit MSB
   e_type                  2            2
   e_machine            0x3e         0x3e
   e_version               1            1
   e_entry          0x401000     0x401000
   e_phoff                 0            0
   e_shoff                 0            0
   e_flags                 0            0
   e_ehsize               64           52
   e_phentsize            56           32
   e_phnum                 0            0
   e_shentsize            64           40
   e_shnum                 0            0
   e_shstrndx              0            0

   e_type 2 is EXEC, e_machine 0x3e is X86_64,
   in both -- two layouts, one header, because the file said which
   layout it used before its first multi-byte field.

4. READ THE BIG-ENDIAN ONE AS IF IT WERE LITTLE
------------------------------------------------------------------------
   e_type      512      (0x0200, not in the table: 1..4)
   e_machine   15872    (0x3e00, not x86-64)
   e_entry     0x00104000

   Nothing raised. Every wrong reading of a header is a number, which
   is why byte 5 exists: the alternative is guessing from plausibility.

5. A SECTION'S NAME IS AN OFFSET INTO A STRING TABLE
------------------------------------------------------------------------
   .shstrtab bytes   00 2e 74 65 78 74 00 2e 73 68 73 74 72 74 61 62 00
   as text           b'\x00.text\x00.shstrtab\x00'

   sh_name = 1   -> read to the next NUL -> '.text'      ok
   sh_name = 7   -> read to the next NUL -> '.shstrtab'  ok

   A section header holds no name, only sh_name, an offset into the
   section that e_shstrndx points at. Names are NUL-terminated there,
   and offset 0 is an empty string on purpose: the unnamed section.

The file says which end, and almost nothing else here does

The bytes do not say which end is the chapter-1 lesson: four bytes on their own are two numbers, and the file does not record which was meant. ELF is the counter-example. Byte 5 is that record, and section 3 above is what it buys — the same header laid out two ways, read back to the same thirteen numbers with no guessing. Section 4 is what the absence of byte 5 would cost: read the big-endian header with the little-endian rule and e_type is 512, a perfectly good number that means nothing.

Of the twenty-four formats in this chapter, only DEX has a field like it. Mach-O lets you infer the order from how its magic reads; PE, COFF and the rest fixed it once in a document. Raw binary makes you type it in.

A section's name is not in its header

A section header is 40 or 64 bytes and holds no name — sh_name is an offset into a string table, and e_shstrndx in the file header says which section that table is. Names there are NUL-terminated, offset 0 is the empty string, and section 5 above reads two names out of a seventeen-byte table. This is the arrangement PE and COFF also use for long names (COFF, where the offset is written in ASCII decimal), and the opposite of NE, OMF and PEF, which put a length byte in front of every string instead.

Measured on real files

Measured 2026-09-13 — /bin/ls in ubuntu:24.04 under Docker, od -A x -t x1 -N 64; the header read off those bytes by the rules above. Not machine-checked: the bytes are a property of that package
000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
000010 03 00 3e 00 01 00 00 00 30 6d 00 00 00 00 00 00
000020 40 00 00 00 00 00 00 00 28 24 02 00 00 00 00 00
000030 00 00 00 00 40 00 38 00 0d 00 40 00 1f 00 1e 00

byte 4 = 02  64-bit      byte 5 = 01  little-endian
e_type     03 00        3   DYN, a position-independent executable
e_machine  3e 00        0x3e  x86-64
e_entry    30 6d 00 00 00 00 00 00   0x6d30
e_phoff    0x40   e_shoff  0x22428   e_ehsize 0x40 = 64
e_phentsize 0x38 = 56   e_phnum 13   e_shentsize 0x40 = 64   e_shnum 31   e_shstrndx 30
Measured 2026-09-13 — readelf -h /bin/ls in the gcc:14 image (Debian's ls, a different binary from the one above), abridged
Class:                             ELF64
Data:                              2's complement, little endian
Type:                              DYN (Position-Independent Executable file)
Machine:                           Advanced Micro Devices X86-64
Entry point address:               0x6760
Start of section headers:          156712 (bytes into file)
Size of this header:               64 (bytes)
Size of section headers:           64 (bytes)
Number of section headers:         30
Section header string table index: 29

Two builds of ls, both 64-bit little-endian, and the fields that differ — entry point, table offsets, section count — are the ones that depend on what was linked. Everything readelf prints in words is a number in the dump above it, and the first two lines are bytes 4 and 5.

What Ghidra checks

ElfLoader constructs an ElfHeader, which refuses a file whose first four bytes are not 7f 45 4c 46, then asks its opinion service for a language matching e_machine and e_flags, and drops any candidate wider than 32 bits when byte 4 says the file is 32-bit. The name it shows is Executable and Linking Format (ELF).

If you are coming from Python or ABAP

Python. struct is the right tool and the ident is its argument: '<' if data[5] == 1 else '>' gives the first character, and the class byte picks I or Q for the three wide fields — which is all elf_py.py does. Read the sixteen bytes first, build the format, then unpack; a format string written down before the file is open is a guess about the file. int.from_bytes(b, 'little') does one field at a time and takes the same decision as an argument.

ABAP. (Not machine-checked — CI cannot run ABAP.) An xstring sliced with +off(len) reaches any field, and an assignment of a 4-byte slice to an i reads it as big-endian (Bytes, hex and int has the rule and its link). So the little-endian header above needs its bytes reversed before the assignment, and it is byte 5 that tells you whether to reverse — the same decision, taken by hand. There is no struct; write the field widths down, and take them from the class byte, because a 64-bit e_entry will not fit an i at all.

Try it

  1. head -c 64 /bin/ls | xxd on a Linux machine. Say what bytes 4 and 5 are before you read anything else, then find e_shstrndx in the last two bytes.
  2. readelf -h on the same file and match every line to a field in the dump. The two lines that are words rather than numbers came from one byte each.
  3. Find a 32-bit binary — an old Raspberry Pi image, a .ko for an embedded board — and compare e_ehsize. It is 52.
  4. readelf -S lists section names. Then readelf -x .shstrtab dumps the table they are read from; count the NULs.
  5. Take any ELF and flip byte 5 with a hex editor. readelf -h on the copy: every number is now wrong and none is reported as an error.

Practice

Four idents. Here are the first six bytes of four ELF files:

7f 45 4c 46 02 01
7f 45 4c 46 01 02
7f 45 4c 46 02 02
7f 45 4c 46 01 01

For each, say the width, the byte order, the header size in bytes, and the struct format string for the thirteen fields after e_ident. Then the question with a trap in it: e_ehsize is in the header — why can a reader not simply read that field to learn the header size?

Answers

Verified output of elf_kata_py.py — regenerated by tools/run_examples.py, never hand-typed.

   ident (first 6)      class   data     header   struct format

   7f 45 4c 46 02 01    64-bit  little    64 bytes  '<HHIQQQIHHHHHH'     Linux x86-64, the usual
   7f 45 4c 46 01 02    32-bit  big       52 bytes  '>HHIIIIIHHHHHH'     a 32-bit big-endian target, PowerPC or MIPS
   7f 45 4c 46 02 02    64-bit  big       64 bytes  '>HHIQQQIHHHHHH'     64-bit big-endian: s390x, or SPARC64
   7f 45 4c 46 01 01    32-bit  little    52 bytes  '<HHIIIIIHHHHHH'     32-bit little-endian: i386, ARM

   The answer is two bytes long. Byte 4 picks the widths and so the
   header size, 52 or 64; byte 5 picks the byte order and so the first
   character of the format string. Nothing after byte 5 is readable
   until both have been read, and nothing before it is multi-byte.

   THE TRAP: e_ehsize is in the header too, as a 16-bit number -- so a
   reader that wants the header size from e_ehsize has to know the byte
   order to read it, and it is the ident that says. 0x0040 read the
   wrong way round is 16384, which is not a header size anyone has.

See also

  • The bytes do not say which end — the question byte 5 answers, asked of a file that has no byte 5
  • Mach-O — the other Unix executable, which answers the same question by how its magic reads
  • DEX — the only other format here with a byte-order field, and it is a 32-bit constant rather than a byte
  • PE — the header that took its section-name-as-offset idea from COFF, one lineage over
  • Packing a record — width, byte order and padding for a record you design; a header is one you were handed
  • The first two bytes — the kernel's binfmt_elf wants exactly these four bytes at offset 0, and decodes nothing
  • System V ABI, chapter 4: ELF header ↗ — the table the one above was checked against