The intro showed you the first 32 bytes of a Mach-O file. That is the entry point into the format — not the format itself. Before decoding any single field, you need the whole map: which regions exist, in what order they sit on disk, and where one ends and the next begins.
Everyone who touches a Mach-O needs this map. dyld walks it to map segments and find the entry point. A debugger walks it to find symbols. A code-signing tool rewrites part of it. Get one size wrong — trust a stale sizeofcmds, misread a fileoff — and every region after it is garbage and the file will not load.
Think of a thin Mach-O file as four zones front to back:
ncmds and sizeofcmds.sizeofcmds bytes starting at offset 32, a flat array of variable-length commands telling the loader what to do.fileoff and filesize.The model's missing piece: there is no master pointer to any of this. The header carries counts, not addresses. The only fixed fact is that load commands begin at byte 32 — every other region is discovered by walking outward from there. (ELF at least stores e_phoff and e_shoff pointers; Mach-O makes you earn it.)
Here is the exact byte map of prog64.macho (12616 bytes, MH_EXECUTE, x86_64), read from its four LC_SEGMENT_64 commands:
| Region | File range | Size | What it is |
|---|---|---|---|
| mach_header_64 | 0x0000–0x001F | 32 | magic, cputype, filetype 2 (EXECUTE), ncmds 14, sizeofcmds 968, flags 0x00200085 |
| Load commands | 0x0020–0x03E7 | 968 | 14 commands; area ends at byte 1000 (32 + 968) |
| __PAGEZERO | — | 0 | vmaddr 0, vmsize 0x100000000, filesize 0 — memory only, no file bytes |
| __TEXT | 0x0000–0x1FFF | 8192 | fileoff 0 — the header and load commands live inside this segment, plus __text, __unwind_info, __eh_frame |
| __DATA | 0x2000–0x2FFF | 4096 | fileoff 8192 — __data, __common |
| __LINKEDIT | 0x3000–0x32D7 | 328 | fileoff 12288 — chained fixups, exports, symbols, strings; 12288 + 328 = 12616 = file size |
Notice two things. First, __TEXT maps from file offset 0, so the header itself is part of the first segment — loader.h says the first segment of MH_EXECUTE and MH_DYLIB files contains the mach_header and load commands. Second, __PAGEZERO occupies the low 4 GiB of memory with filesize 0: a segment can be pure address space with no file behind it.
loader.h says all sections sit in "one unnamed segment with no segment padding." In prog64.o (992 bytes) the header is 32 bytes, the four load commands span 512, so the payload starts at exactly 544 = 32 + 512 — and otool prints the segment's segname as blank, while each section still carries its own __TEXT/__DATA segname.Three cuts through prog64.macho, one per zone boundary:
00000000: cffa edfe 0700 0001 0300 0080 0200 0000 ................ 00000010: 0e00 0000 c803 0000 8500 2000 0000 0000 .......... .....
Bytes 16–23: ncmds = 14, sizeofcmds = 0x3C8 = 968 — so the load command area is bytes 32 through 999, and the flags word 0x00200085 unpacks to NOUNDEFS | DYLDLINK | TWOLEVEL | PIE.
00002000: 2a00 0000 0000 0000 0000 0000 0000 0000 *...............
At file offset 8192 — the fileoff of __DATA — the first word is 2A 00 00 00 = 42. That is the source file's int global_counter = 42;, which nm reports as D _global_counter at vmaddr 0x100002000. Segment vmaddr 0x100002000 maps file byte 8192: the disk-to-memory contract in one line.
00003000: 0000 0000 2000 0000 3800 0000 3800 0000 .... ...8...8... 00003010: 0000 0000 0100 0000 0000 0000 0000 0000 ................
At file offset 12288 — __LINKEDIT's fileoff — metadata begins. The first blob is the LC_DYLD_CHAINED_FIXUPS payload (dataoff 12288, datasize 56): fixups_version = 0, starts_offset = 0x20. Symbols, strings, and the code signature all live further inside this same segment, each located by its own load command.
Any Mach-O works — on macOS use otool, on Linux llvm-otool:
$ llvm-otool -l prog64.macho | grep -E "segname|fileoff|filesize"
segname __PAGEZERO ... fileoff 0 filesize 0
segname __TEXT ... fileoff 0 filesize 8192
segname __DATA ... fileoff 8192 filesize 4096
segname __LINKEDIT ... fileoff 12288 filesize 328What to look for: the four fileoffs tile the file with no gaps and no overlaps (0, 8192, 12288, end 12616), and the last segment's fileoff + filesize equals the file size exactly — __LINKEDIT always finishes the job. If your numbers do not tile, you are reading the wrong fields.
Without looking back: where does the load command area start and end, and which header fields bound it?
In prog64.macho, __LINKEDIT has fileoff 12288 and filesize 328. The file is 12616 bytes. What does that tell you?
The pattern: segments tile the file through their fileoff/filesize pairs, the last one landing exactly on EOF. Header, commands, payload, __LINKEDIT — in that order, every time.
You have the map now: fixed header at 0, load commands from 32, payload located per segment, __LINKEDIT last. The header region starts with four bytes that decide how every subsequent field is read — they are not the same four bytes in every Mach-O file.
Next: Magic Numbers and Byte Order — why a thin file opens CF FA ED FE, a universal file opens CA FE BA BE, and only one of them is little-endian.