Every Mach-O parser — dyld, otool, lldb, the kernel — makes exactly one decision before it reads anything else: look at the first four bytes and decide what kind of file this is, and which byte order its fields use. Get that decision wrong and every field after the magic is scrambled: your cputype becomes nonsense, your ncmds becomes a billion, and the walk over the load commands runs off the end of the file.
Mach-O is unusual because it has two byte-order regimes in one format. Thin files are little-endian on disk. FAT (universal) headers are always big-endian. Knowing which regime you are in is half the format.
Ask two questions of the first four bytes:
CA FE BA BE, this is a FAT container — stop, parse the big-endian fat header instead of a mach_header. Otherwise it is a thin file and byte 0 opens a mach_header.The model's missing piece: CIGAM is not a separate file type. It is the magic read "backwards." The name is literally MAGIC spelled in reverse, and the values are the byte-swapped constants loader.h defines (MH_CIGAM, MH_CIGAM_64).
Apple's <mach-o/loader.h> and <mach-o/fat.h> define the full set:
| Constant | Value | Bytes on disk | Meaning |
|---|---|---|---|
| MH_MAGIC | 0xFEEDFACE | CE FA ED FE | 32-bit thin file, little-endian |
| MH_MAGIC_64 | 0xFEEDFACF | CF FA ED FE | 64-bit thin file, little-endian — every modern sample |
| MH_CIGAM | 0xCEFAEDFE | FE ED FA CE | Byte-swapped 32-bit magic |
| MH_CIGAM_64 | 0xCFFAEDFE | FE ED FA CF | Byte-swapped 64-bit magic (NXSwapInt(MH_MAGIC_64)) |
| FAT_MAGIC | 0xCAFEBABE | CA FE BA BE | FAT container — always written big-endian |
| FAT_CIGAM | 0xBEBAFECA | BE BA FE CA | Byte-swapped FAT magic — what a little-endian reader that forgets to swap sees |
How the swap trick works, concretely: a little-endian tool reads four bytes and interprets them little-endian. Bytes CF FA ED FE become 0xFEEDFACF — normal. Bytes FE ED FA CF become 0xCFFAEDFE — exactly MH_CIGAM_64, the signal "this file's fields are encoded opposite to your native order; byte-swap before trusting anything." The FAT side is simpler: fat.h says "All structures defined here are always written and read to/from disk in big-endian order" — that single sentence is why CA FE BA BE reads as 0xCAFEBABE with no swap, everywhere.
FE ED FA CF. In practice every modern 64-bit Mach-O stores CF FA ED FE — including every sample in this course. The FE-ED-FA-CF form belongs to the byte-swapped (CIGAM) case, not to a normal file.The first bytes of our two sample families — thin object file, then FAT container:
00000000: cffa edfe 0700 0001 0300 0000 0100 0000 ................ 00000010: 0400 0000 0002 0000 0020 0000 0000 0000 ......... ......
prog64.o: bytes CF FA ED FE read little-endian are 0xFEEDFACF — MH_MAGIC_64, thin file, fields little-endian from here on. (Full field decode is the next concept.)
00000000: cafe babe 0000 0002 0100 0007 8000 0003 ................ 00000010: 0000 1000 0000 3148 0000 000c 0100 000c ......1H........ 00000020: 0000 0000 0000 8000 0000 8300 0000 000e ................
universal.macho: bytes CA FE BA BE are FAT_MAGIC read big-endian. Next word 00 00 00 02 = nfat_arch 2, and each 20-byte fat_arch that follows is big-endian too — cputype 01 00 00 07 (x86_64), then 00 00 10 00 = offset 4096. Peek at offset 4096 and the magic regime changes: the slice opens CF FA ED FE again — a complete little-endian thin Mach-O nested inside a big-endian envelope.
Compare a thin file and a FAT file side by side:
$ xxd -l 16 prog64.o
00000000: cffa edfe 0700 0001 0300 0000 0100 0000 ........
$ xxd -l 16 universal.macho
00000000: cafe babe 0000 0002 0100 0007 8000 0003 ........
$ xxd -s 4096 -l 16 universal.macho
00001000: cffa edfe 0700 0001 0300 0080 0200 0000 ........What to look for: the thin file and the nested slice both start cf fa ed fe; only the container starts ca fe ba be. First four bytes tell you which parser to use, and the slice magic tells you the payload inside is ordinary little-endian Mach-O again.
Without looking back: what four bytes open a thin 64-bit Mach-O, a FAT container, and what byte order does each use?
A tool reads the first four bytes of a file as a little-endian u32 and gets 0xCFFAEDFE. Which constant did it just read, and what must the tool do next?
The pattern: four bytes choose the parser and the byte order for everything after them. Thin = little-endian, FAT = big-endian, CIGAM = swap.
You have the layout and the regime selector: four bytes at offset 0 tell every tool which structure follows and how to read it. Once the magic says "64-bit thin, little-endian," the next 28 bytes are a fixed eight-field header — the one dyld decodes before anything else.
Next: The mach_header_64 Field by Field — all 32 bytes decoded: cputype, filetype, ncmds, sizeofcmds, flags, and the reserved word.