Mach-O (universal/fat) file format
A macOS universal (fat) Mach-O binary bundling multiple architectures.
- MIME type
- application/x-mach-binary
- Extensions
- .dylib, .bundle
- Category
- Executables & bytecode
Magic-number signature
| 0x00000000 | CAFEBABE | ···· | 'CAFEBABE' fat magic |
About the Mach-O (universal/fat) format
A universal binary is a wrapper, not a binary: after an eight-byte header it carries one twenty-byte descriptor per architecture, and then the complete, independent Mach-O images themselves, each page-aligned at the offset its descriptor names. That is why a universal file is roughly the sum of its slices rather than a clever merge, and why lipo can add or remove an architecture without touching any code. The whole fat header is big-endian regardless of what the slices inside it are, a NeXT-era decision that survives because the format has been through three CPU transitions unchanged.
Header layout, byte by byte
| Offset | Length | Field | Meaning |
|---|---|---|---|
| 0 | 4 | magic (FAT_MAGIC) | CA FE BA BE, read big-endian as 0xCAFEBABE — the same four bytes as a Java class file. The variant CA FE BA BF (FAT_MAGIC_64) means the architecture descriptors below use 64-bit offsets and sizes, for binaries that exceed four gigabytes. |
| 4 | 4 | nfat_arch | How many architecture descriptors follow, big-endian. Realistically 1 to about 4 — two is overwhelmingly the common case, x86-64 plus arm64. Because the count is small, its upper two bytes are zero, which is exactly the property that separates this header from a Java class file. |
| 8 | 4 | fat_arch[0].cputype | CPU of the first slice: 0x01000007 x86-64, 0x0100000C arm64, and without the 0x01000000 flag the 32-bit forms. This is the field a build system reads to answer whether the file supports the machine in front of it. |
| 12 | 4 | fat_arch[0].cpusubtype | Variant within that architecture — 3 for x86-64 ALL, 0x80000002 for arm64e. Two slices may share a cputype and differ only here, which is how arm64 and arm64e can both be present. |
| 16 | 4 | fat_arch[0].offset | File offset where this slice's own Mach-O image starts, big-endian. Seek there and you should find CF FA ED FE. Everything inside the slice is measured from that point, not from the start of the fat file, which is why a slice can be copied out byte for byte and still run. |
| 20 | 4 | fat_arch[0].size | Byte length of that slice. Offset plus size for the last architecture is normally the file size; anything after it is padding or an appended signature. |
| 24 | 4 | fat_arch[0].align | Alignment as a power of two — 14 means the slice starts on a 16384-byte boundary, which is the page size on Apple silicon. This is why there are large runs of zero bytes between slices and why a universal binary is slightly larger than the sum of its parts. |
| 28 | 20 | fat_arch[1] | The next descriptor, same five fields in the same order. Descriptors are fixed at twenty bytes each (thirty-two in the FAT_MAGIC_64 form), so the nth one starts at 8 + 20n and there is no list terminator — nfat_arch is the only bound. |
Telling it apart from its neighbours
| Easily confused with | How to tell |
|---|---|
| Java class file | The collision that has confused magic-number tables for thirty years: both start CA FE BA BE. Read bytes 4-7 as a 16-bit pair. A Java class file has minor_version then major_version, and major_version is at least 45 for any real class file. A fat binary has nfat_arch there, a small count, so the bytes where major_version would sit are zero. Checking for a nonzero major_version first resolves it cleanly. |
| Mach-O executable | A thin Mach-O begins with CF FA ED FE or CE FA ED FE and its header fields are little-endian; the fat wrapper begins CA FE BA BE and every field is big-endian. If you extract a slice and inspect it separately, it is a thin binary — the fat header does not survive the copy, which is the correct behaviour. |
| FAT_MAGIC_64 (CAFEBABF) | One bit apart from the 32-bit form, and mixing them up is silent rather than loud: the descriptors are thirty-two bytes rather than twenty, with 64-bit offset and size. A parser assuming the short layout reads the second architecture's fields from the middle of the first descriptor and reports plausible garbage. |
| Java JAR | Worth stating because it is how the collision usually reaches a person: a JAR is a ZIP (50 4B 03 04), and the CA FE BA BE inside it is a compressed class file rather than anything Apple-related. Whatever appears at offset 0 is what counts. |
How these files break
- A truncated final slice. The fat header is at the front and lists every architecture with its offset and size, so a partial download still reports 'universal binary with 2 architectures' while the last slice's bytes stop early. Compare the last descriptor's offset plus size against the real file size.
- Descriptors that no longer match the slices. Editing a slice in place and changing its length without updating the fat header leaves the following architecture's offset pointing into the middle of the previous one, and the second slice's magic no longer reads CF FA ED FE. Every slice offset should land on a Mach-O magic.
- Prepended junk. An error page or a BOM saved ahead of the binary shifts every slice offset by the same amount, so the file still identifies as CAFEBABE-something while nothing inside it resolves. The fix is deleting everything before the magic, not repairing the descriptors.
- A code signature invalidated per slice. Each slice is signed independently, so patching one architecture leaves the others valid and produces a binary that runs on an Intel Mac and is killed on Apple silicon, or vice versa — the confusing symptom that sends people looking for a hardware problem.
Where the format stands today
Universal binaries are the mechanism Apple uses to get through CPU transitions without shipping two downloads: 68k to PowerPC, PowerPC to Intel, and Intel to Apple silicon each ran on this same header. Today a universal file usually means x86-64 plus arm64, which is what most Mac apps and Homebrew bottles that predate Apple-silicon-only builds contain, and what Xcode produces when a project targets both. The cost is size — the file carries complete copies of the program — so App Store delivery thins them before download and most in-house tools now ship arm64 only. The format itself is unlikely to disappear even so, because it is also how arm64 and arm64e images coexist in one file, and because lipo makes adding or stripping an architecture a header rewrite rather than a rebuild.
Open Mach-O (universal/fat) files on this site
Every one of these runs in your browser — the file never leaves your machine.
Related formats
Formats that are easy to confuse with Mach-O (universal/fat) — compare their leading bytes.
| Format | First bytes | Extension |
|---|---|---|
| Mach-O executable | CF FA ED FE | .dylib |
| Java class file | CA FE BA BE | .class |
| ELF executable | 7F 45 4C 46 | .elf |