Try to launch an Intel-only binary on Apple Silicon (or the reverse) and macOS refuses with "Bad CPU type in executable." That verdict comes from two 4-byte header fields: cputype and cpusubtype. They are also how lipo, the FAT parser, and dyld's slice picker choose which code in a universal file is allowed to run on the machine in front of them.
Misread these values and you will "run" x86 code on arm64 — or decide a slice is compatible when its capability bits say otherwise. The encoding is simple once you see that both fields pack a base value plus flags.
Two questions, two fields:
The model's missing piece: the loader masks off the capability byte before choosing a slice — "ALL" subtypes mean "runs on any hardware of this type," while capability bits annotate how the binary was built (64-bit libraries, pointer authentication).
Apple's <mach/machine.h> defines the constants (cputype is integer_t, 4 bytes, stored little-endian in thin files):
| Constant | Value | How it is formed |
|---|---|---|
| CPU_TYPE_X86 | 7 | base family number |
| CPU_TYPE_X86_64 | 0x01000007 (16777223) | CPU_TYPE_X86 | CPU_ARCH_ABI64 (0x01000000) |
| CPU_TYPE_ARM | 12 | base family number |
| CPU_TYPE_ARM64 | 0x0100000C (16777228) | CPU_TYPE_ARM | CPU_ARCH_ABI64 |
| CPU_SUBTYPE_X86_64_ALL | 3 | runs on any x86_64 hardware |
| CPU_SUBTYPE_ARM64_ALL | 0 | runs on any arm64 hardware |
| CPU_SUBTYPE_ARM64E | 2 | arm64e — pointer-authenticated ABI |
| CPU_SUBTYPE_LIB64 | 0x80000000 | capability bit ORed into the subtype's high byte |
In real headers: prog64.macho stores cpusubtype bytes 03 00 00 80 = 0x80000003 — subtype 3 (X86_64_ALL) with the LIB64 capability bit, which otool helpfully splits into subtype ALL and caps 0x80. prog_arm64.macho stores subtype 0 (ARM64_ALL), caps 0x00. In the FAT tables, universal.macho's x86_64 arch carries 0x80000003 while its arm64 arch carries plain 0.
The same twelve bytes from our two thin executables — only cputype and subtype differ:
prog64.macho (x86_64): 00000000: cffa edfe 0700 0001 0300 0080 0200 0000 ........ prog_arm64.macho (arm64): 00000000: cffa edfe 0c00 0001 0000 0000 0200 0000 ........
CF FA ED FE — same magic, same 64-bit little-endian regime.07 00 00 01 = 0x01000007 (x86_64) vs 0C 00 00 01 = 0x0100000C (arm64). Same base-plus-ABI64 construction, different base (7 vs 12).03 00 00 80 = 0x80000003 (X86_64_ALL | LIB64) vs 00 00 00 00 = 0 (ARM64_ALL).These are exactly the two slices inside universal.macho — the FAT arch entries store the same pairs (0x01000007/0x80000003 and 0x0100000C/0) in big-endian.
Compare architectures across every sample at once:
$ for f in *.macho *.o; do echo "== $f"; xxd -l 12 "$f" | head -1; done
== prog64.o
00000000: cffa edfe 0700 0001 0300 0000 ....
== prog_arm64.o
00000000: cffa edfe 0c00 0001 0000 0000 ....
$ llvm-readobj --file-headers prog_arm64.macho | grep Cpu
CpuType: Arm64 (0x100000C)
CpuSubType: CPU_SUBTYPE_ARM64_ALL (0x0)What to look for: bytes 4–7 are the whole architecture story — 07 00 00 01 versus 0C 00 00 01, little-endian, identical position in every 64-bit Mach-O ever made. Bytes 8–11 then refine the variant and carry the capability byte.
Without looking back: how is CPU_TYPE_ARM64 (0x0100000C) constructed, and what does the high byte of a cpusubtype like 0x80000003 carry?
A FAT header entry reads cputype 0x01000007, cpusubtype 0x80000003. Which slice is this, and how would a tool display the subtype?
The pattern: cputype = family | ABI-width bits; cpusubtype = variant | capability bits in the top byte. Mask before you compare.
cputype and cpusubtype are not only header decoration — they are the selection keys of a container format. When one file must carry both an x86_64 slice (0x01000007) and an arm64 slice (0x0100000C), those pairs are written into a big-endian table up front, and the loader picks by them.
Next: Universal (FAT) Binaries — fat_header, fat_arch, slice offsets and alignment, walked through the real universal.macho and libasyncProfiler.dylib.