Between the shell's execve and the first line of your main() sits dyld, the dynamic linker — the only component that reads every load command you have studied and acts on them. Segments get mapped, dependencies chased, pointers fixed, exports published, initializers run, entry jumped to. A load command nobody reads is dead bytes; dyld is the reader.
It also explains most load-time mysteries: why a missing dylib fails before any of your code runs, why a function pointer is wrong until "fixups" happen, why constructors fire in an order you did not choose, and why an unknown command marked LC_REQ_DYLD can stop the image from loading at all while an unknown optional command is silently ignored.
Seven stages, each backed by a load command we can point at in our samples:
/usr/lib/dyld, and transfers control to it.The model's missing piece: stages 4 through 6 leave no line in otool — their evidence is the offsets and formats you decoded in earlier concepts. And dyld's shared-cache bootstrap (how /usr/lib/libSystem.B.dylib materializes without a loose file) lives outside our samples: we can see the request for that dylib, never the cache itself.
Our two samples sit on opposite sides of a format-era split. Same pipeline, different evidence — every cell read from the files:
| Stage evidence | prog64.macho (modern) | libasyncProfiler.dylib (classic) |
|---|---|---|
| Image / flags | EXECUTE, ncmds 14 — flags include MH_PIE (random base) | DYLIB, ncmds 18 — no MH_PIE; flags: NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK NO_REEXPORTED_DYLIBS |
| Dynamic linker | LC_LOAD_DYLINKER → /usr/lib/dyld | none — dylibs are loaded, not launched ("a file can have at most one of these") |
| Entry | LC_MAIN, entryoff 1104 | none — LC_MAIN is "only used in MH_EXECUTE filetypes" |
| Dependencies | none — no LC_LOAD_DYLIB at all | /usr/lib/libSystem.B.dylib, /usr/lib/libc++.1.dylib |
| Run paths | none | LC_RPATH: @executable_path/../lib and @executable_path/../lib/server |
| Pointer fixing | LC_DYLD_CHAINED_FIXUPS, 56 bytes at 12288 | LC_DYLD_INFO_ONLY: rebase 573440/248, bind 573688/2664, lazy 577472/4656 |
| Exports | LC_DYLD_EXPORTS_TRIE, 96 bytes at 12344 | export region 582128/1432 inside dyld_info |
| Full symbols | LC_SYMTAB, nsyms 6 | LC_SYMTAB, nsyms 2015 (strings at 619424, 87936 bytes) |
The dylib also shows the install-name machinery the closure stage consumes: LC_ID_DYLIB names itself build/lib/libasyncProfiler.dylib, and each LC_LOAD_DYLIB carries an absolute path — no rpaths needed for those two, but the two LC_RPATH commands are there for any @rpath-prefixed dependency. rpath_command's comment is exact: "a path which at runtime should be added to the current run path used to find @rpath prefixed dylibs."
The modern fixup stage, in full: LC_DYLD_CHAINED_FIXUPS's 56-byte header at file offset 12288, decoded field by field against fixup-chains.h:
00003000: 0000 0000 2000 0000 3800 0000 3800 0000 .... ...8...8... 00003010: 0000 0000 0100 0000 0000 0000 0000 0000 ................
fixups_version 0, starts_offset 0x20, imports_offset 0x38, symbols_offset 0x38, imports_count 0, imports_format 1, symbols_format 0. That imports_count of zero is the whole file in one number: this program links no libraries (no LC_LOAD_DYLIB), so it imports no symbols — only rebase chains, relocating code and data within its own image. The header's two length fields even agree: starts begin at 0x20, imports would begin at 0x38, and 56 bytes of payload end exactly at 0x38 with nothing between to count.
Compare the classic side of the same stage — five offset/size pairs dyld walks in order (rebase, bind, weak_bind, lazy_bind, export), then LC_SYMTAB's nsyms 2015 for everything else. Two eras, one pipeline stage.
Watch both halves of the pipeline declare themselves — on macOS use otool, on Linux llvm-otool:
$ llvm-otool -l prog64.macho | grep -A2 LC_LOAD_DYLINKER
cmd LC_LOAD_DYLINKER
cmdsize 32
name /usr/lib/dyld (offset 12)
$ llvm-otool -L libasyncProfiler.dylib
libasyncProfiler.dylib:
build/lib/libasyncProfiler.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1500.65.0)
$ llvm-otool -l libasyncProfiler.dylib | grep -A11 LC_DYLD_INFO_ONLY
cmd LC_DYLD_INFO_ONLY
cmdsize 48
rebase_off 573440
rebase_size 248
bind_off 573688
bind_size 2664
weak_bind_off 576352
weak_bind_size 1120
lazy_bind_off 577472
lazy_bind_size 4656
export_off 582128
export_size 1432What to look for: the executable names its dynamic linker and nothing else to load; otool -L's first line is always the dylib's own install name (its LC_ID_DYLIB), the rest are stage-3 closure edges; and the five dyld_info pairs are the entire classic fixup-plus-exports stage in twelve lines. Every offset points into __LINKEDIT — where the map lesson said this metadata lives.
Without looking back: which load command tells the kernel and dyld where the dynamic linker itself lives, and what does prog64.macho name?
Suppose an executable declares LC_RPATH @executable_path/../lib and links a dependency named @rpath/libFoo.dylib. How does dyld resolve it?
The pattern: every stage of the pipeline has a load command as its receipt — dylinker, dylibs, rpaths, fixups, exports, init sections, entry. Read the commands and you have read the algorithm.
dyld has now consumed the entire format: the protections it mapped, the rebase and bind streams or chained fixups it executed, the export trie it published, and the LC_MAIN entry it is about to jump to. But "the image is loaded" still hides a question every crash report depends on: at which addresses does all this actually exist right now?
Next: Process Memory Layout — preferred addresses, the slide, and the three coordinate systems (file offset, vmaddr, runtime address) that every bug report mixes together.