LC_SYMTAB gives you one flat array of nlist_64 entries. dyld cannot scan 2,005 symbols one by one every time it binds a reference — it needs to know, without searching, where the undefined symbols begin, where the exported definitions live, and which symbol each stub slot or GOT entry points at. LC_DYSYMTAB is that index into the same array: no new symbol storage, just ranges and pointers.
This is the Mach-O answer to ELF's split between .symtab and .dynsym — except here both views share one array, and the "dyn" information is this load command's partitions plus the indirect table.
One 80-byte command (cmd 0xb) holding eighteen u32 fields in five groups:
The model's missing piece: an indirect table entry is not a symbol — it is an index into LC_SYMTAB's array (or one of two specials, INDIRECT_SYMBOL_LOCAL = 0x80000000 and INDIRECT_SYMBOL_ABS = 0x40000000). The stub at address X and the nlist_64 at that index are two views of the same call target.
dysymtab_command's fields, in file order after cmd/cmdsize: ilocalsym, nlocalsym, iextdefsym, nextdefsym, iundefsym, nundefsym (the three partitions), then tocoff/ntoc, modtaboff/nmodtab, extrefsymoff/nextrefsyms, indirectsymoff/nindirectsyms, extreloff/nextrel, locreloff/nlocrel — eighteen u32s, 8 + 72 = 80 bytes total.
The real command from libasyncProfiler.dylib (x86_64 slice):
cmd LC_DYSYMTAB
cmdsize 80
ilocalsym 0
nlocalsym 1752
iextdefsym 1752
nextdefsym 45
iundefsym 1797
nundefsym 218
tocoff 0
ntoc 0
modtaboff 0
nmodtab 0
extrefsymoff 0
nextrefsyms 0
indirectsymoff 617880
nindirectsyms 385
extreloff 0
nextrel 0
locreloff 0
nlocrel 0Read the partitions as half-open ranges: locals [0, 1752), defined externals [1752, 1797), undefined [1797, 2015). And they must tile the whole table — 1752 + 45 + 218 = 2015 = nsyms from LC_SYMTAB. If that sum ever disagrees with nsyms, the file is corrupt or you are looking at the wrong architecture slice.
The indirect table's 385 entries are exactly the slots three sections need. Each section's reserved1 field is its starting index (loader.h documents this), and entry counts fall out of size divided by slot size:
| Section (flags type) | reserved1 | Entries | Slot |
|---|---|---|---|
| __stubs (0x8 = S_SYMBOL_STUBS) | 0 | 172 (size 0x408 ÷ 6, reserved2 = 6) | 6-byte jump stub |
| __got (0x6 = S_NON_LAZY_SYMBOL_POINTERS) | 172 | 41 (size 0x148 ÷ 8) | 8-byte pointer |
| __la_symbol_ptr (0x7 = S_LAZY_SYMBOL_POINTERS) | 213 | 172 (size 0x560 ÷ 8) | 8-byte pointer |
The arithmetic closes twice: 172 + 41 + 172 = 385 = nindirectsyms, and 172 + 41 = 213 = __la_symbol_ptr's starting index. The ranges also line up with the partitions — otool's first stub entries hold indexes 1797, 1798, 1799... exactly where the undefined partition starts, because lazy stubs jump to symbols this image does not define.
A small file shows the same structure: prog64.macho's LC_DYSYMTAB reads 0/1, 1/5, 6/0 — one local (_helper), five defined globals, nothing undefined. Even a fully resolved static-style executable still carries the command; dyld just has less to do.
$ otool -arch x86_64 -Iv libasyncProfiler.dylib | head -8
libasyncProfiler.dylib:
Indirect symbols for (__TEXT,__stubs) 172 entries
address index
0x000000000006cb62 1797
0x000000000006cb68 1798
0x000000000006cb6e 1799
0x000000000006cb74 1800
0x000000000006cb7a 1801What to look for: the address column walks 6 bytes at a time — one stub per entry, matching reserved2 = 6; and every index it prints lands at or past iundefsym (1797). Dump the command itself with otool -arch x86_64 -l libasyncProfiler.dylib | sed -n '/LC_DYSYMTAB/,/nlocrel/p'.
Without looking back: iundefsym = 1797 and nundefsym = 218. Which index range holds the undefined symbols?
__stubs occupies indirect indexes [0, 172) and __got occupies [172, 213). __la_symbol_ptr comes next. What must its reserved1 be, and what is nindirectsyms?
The pattern: the indirect table is one shared, gap-free pool; reserved1 is a running offset, not a count. That is why section order in the file must match the order the linker allocated entries.
Everything here indexes the array from The Symbol Table — same symoff, same nsyms, just ranged and cross-referenced instead of flat.
Next: Relocation Entries — how the linker learns what to patch while the file is still an object, before any of these dynamic tables matter.