Apple transitioned Macs from Intel to Apple Silicon without splitting the ecosystem in two: one download, one .dylib, both architectures. The file format that made that possible is the FAT (or "universal") container — a small big-endian table up front pointing at two or more complete Mach-O slices inside the same file. file(1) on our samples prints it plainly: "Mach-O universal binary with 2 architectures."
Every tool in the chain speaks FAT: lipo creates and inspects them, the kernel reads the arch table to pick a slice before dyld ever runs, and code-signing signs each slice independently. If you have ever wondered where "does not contain a binary compatible with your CPU" comes from, it starts here.
Think of a FAT file as a table of contents followed by stacked payloads:
nfat_arch, the number of table rows that follow.CF FA ED FE, mach_header_64, its own load commands), padded to its declared alignment. The gaps read as zeros — fat.h allows them to be filesystem holes.The model's missing piece: every fat field is big-endian — the opposite regime from the little-endian slices they describe. fat.h: "All structures defined here are always written and read to/from disk in big-endian order." Read the table little-endian and every offset lands in the wrong place.
The two structs from <mach-o/fat.h>, all big-endian:
| Struct | Field | Size | What it says |
|---|---|---|---|
| fat_header | magic | 4 | 0xCAFEBABE (FAT_MAGIC) |
| fat_header | nfat_arch | 4 | number of fat_arch rows |
| fat_arch | cputype | 4 | slice architecture (e.g. 0x01000007) |
| fat_arch | cpusubtype | 4 | slice variant (e.g. 0x80000003) |
| fat_arch | offset | 4 | file offset of the slice's mach magic |
| fat_arch | size | 4 | byte length of the whole slice |
| fat_arch | align | 4 | alignment exponent: offset must honor 2^align |
Verified on our samples — universal.macho (66304 bytes, built with llvm-lipo -create):
| Row | cputype | cpusubtype | offset | size | align |
|---|---|---|---|---|---|
| arch 0 | 0x01000007 (x86_64) | 0x80000003 | 4096 | 12616 | 2^12 = 4096 |
| arch 1 | 0x0100000C (arm64) | 0 | 32768 | 33536 | 2^14 = 16384 |
Slice 0 is byte-for-byte the 12616-byte prog64.macho, slice 1 the 33536-byte prog_arm64.macho — verify by reading offset 4096: CF FA ED FE again, a full little-endian thin file nested in the big-endian table. The signed production sample libasyncProfiler.dylib (1446144 bytes) follows the same shape: x86_64 slice at 0x4000 (size 0xB2890, align 2^14), arm64 at 0xB8000 (size 0xA9100, align 2^14), with the zero padding between the 48-byte arch table and offset 0x4000 there to honor that 2^14-byte alignment.
The first 48 bytes of universal.macho — the header and both arch rows:
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 ................
CA FE BA BE — FAT_MAGIC, read big-endian.00 00 00 02 — nfat_arch = 2 slices follow.00 00 10 00 = 4096, size 00 00 31 48 = 12616, align 00 00 00 0C = 12.00 00 80 00 = 32768, size 00 00 83 00 = 33536, align 00 00 00 0E = 14.Both rows fit in 48 bytes: 8-byte fat_header + 2 × 20-byte fat_arch. The table ends at offset 48; everything until 4096 is zero padding to arch 0's alignment.
Inspect any FAT file two ways — raw bytes and the tool built for the job:
$ xxd -l 48 universal.macho
00000000: cafe babe 0000 0002 0100 0007 8000 0003 ........
$ llvm-lipo -info universal.macho
Architectures in the fat file universal.macho are: x86_64 arm64
$ xxd -s 4096 -l 4 universal.macho
00001000: cffa edfe ....What to look for: nfat_arch and lipo's architecture count agree (2), each fat_arch cputype matches the slice's own mach_header cputype at its offset, and the slice magic flips back to little-endian CF FA ED FE. Big-endian table, little-endian payloads — both facts visible in three commands.
Without looking back: what byte order do fat_header and fat_arch use, how big is a fat_arch, and where does the first slice's magic live?
fat_arch[0] of universal.macho reads offset 4096, size 12616, align 12. Which statement is true?
The pattern: table first (big-endian), payload at declared offset (little-endian inside), each slice self-contained down to its own magic.
You can now enter a Mach-O from either door: thin files at magic CF FA ED FE, containers at CA FE BA BE pointing into slices with that same thin magic. Once inside a slice, the 32-byte header hands you off to the region where the format's real instructions live — the flat array of load commands at byte 32.
Next: The Load Command Area — the cmd/cmdsize walk, the LC constants every tool knows, and the real 14-command list of prog64.macho.