Back to course

The mach_header_64 Field by Field

20 minintermediateModule 2: HeadersContinues from macho-magic
20 min · Module 2: Headers

Why This Matters

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.

A Simple Model

Eight 4-byte fields, front to back — seven of them shared with the 32-bit header, plus one 64-bit-only extra:

  1. Identity — magic (0xFEEDFACF), cputype, cpusubtype: what machine this file is for.
  2. Purpose — filetype: object, executable, dylib, bundle, dSYM…
  3. Bounds — ncmds and sizeofcmds: the count and byte-span of the load command array that starts at offset 32.
  4. Behavior — flags (a bitmask) and reserved (must be zero on 64-bit files).

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.

The Actual Detail

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):

OffsetSizeFieldprog64.macho value
04magic0xFEEDFACF (MH_MAGIC_64)
44cputype0x01000007 (CPU_TYPE_X86_64)
84cpusubtype0x80000003 (X86_64_ALL | LIB64)
124filetype2 (MH_EXECUTE)
164ncmds14
204sizeofcmds968
244flags0x00200085
284reserved0 (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).

Common mistake. You might think the load commands start "after the first segment." They start at a fixed byte 32 — always. The first segment's payload may even cover the header itself: in executables, __TEXT maps from fileoff 0, so the header sits inside segment 0 while still being the first 32 bytes of the file.

A Real Example

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  .......... ......
  1. Bytes 0–3: CF FA ED FE — MH_MAGIC_64 little-endian.
  2. Bytes 4–7: 07 00 00 01 — 0x01000007 = CPU_TYPE_X86_64.
  3. Bytes 8–11: 03 00 00 80 — 0x80000003 = CPU_SUBTYPE_X86_64_ALL (3) with the CPU_SUBTYPE_LIB64 capability bit (0x80000000) ORed in.
  4. Bytes 12–15: 02 00 00 00 — filetype 2 = MH_EXECUTE.
  5. Bytes 16–19: 0E 00 00 00 — ncmds = 14.
  6. Bytes 20–23: C8 03 00 00 — sizeofcmds = 0x3C8 = 968, so commands occupy bytes 32–999.
  7. Bytes 24–27: 85 00 20 00 — 0x00200085 = NOUNDEFS | DYLDLINK | TWOLEVEL | PIE.
  8. Bytes 28–31: 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.

Try It

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: 968

What 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.

Check Your Understanding

Without looking back: what are the eight mach_header_64 fields in order, and which two bound the load command area?

Apply It

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.

Connect

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.