Double-click an app on macOS and it runs. The file on disk — whether an app bundle's main executable, a framework, or a command-line tool — is a Mach-O file. How does the kernel know where the code starts, which libraries it must load first, where the symbols live, and which bytes are signed?
The Mach Object (Mach-O) format answers all of those questions. It is the contract between Apple's toolchain (clang, ld64) and the dynamic linker dyld, which runs before your program's main() ever executes. If you have ever wondered what otool or nm actually reads, or how code signing and ASLR work at the byte level, this format is the whole story.
Without a defined layout, no program could be loaded — every compiler would invent its own structure, and macOS could not run any of them.
Think of a Mach-O file as a header followed by a list of instructions for the loader:
CF FA ED FE (0xFEEDFACF), then the CPU type, file type, and counts.__LINKEDIT segment holding symbol tables, string tables, rebase/bind info, and code signatures.The model's missing piece: those load commands are variable-length, so the header's ncmds and sizeofcmds fields are what let the loader walk them safely. Get those two wrong and the file will not load.
Apple's <mach-o/loader.h> defines the on-disk order exactly:
| Region | Starts at | Size | What it says |
|---|---|---|---|
| mach_header_64 | File offset 0 | 32 bytes | magic = 0xFEEDFACF, cputype, filetype, ncmds, sizeofcmds, flags |
| Load commands | Offset 32 | sizeofcmds bytes | LC_SEGMENT_64, LC_SYMTAB, LC_LOAD_DYLIB, LC_MAIN, ... |
| Segment data | Per-segment fileoff | Per-segment filesize | __TEXT, __DATA_CONST, __DATA, __LINKEDIT contents |
| __LINKEDIT | Last segment's fileoff | filesize | Symbols, strings, rebase/bind opcodes, export trie, code signature |
Unlike PE (which hides the real header behind an MS-DOS stub) or ELF (which has two header tables), Mach-O puts its single header at byte 0 and follows it with one flat array of load commands. The vocabulary you will use all course: file offset is a position on disk; vmaddr is where a segment lands in memory; __LINKEDIT is the trailing metadata segment that dyld and the kernel both read.
feedface as a single big-endian word. It does not — the bytes on disk are CF FA ED FE, which is 0xFEEDFACF read little-endian. FAT (universal) binaries are the exception: they start CA FE BA BE (0xCAFEBABE big-endian).First 32 bytes of a real 64-bit Mach-O object file (prog64.o, built with clang -target x86_64-apple-darwin -c):
00000000: cffaedfe 07000001 03000000 01000000 ................ 00000010: 04000000 00020000 00200000 00000000 ......... ......
Walk-through:
CF FA ED FE — MH_MAGIC_64 (0xFEEDFACF little-endian), so this is a 64-bit Mach-O.07 00 00 01 — cputype 0x01000007 = CPU_TYPE_X86_64.03 00 00 00 — cpusubtype 3 = CPU_SUBTYPE_X86_64_ALL.01 00 00 00 — filetype 1 = MH_OBJECT (a relocatable object, not an executable).04 00 00 00 — ncmds = 4 load commands follow.00 02 00 00 — sizeofcmds = 0x200 = 512 bytes.00 20 00 00 — flags = 0x2000 = MH_SUBSECTIONS_VIA_SYMBOLS.That is a complete header decode from 32 bytes — the pattern every Mach-O parse starts with.
Any Mach-O file works — app binaries, dylibs, or object files. On Linux or macOS:
$ file /usr/bin/ls
/usr/bin/ls: Mach-O 64-bit executable arm64
$ xxd -l 32 /usr/bin/ls
00000000: cffaedfe 0c000001 00000000 02000000 ................
...What to look for: the first four bytes are always CF FA ED FE for thin 64-bit files, and every multi-byte field you read from the file is little-endian — the lowest-address byte is the least significant. That single rule decodes half the format (FAT headers are the big-endian exception).
Without looking back: what four bytes open a thin 64-bit Mach-O file, and what 32-bit value do they form little-endian?
A Mach-O header has ncmds = 14 and sizeofcmds = 968. The header is 32 bytes. At what file offset does the first load command start, and where does segment data begin?
The pattern: header is always 32 bytes, load commands start at 32, and the first segment's fileoff is normally 32 + sizeofcmds (though segments can declare any fileoff).
This course follows the same spine the ELF and PE courses did: headers first (what the loader reads), then addressing (how disk maps to memory), then segments and the tables inside them (symbols, dynamic linking, exports), and finally the loader (dyld) itself.
Next: Mach-O File Layout — the full map from byte 0 to the last byte of __LINKEDIT, before any individual field is decoded.