Skip to content

Level 4 · Chapter 4.1

Executable files: ELF, PE and Mach-O

What's inside a program file and how the OS reads it: headers, entry points, segments and sections, permissions, dynamic-linking information — dissected on real ELF, PE and Mach-O files, with the security flags they carry.

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:

FormatUsed byMagic bytes
ELF (Executable and Linkable Format)Linux, the BSDs, Android, most embedded systems7f 45 4c 46 — \x7fELF
PE (Portable Executable)Windows, UEFI firmware4d 5a — MZ
Mach-OmacOS, iOScf 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...@.....
BytesFieldValue
7f 45 4c 46magic\x7fELF
02class64-bit
01datalittle-endian
02 00typeET_EXEC, a fixed-address executable
3e 00machine0x3e, x86-64
60 11 20 00 …entry point0x201160, the address of _start
40 00 …program header offsetright after the header
05 00program header count5
09 00section header count9

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 LOAD has a file size of 4 bytes (.data) but a memory size of 0x1004. The extra 4 KB is .bss: the kernel provides it as zeroed memory, and it takes no space in the file.
  • STACK with flags rw- 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.
  • INTERP names the dynamic linker to run first. On x86-64 Linux it's /lib64/ld-linux-x86-64.so.2.
  • DYNAMIC points to the dynamic section, whose first entry here is NEEDED libc.so.6: the libraries to load. It also holds the relocation tables and FLAGS_1: PIE.
  • GNU_RELRO marks 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 (here 0x78) points to the real header: the signature PE\0\0, then the COFF header (machine 0x8664 = x86-64, number of sections…) and the optional header, which isn't optional at all.

Some fields from this file's optional header:

FieldValueMeaning
Magic0x20BPE32+, a 64-bit image
ImageBase0x140000000preferred load address (the default for 64-bit programs)
AddressOfEntryPoint0x1000entry point, as an RVA
SectionAlignment / FileAlignment0x1000 / 0x200sections are 4 KB apart in memory, 512 B apart in the file
Subsystem3a console program
DllCharacteristics0x8160ASLR (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_64 commands define the segments: __PAGEZERO, __TEXT (code and constants), __DATA and __LINKEDIT (symbols and linking information). __PAGEZERO maps the lowest 4 GB with no access at all on 64-bit, so any pointer truncated to 32 bits or null crashes immediately.
  • LC_MAIN gives the entry point, LC_LOAD_DYLINKER names the dynamic linker (/usr/lib/dyld), and LC_LOAD_DYLIB lists libraries — every program loads libSystem.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

ELFPEMach-O
Entry pointheader e_entryAddressOfEntryPoint (RVA)LC_MAIN
Load descriptionprogram headers (segments)section table + alignmentLC_SEGMENT_64 commands
Needed librariesDT_NEEDEDimport directoryLC_LOAD_DYLIB
Imported function pointersGOT (+ PLT stubs)IATlazy and non-lazy symbol pointers
Dynamic linkerPT_INTERP (ld-linux…)built into the OS loaderLC_LOAD_DYLINKER (dyld)
Position independencePIE (ET_DYN)DYNAMIC_BASE + .relocPIE flag

Reading one yourself

TaskLinux / ELFWindows / PEmacOS / Mach-O
what is this file?filefile, PE-bearfile, lipo -archs
headerreadelf -hdumpbin /headersotool -hv
segments / sectionsreadelf -lW, readelf -SWdumpbin /headersotool -l
libraries neededreadelf -ddumpbin /importsotool -L
disassembleobjdump -d -M inteldumpbin /disasmotool -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 strip works.
  • 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.

In this level

  1. 4.1Executable files: ELF, PE and Mach-O
  2. 4.2Processes and the address spacePlanned
  3. 4.3System calls and privilege levelsPlanned
  4. 4.4Virtual memory and pagingPlanned
  5. 4.5Files, devices and I/OPlanned
  6. 4.6Threads and synchronizationPlanned
  7. 4.7Hardware virtualization and hypervisorsPlanned
  8. 4.8Inside UNIX and WindowsPlanned