After the magic, a 64-bit Mach-O file has exactly 28 more header bytes before the load commands begin. Eight fields, all fixed offsets, no pointers. Those fields answer the loader's first questions in order: what CPU does this need, what kind of file is it, how many load commands follow, how many bytes do they occupy, and which global behaviors (PIE, two-level namespace, weak binding) are in effect.
If ncmds or sizeofcmds is corrupted, the load command walk goes out of bounds and the file is rejected — or worse, parsed into garbage. The header is small enough to memorize, and every Mach-O tool you will ever use starts by decoding exactly these 32 bytes.
Eight 4-byte fields, front to back — seven of them shared with the 32-bit header, plus one 64-bit-only extra:
The model's missing piece: none of these fields tells you where any segment, symbol, or string lives. The header is an identity card and a bounding box — everything with an address lives in the load commands it introduces.
loader.h defines struct mach_header_64 as eight uint32-sized fields (cputype and cpusubtype are signed cpu_type_t/cpu_subtype_t, but the widths are 4 bytes):
| Offset | Size | Field | prog64.macho value |
|---|---|---|---|
| 0 | 4 | magic | 0xFEEDFACF (MH_MAGIC_64) |
| 4 | 4 | cputype | 0x01000007 (CPU_TYPE_X86_64) |
| 8 | 4 | cpusubtype | 0x80000003 (X86_64_ALL | LIB64) |
| 12 | 4 | filetype | 2 (MH_EXECUTE) |
| 16 | 4 | ncmds | 14 |
| 20 | 4 | sizeofcmds | 968 |
| 24 | 4 | flags | 0x00200085 |
| 28 | 4 | reserved | 0 (64-bit only — the 32-bit mach_header is 28 bytes) |
filetype (loader.h): MH_OBJECT = 1, MH_EXECUTE = 2, MH_CORE = 4, MH_DYLIB = 6, MH_DYLINKER = 7, MH_BUNDLE = 8, MH_DSYM = 0xA. Object = what clang -c emits; executable = your program; dylib = a .dylib; dSYM = the debug companion.
flags you will meet constantly (loader.h): MH_NOUNDEFS = 0x1 (no undefined references), MH_DYLDLINK = 0x4 (input to the dynamic linker), MH_TWOLEVEL = 0x80 (two-level namespace), MH_SUBSECTIONS_VIA_SYMBOLS = 0x2000 (safe dead-strip granularity), MH_WEAK_DEFINES = 0x8000, MH_BINDS_TO_WEAK = 0x10000, MH_NO_REEXPORTED_DYLIBS = 0x100000, MH_PIE = 0x200000 (load at random address).
All 32 bytes of prog64.macho, one field at a time:
00000000: cffa edfe 0700 0001 0300 0080 0200 0000 ................ 00000010: 0e00 0000 c803 0000 8500 2000 0000 0000 .......... ......
CF FA ED FE — MH_MAGIC_64 little-endian.07 00 00 01 — 0x01000007 = CPU_TYPE_X86_64.03 00 00 80 — 0x80000003 = CPU_SUBTYPE_X86_64_ALL (3) with the CPU_SUBTYPE_LIB64 capability bit (0x80000000) ORed in.02 00 00 00 — filetype 2 = MH_EXECUTE.0E 00 00 00 — ncmds = 14.C8 03 00 00 — sizeofcmds = 0x3C8 = 968, so commands occupy bytes 32–999.85 00 20 00 — 0x00200085 = NOUNDEFS | DYLDLINK | TWOLEVEL | PIE.00 00 00 00 — reserved = 0.That is the entire header: 32 bytes, eight fields, every later structure found by walking from byte 32 using the bounds in fields 4 and 5.
Dump and decode any Mach-O header in one step:
$ xxd -l 32 prog64.macho
00000000: cffa edfe 0700 0001 0300 0080 0200 0000 ................
00000010: 0e00 0000 c803 0000 8500 2000 0000 0000 ................
$ llvm-readobj --file-headers prog64.macho
Magic: Magic64 (0xFEEDFACF)
FileType: Executable (0x2)
NumOfLoadCommands: 14
SizeOfLoadCommands: 968What to look for: xxd words at offsets 16 and 20 are exactly the NumOfLoadCommands and SizeOfLoadCommands that llvm-readobj prints — the tool is reading the same eight fields you just did, nothing more, before it walks the command area.
Without looking back: what are the eight mach_header_64 fields in order, and which two bound the load command area?
prog64.o has ncmds = 4 and sizeofcmds = 512. Where do its load commands end, and where does the first payload byte sit?
The pattern: end of commands = 32 + sizeofcmds. In object files the payload starts right there; in executables the header region is instead covered by __TEXT from fileoff 0.
Thirty-two bytes, decoded: identity, purpose, bounds, behavior. Two of those fields — cputype and cpusubtype — are the pair the kernel and dyld use to decide whether this file can run on the machine in front of them, and the pair a FAT container stores once per slice.
Next: CPU Types and Subtypes — how 0x01000007 is built from CPU_TYPE_X86 and the ABI64 bit, why the subtype carries a LIB64 flag, and what ARM64E means.