An executable is not just machine code. It's a file format: a contract between the linker that writes it and the operating system that loads it. The file tells the OS which CPU it's for, which bytes to put where in memory and with which permissions, which libraries it needs, and where the first instruction is. The assembler, linker and loader chapter followed a program through that toolchain; this one opens the file itself.
Three formats cover nearly every computer today:
| Format | Used by | Magic bytes |
|---|---|---|
| ELF (Executable and Linkable Format) | Linux, the BSDs, Android, most embedded systems | 7f 45 4c 46 — \x7fELF |
| PE (Portable Executable) | Windows, UEFI firmware | 4d 5a — MZ |
| Mach-O | macOS, iOS | cf fa ed fe (64-bit) |
The file command identifies them by these magic bytes. All three share the same anatomy: a header, a description of how to map the file into memory, the code and data, and extra tables for symbols, relocations and dynamic linking.
ELF, byte by byte
Here is a tiny x86-64 Linux program — it writes hello and exits — linked into a static executable of 1,288 bytes. It really runs: hello, exit code 0. These are its first 64 bytes, the ELF header:
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0200 3e00 0100 0000 6011 2000 0000 0000 ..>.....`. .....
00000020: 4000 0000 0000 0000 c802 0000 0000 0000 @...............
00000030: 0000 0000 4000 3800 0500 4000 0900 0700 ....@.8...@.....
| Bytes | Field | Value |
|---|---|---|
7f 45 4c 46 | magic | \x7fELF |
02 | class | 64-bit |
01 | data | little-endian |
02 00 | type | ET_EXEC, a fixed-address executable |
3e 00 | machine | 0x3e, x86-64 |
60 11 20 00 … | entry point | 0x201160, the address of _start |
40 00 … | program header offset | right after the header |
05 00 | program header count | 5 |
09 00 | section header count | 9 |
Every multi-byte field is little-endian. The entry point is where the kernel sends the CPU once the program is loaded.
Segments: how to load it
The program headers describe segments: ranges of the file to map into memory, with permissions. They're what the kernel reads:
Type Offset VirtAddr FileSiz MemSiz Flags
PHDR 0x000040 0x200040 0x000118 0x000118 r--
LOAD 0x000000 0x200000 0x00015e 0x00015e r-- headers + .rodata
LOAD 0x000160 0x201160 0x000021 0x000021 r-x .text
LOAD 0x000181 0x202181 0x000004 0x001004 rw- .data + .bss
STACK 0 0 0 0 rw-
- The code segment is readable and executable, but not writable. The data segment is writable, but not executable. This separation, called W^X (write xor execute), means injected data can't simply be run as code.
- The last
LOADhas a file size of 4 bytes (.data) but a memory size of0x1004. The extra 4 KB is.bss: the kernel provides it as zeroed memory, and it takes no space in the file. STACKwith flagsrw-asks for a non-executable stack.
Sections: the linker's view
Sections — .text, .rodata, .data, .bss, .symtab… — are the finer-grained view used by linkers, debuggers and disassemblers. Several sections are grouped into each segment by permission. The kernel doesn't need the section table at all: strip can remove symbols and the program still runs. That's why stripped binaries are harder to reverse — the names are gone — but load exactly the same.
Dynamic ELF: PIE, interpreter, libraries
Most Linux programs aren't static. Here is a C program compiled with the default gcc settings (on an ARM64 Linux machine; the structure is identical on x86-64). Its header says Type: DYN (Position-Independent Executable file), and its program headers include more entries:
Type Offset VirtAddr FileSiz MemSiz Flg
PHDR 0x000040 0x000040 0x0001f8 0x0001f8 R
INTERP 0x000238 0x000238 0x00001b 0x00001b R
[Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
LOAD 0x000000 0x000000 0x00088c 0x00088c R E
LOAD 0x00fdc8 0x01fdc8 0x000274 0x000278 RW
DYNAMIC 0x00fdd8 0x01fdd8 0x0001e0 0x0001e0 RW
GNU_STACK 0 0 0 0 RW
GNU_RELRO 0x00fdc8 0x01fdc8 0x000238 0x000238 R
- PIE: the addresses start at 0. The whole program is loaded at a random base chosen at run time (ASLR), so its entry point,
0x640, is only an offset. INTERPnames the dynamic linker to run first. On x86-64 Linux it's/lib64/ld-linux-x86-64.so.2.DYNAMICpoints to the dynamic section, whose first entry here isNEEDED libc.so.6: the libraries to load. It also holds the relocation tables andFLAGS_1: PIE.GNU_RELROmarks data that must become read-only once the dynamic linker has applied relocations, such as the GOT with full RELRO.
To see the result in memory, read /proc/<pid>/maps. For a running cat, it shows each segment of the program, libc and the dynamic linker at their random addresses, with the permissions the program headers asked for:
aaaae4b10000-aaaae4b19000 r-xp /usr/bin/cat
aaaae4b2f000-aaaae4b30000 r--p /usr/bin/cat
aaaae4b30000-aaaae4b31000 rw-p /usr/bin/cat
aaaaf18be000-aaaaf18df000 rw-p [heap]
ffffbbb90000-ffffbbd1b000 r-xp …/libc.so.6
ffffbbd47000-ffffbbd6e000 r-xp …/ld-linux-aarch64.so.1
ffffbbd83000-ffffbbd85000 r-xp [vdso]
fffff8b3a000-fffff8b5b000 rw-p [stack]
The [vdso] is a small library the kernel maps into every process, so that calls like gettimeofday don't need a real system call.
PE: the Windows format
A PE file starts with a 1980s relic. Here are the first 96 bytes of a minimal 64-bit Windows executable:
00000000: 4d5a 7800 0100 0000 0400 0000 0000 0000 MZx.............
00000010: 0000 0000 0000 0000 4000 0000 0000 0000 ........@.......
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 7800 0000 ............x...
00000040: 0e1f ba0e 00b4 09cd 21b8 014c cd21 5468 ........!..L.!Th
00000050: 6973 2070 726f 6772 616d 2063 616e 6e6f is program canno
MZ: every PE file begins with an MS-DOS header, and a tiny DOS program, the stub, that prints This program cannot be run in DOS mode.- At offset
0x3C,e_lfanew(here0x78) points to the real header: the signaturePE\0\0, then the COFF header (machine0x8664= x86-64, number of sections…) and the optional header, which isn't optional at all.
Some fields from this file's optional header:
| Field | Value | Meaning |
|---|---|---|
| Magic | 0x20B | PE32+, a 64-bit image |
| ImageBase | 0x140000000 | preferred load address (the default for 64-bit programs) |
| AddressOfEntryPoint | 0x1000 | entry point, as an RVA |
| SectionAlignment / FileAlignment | 0x1000 / 0x200 | sections are 4 KB apart in memory, 512 B apart in the file |
| Subsystem | 3 | a console program |
| DllCharacteristics | 0x8160 | ASLR (DYNAMIC_BASE), 64-bit ASLR (HIGH_ENTROPY_VA), NX_COMPAT, terminal-server aware |
PE addresses are mostly RVAs — relative virtual addresses, offsets from wherever the image is loaded. The optional header ends with data directories: pointers to the import table (the DLLs and functions to resolve into the IAT), the export table (for DLLs), the base relocations (.reloc, needed when ASLR moves the image), resources, TLS and debug information.
Mach-O: the Apple format
A Mach-O file is a header followed by a list of load commands. A small C program built on a Mac:
magic cputype filetype ncmds flags
MH_MAGIC_64 ARM64 EXECUTE 17 NOUNDEFS DYLDLINK TWOLEVEL PIE
LC_SEGMENT_64commands define the segments:__PAGEZERO,__TEXT(code and constants),__DATAand__LINKEDIT(symbols and linking information).__PAGEZEROmaps the lowest 4 GB with no access at all on 64-bit, so any pointer truncated to 32 bits or null crashes immediately.LC_MAINgives the entry point,LC_LOAD_DYLINKERnames the dynamic linker (/usr/lib/dyld), andLC_LOAD_DYLIBlists libraries — every program loadslibSystem.B.dylib.LC_CODE_SIGNATURE: on Apple silicon, every executable must carry a signature, even an ad-hoc one, or the kernel refuses to run it.
Mach-O also has universal (or fat) binaries: several complete Mach-O files, one per architecture, behind a small header. On this Mac, /usr/bin/ssh contains both an x86_64 and an arm64e slice.
Side by side
| ELF | PE | Mach-O | |
|---|---|---|---|
| Entry point | header e_entry | AddressOfEntryPoint (RVA) | LC_MAIN |
| Load description | program headers (segments) | section table + alignment | LC_SEGMENT_64 commands |
| Needed libraries | DT_NEEDED | import directory | LC_LOAD_DYLIB |
| Imported function pointers | GOT (+ PLT stubs) | IAT | lazy and non-lazy symbol pointers |
| Dynamic linker | PT_INTERP (ld-linux…) | built into the OS loader | LC_LOAD_DYLINKER (dyld) |
| Position independence | PIE (ET_DYN) | DYNAMIC_BASE + .reloc | PIE flag |
Reading one yourself
| Task | Linux / ELF | Windows / PE | macOS / Mach-O |
|---|---|---|---|
| what is this file? | file | file, PE-bear | file, lipo -archs |
| header | readelf -h | dumpbin /headers | otool -hv |
| segments / sections | readelf -lW, readelf -SW | dumpbin /headers | otool -l |
| libraries needed | readelf -d | dumpbin /imports | otool -L |
| disassemble | objdump -d -M intel | dumpbin /disasm | otool -tV, objdump -d |
One warning: ldd lists a program's libraries, but on some systems it does so by running the program's dynamic linker. Don't use it on a file you don't trust; readelf -d or objdump -p read the same information without executing anything.
Takeaways
- An executable is a format shared by the linker and the OS loader: ELF on Linux, PE on Windows, Mach-O on Apple systems.
- The header gives the architecture and the entry point. Segments (ELF program headers, PE sections, Mach-O
LC_SEGMENT_64) say what to map where, with which permissions. - Sections are the linker's and debugger's finer view. The loader doesn't need them, which is why
stripworks. - Dynamic programs name an interpreter or dynamic linker and their needed libraries, and carry the tables the loader fills: the GOT, the IAT, the symbol pointers.
- The headers also record security properties — PIE/ASLR, a non-executable stack, RELRO,
NX_COMPAT, code signatures — which is why they're the first thing to read when analyzing a binary.