A crash log says 0x7fff5fbff000. Is that stack, a dylib, or the NULL guard? A debugger says your code is at 0x100004450 but the file says 0x100000450. Symbol offsets in a bug tracker are file bytes, not addresses. Every one of these is a collision between three coordinate systems — file offset, preferred vmaddr, and runtime address — and this lesson is where you learn to tell them apart.
The layout is also the contract between everything so far: __PAGEZERO set the base, dyld applied the slide, segments declared what can be read or written. Get the translation wrong and you will "fix" a bug at an address that is not even mapped.
One picture, then the rule:
The rule: runtime address = preferred vmaddr + slide, applied uniformly to every image. The rule's missing piece: preferred is a preference, not a fact — MH_PIE exists so the second run of the program sees none of these numbers as printed. And file offsets are a separate axis entirely: to translate, find the segment whose file range contains the offset, then add (offset − fileoff) to that segment's vmaddr.
The full preferred map of prog64.macho, straight from its four LC_SEGMENT_64 commands:
| Segment | Preferred vmaddr | vmsize | fileoff / filesize | initprot |
|---|---|---|---|---|
| __PAGEZERO | 0x0 | 0x100000000 | 0 / 0 | 0 |
| __TEXT | 0x100000000 | 0x2000 | 0 / 8192 | 5 |
| __DATA | 0x100002000 | 0x1000 | 8192 / 4096 | 3 |
| __LINKEDIT | 0x100003000 | 0x148 | 12288 / 328 | 1 |
Every vmaddr + vmsize equals the next vmaddr — the four segments tile address space contiguously from 0 through 0x100003148 with zero gaps, exactly as their file ranges tile the 12616-byte file. Two section-level details finish the map: __text begins at file offset 1040 (addr 0x100000410, flags 0x80000400 = pure instructions + some instructions), and __common sits at addr 0x100002004 with offset 0 and flags 0x1 = S_ZEROFILL — "zero fill on demand" — so _bss_counter's 4 bytes exist only in memory, which is why filesize accounting stops at initialized data. loader.h even reserves the convention: the link-editor "will allocate common symbols at the end of the __common section in the __DATA segment."
Contrast with ELF, once: a PIE ELF executable typically declares no fixed base and lets the loader choose; Mach-O's MH_EXECUTE names 0x100000000 explicitly and reserves everything below it. Same ASLR outcome, different philosophy — the address story is part of the file format, not just the loader.
universal.macho each slice — x86_64 at file offset 4096, arm64 at 32768 — is a complete thin Mach-O with its own segment map, its own preferred addresses, its own UUID. Only one slice's map is ever live in a given process.Every symbol in the file, placed on the map by one nm run:
0000000100000000 T __mh_execute_header 0000000100000410 T _add 0000000100000440 t _helper 0000000100000450 T _main 00000001000002000 D _global_counter 00000001000002004 S _bss_counter
Read the column as coordinates, not decoration: __mh_execute_header marks 0x100000000, the __TEXT base; _add, _helper, _main sit inside __text (0x100000410 … 0x100000484); _global_counter (type D, data) is the first byte of __DATA — the 42 you saw at file offset 8192; _bss_counter (type S, zero-fill) is the next 4 bytes at 0x100002004 with no file byte behind them. Lowercase t for _helper says "local text" — the same address space, tighter binding. Two address columns (file layout lesson and this one) now join into one picture.
Verify contiguity and practice the translation — on macOS use otool, on Linux llvm-otool:
$ llvm-otool -l prog64.macho | grep -E "segname|vmaddr|vmsize|initprot"
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
initprot 0x00000000
segname __TEXT
vmaddr 0x0000000100000000
vmsize 0x0000000000002000
initprot 0x00000005
... __DATA vmaddr 0x100002000 vmsize 0x1000 initprot 0x3
... __LINKEDIT vmaddr 0x100003000 vmsize 0x148 initprot 0x1
$ python3 -c "print(hex(0x100000000 + 1104))"
0x100000450What to look for: each vmaddr + vmsize equals the next vmaddr — add them on paper until you reach 0x100003148, the first address past the image's preferred extent. The python line is the file-offset translation you will need constantly: LC_MAIN's entryoff (1104) lives in __TEXT, whose fileoff is 0, so the preferred address is base + offset. Next, take a random offset like 8192 or 12288 and do the same by hand.
Without looking back: what is __TEXT's preferred address in prog64.macho, and what determines that particular number?
A fuzzer reports a crash at a file offset of 12288 inside prog64.macho (preferred addresses, slide 0). What virtual address is that?
The pattern: pick the segment by file range, add (offset − fileoff) to vmaddr for the preferred address, add the slide last — in that order, never mixed.
The map is live now: preferred addresses from the segment commands, a randomized slide from dyld, and a translation rule between disk and memory. One number in that picture has not fired yet — LC_MAIN's entryoff of 1104, sitting in __text at 0x100000450, waiting.
Next: The Startup Sequence — what the kernel does first, where argv really comes from, and why entryoff does not always point at main().