The ELF header, byte by byte
Every Linux executable, shared library and object file starts with an ELF header. On 64-bit systems it's exactly 64 bytes. It says what kind of file this is, which CPU it is for, where execution starts, and where the rest of the file's tables are. Let's decode a real one: /bin/ls from Ubuntu 24.04.
The bytes
xxd -l 64 /bin/ls00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 03 00 3e 00 01 00 00 00 30 6d 00 00 00 00 00 00 |..>.....0m......|
00000020 40 00 00 00 00 00 00 00 28 24 02 00 00 00 00 00 |@.......($......|
00000030 00 00 00 00 40 00 38 00 0d 00 40 00 1f 00 1e 00 |....@.8...@.....|Everything after the first 16 bytes is little-endian. If that's new, read the hexdump post first.
Bytes 0x00 to 0x0f: e_ident
The first 16 bytes identify the file and say how to read the rest.
7f 45 4c 46: the magic number,0x7ffollowed byELF. Thefilecommand checks exactly this.02: class.1= 32-bit,2= 64-bit.01: data encoding.1= little-endian,2= big-endian.01: ELF version, always 1.00: OS/ABI,0= System V (the usual value on Linux).- The rest are the ABI version and padding.
Bytes 0x10 to 0x3f: the fields
Each field below is read little-endian.
0x10e_type, 2 bytes:03 00= 3 =ET_DYN. That's a shared object, which is also what a position-independent executable (PIE) is. A non-PIE executable would be 2 (ET_EXEC), and an object file.ois 1 (ET_REL).0x12e_machine, 2 bytes:3e 00=0x3e= 62 = x86-64. You'd see0xb7for AArch64 and0x03for 32-bit x86.0x14e_version, 4 bytes:01 00 00 00= 1.0x18e_entry, 8 bytes:30 6d 00 …=0x6d30. The address where execution starts. In a PIE it's relative, because the loader picks the real base address at run time.0x20e_phoff, 8 bytes:0x40= 64. The program headers start right after this header.0x28e_shoff, 8 bytes:28 24 02 00 …=0x22428= 140328. The section headers are near the end of the file.0x30e_flags, 4 bytes: 0, unused on x86-64.0x34e_ehsize:40 00= 64, the size of this header.0x36e_phentsize:38 00= 56 bytes per program header, and0x38e_phnum:0d 00= 13 of them.0x3ae_shentsize:40 00= 64 bytes per section header, and0x3ce_shnum:1f 00= 31 of them.0x3ee_shstrndx:1e 00= 30. The section that holds the section names.
Check it
readelf -h /bin/lsreadelf agrees with every value: Type DYN, Machine X86-64, Entry point 0x6d30, program headers at 64 with 13 entries of 56 bytes, section headers at 140328 with 31 entries of 64 bytes, string table index 30.
Program headers vs. section headers
The header points to two tables, and it's worth knowing why there are two:
- Program headers (segments) are what the loader uses. They say which byte ranges of the file to map into memory, at what address, and with which permissions (read, write, execute).
- Section headers (
.text,.rodata,.data,.bss, …) are for linkers and tools. A program runs fine without them;stripremoves symbols, and some packers damage section headers on purpose to confuse analysis tools.
That second point is the practical lesson: when a tool chokes on a binary, check the header and the program headers by hand. They are what the kernel actually trusts.
Try it
readelf -l /bin/ls: list the segments and their permissionsreadelf -S /bin/ls: list the sectionsxxd -s 0x40 -l 56 /bin/ls: the first program header, raw