Sprint 12: GOT, Stubs, Lazy Pointers
Prerequisites
Sprints 5, 10, 11 — dylibs loaded, layout pass, core reloc application.
Goals
Synthesize __got, __stubs, __stub_helper, __la_symbol_ptr. Wire GOT_LOAD_* / POINTER_TO_GOT relocations to GOT slots. Rewire BRANCH26 to a dylib import through a stub. Classic lazy-binding model; chained fixups land in Sprint 15.5.
Deliverables
1. GOT synthetic section
afs-ld/src/synth/got.rs:
pub struct GotSection {
entries: Vec<GotEntry>,
index: HashMap<SymbolId, usize>,
}
pub struct GotEntry { pub symbol: SymbolId, pub weak_import: bool }
- Lives in
__DATA_CONST,__got, section flagsS_NON_LAZY_SYMBOL_POINTERS(type = 6).reserved1in the section header = the starting indirect-symbol-table index. - 8 bytes per entry, aligned 8.
- GOT entry for a Defined symbol holds that symbol's address directly (no dyld bind).
- GOT entry for a DylibImport is zeroed in the file; dyld binds it at load time via the non-lazy bind stream (Sprint 15).
2. Stubs synthetic section
afs-ld/src/synth/stubs.rs:
ARM64 stub is 12 bytes:
ADRP x16, la_symbol_ptr@PAGE
LDR x16, [x16, la_symbol_ptr@PAGEOFF]
BR x16
- Lives in
__TEXT,__stubs, section flagsS_SYMBOL_STUBS | S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS(type = 8).reserved1= starting indirect-sym index,reserved2= 12 (stub size). - One stub per dylib-imported function whose address is branched to (
BRANCH26target).
3. Lazy symbol pointers
__DATA,__la_symbol_ptr, section flags S_LAZY_SYMBOL_POINTERS (type = 7). Each 8-byte entry is initialized to point at the corresponding __stub_helper entry; at first call, the stub-helper resolves the symbol and patches the lazy pointer.
4. Stub helper
__TEXT,__stub_helper:
Header (24 bytes on arm64):
ADRP x17, __dyld_private@PAGE
ADD x17, x17, __dyld_private@PAGEOFF
STP x16, x17, [sp, #-16]!
ADRP x16, dyld_stub_binder@GOTPAGE
LDR x16, [x16, dyld_stub_binder@GOTPAGEOFF]
BR x16
Per-symbol entry (12 bytes):
LDR w16, =<lazy_bind_offset>
B <header_addr>
Where <lazy_bind_offset> is the offset of this symbol's opcode sequence within the __LINKEDIT lazy-bind stream (Sprint 15 wires this).
Needs ___dyld_private (local anchor) and _dyld_stub_binder (dylib import from libSystem).
5. Binding strategy
_dyld_stub_binderis imported fromlibSystem. Gets a GOT entry; no stub (we take its address directly).___dyld_privateis a 0-filled 8-byte slot in__DATA,__data. Not exported. Dyld uses it as scratch during binding.
6. Reloc rewiring
Relocation application pass (Sprint 11 + this):
GOT_LOAD_PAGE21/GOT_LOAD_PAGEOFF12→ target = GOT slot address.POINTER_TO_GOT→ target = GOT slot address (used for 32-bit pointer-to-GOT references).BRANCH26to a dylib import → target = stub address.BRANCH26to a Defined → target unchanged (direct call).BRANCH26to an Undefined resolved via-undefined dynamic_lookup→ target = stub address.
7. Indirect symbol table
__LINKEDIT indirect-symbol table = list of u32 symbol-table indices, used by dyld to map each stub / lazy pointer / GOT slot entry back to its symbol. Populated here, pointed at by LC_DYSYMTAB.indirectsymoff.
8. Weak-import dylib functions
weak_import symbols get stubs whose lazy binding opcode sequence includes the BIND_SYMBOL_FLAGS_WEAK_IMPORT flag. At runtime, if the symbol is missing, dyld patches the lazy pointer to 0 instead of erroring. The call site must test for null before branching — that's user code's responsibility.
Testing Strategy
- Hello-world staging fixture: a
.othat calls_printf+ references_errno. Produces__stubs,__la_symbol_ptr,__stub_helper,__gotin the expected order and sizes. - Differential: stub/lazy-pointer/GOT layout byte-identical to
ldon the staging fixture. - Reloc-rewire test:
BRANCH26to a dylib-imported function lands in the stub, not in the dylib directly. - Disassembly test:
otool -v -ton__stubsmatches the expected three-instruction sequence for every entry.
Definition of Done
- GOT, stubs, lazy pointers, stub helper all emitted with correct flags and
reserved*fields. - Indirect symbol table populated correctly (Sprint 14 consumes it).
- BRANCH26-to-dylib correctly rewired to stubs.
- Differential pass on the staging hello-world fixture.
View source
| 1 | # Sprint 12: GOT, Stubs, Lazy Pointers |
| 2 | |
| 3 | ## Prerequisites |
| 4 | Sprints 5, 10, 11 — dylibs loaded, layout pass, core reloc application. |
| 5 | |
| 6 | ## Goals |
| 7 | Synthesize `__got`, `__stubs`, `__stub_helper`, `__la_symbol_ptr`. Wire `GOT_LOAD_*` / `POINTER_TO_GOT` relocations to GOT slots. Rewire `BRANCH26` to a dylib import through a stub. Classic lazy-binding model; chained fixups land in Sprint 15.5. |
| 8 | |
| 9 | ## Deliverables |
| 10 | |
| 11 | ### 1. GOT synthetic section |
| 12 | `afs-ld/src/synth/got.rs`: |
| 13 | |
| 14 | ```rust |
| 15 | pub struct GotSection { |
| 16 | entries: Vec<GotEntry>, |
| 17 | index: HashMap<SymbolId, usize>, |
| 18 | } |
| 19 | |
| 20 | pub struct GotEntry { pub symbol: SymbolId, pub weak_import: bool } |
| 21 | ``` |
| 22 | |
| 23 | - Lives in `__DATA_CONST,__got`, section flags `S_NON_LAZY_SYMBOL_POINTERS` (type = 6). `reserved1` in the section header = the starting indirect-symbol-table index. |
| 24 | - 8 bytes per entry, aligned 8. |
| 25 | - GOT entry for a Defined symbol holds that symbol's address directly (no dyld bind). |
| 26 | - GOT entry for a DylibImport is zeroed in the file; dyld binds it at load time via the non-lazy bind stream (Sprint 15). |
| 27 | |
| 28 | ### 2. Stubs synthetic section |
| 29 | `afs-ld/src/synth/stubs.rs`: |
| 30 | |
| 31 | ARM64 stub is 12 bytes: |
| 32 | ``` |
| 33 | ADRP x16, la_symbol_ptr@PAGE |
| 34 | LDR x16, [x16, la_symbol_ptr@PAGEOFF] |
| 35 | BR x16 |
| 36 | ``` |
| 37 | |
| 38 | - Lives in `__TEXT,__stubs`, section flags `S_SYMBOL_STUBS | S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS` (type = 8). `reserved1` = starting indirect-sym index, `reserved2` = 12 (stub size). |
| 39 | - One stub per dylib-imported function whose address is branched to (`BRANCH26` target). |
| 40 | |
| 41 | ### 3. Lazy symbol pointers |
| 42 | `__DATA,__la_symbol_ptr`, section flags `S_LAZY_SYMBOL_POINTERS` (type = 7). Each 8-byte entry is initialized to point at the corresponding `__stub_helper` entry; at first call, the stub-helper resolves the symbol and patches the lazy pointer. |
| 43 | |
| 44 | ### 4. Stub helper |
| 45 | `__TEXT,__stub_helper`: |
| 46 | |
| 47 | Header (24 bytes on arm64): |
| 48 | ``` |
| 49 | ADRP x17, __dyld_private@PAGE |
| 50 | ADD x17, x17, __dyld_private@PAGEOFF |
| 51 | STP x16, x17, [sp, #-16]! |
| 52 | ADRP x16, dyld_stub_binder@GOTPAGE |
| 53 | LDR x16, [x16, dyld_stub_binder@GOTPAGEOFF] |
| 54 | BR x16 |
| 55 | ``` |
| 56 | |
| 57 | Per-symbol entry (12 bytes): |
| 58 | ``` |
| 59 | LDR w16, =<lazy_bind_offset> |
| 60 | B <header_addr> |
| 61 | ``` |
| 62 | |
| 63 | Where `<lazy_bind_offset>` is the offset of this symbol's opcode sequence within the `__LINKEDIT` lazy-bind stream (Sprint 15 wires this). |
| 64 | |
| 65 | Needs `___dyld_private` (local anchor) and `_dyld_stub_binder` (dylib import from `libSystem`). |
| 66 | |
| 67 | ### 5. Binding strategy |
| 68 | - `_dyld_stub_binder` is imported from `libSystem`. Gets a GOT entry; no stub (we take its address directly). |
| 69 | - `___dyld_private` is a 0-filled 8-byte slot in `__DATA,__data`. Not exported. Dyld uses it as scratch during binding. |
| 70 | |
| 71 | ### 6. Reloc rewiring |
| 72 | Relocation application pass (Sprint 11 + this): |
| 73 | |
| 74 | - `GOT_LOAD_PAGE21` / `GOT_LOAD_PAGEOFF12` → target = GOT slot address. |
| 75 | - `POINTER_TO_GOT` → target = GOT slot address (used for 32-bit pointer-to-GOT references). |
| 76 | - `BRANCH26` to a dylib import → target = stub address. |
| 77 | - `BRANCH26` to a Defined → target unchanged (direct call). |
| 78 | - `BRANCH26` to an Undefined resolved via `-undefined dynamic_lookup` → target = stub address. |
| 79 | |
| 80 | ### 7. Indirect symbol table |
| 81 | `__LINKEDIT` indirect-symbol table = list of u32 symbol-table indices, used by dyld to map each stub / lazy pointer / GOT slot entry back to its symbol. Populated here, pointed at by `LC_DYSYMTAB.indirectsymoff`. |
| 82 | |
| 83 | ### 8. Weak-import dylib functions |
| 84 | `weak_import` symbols get stubs whose lazy binding opcode sequence includes the `BIND_SYMBOL_FLAGS_WEAK_IMPORT` flag. At runtime, if the symbol is missing, dyld patches the lazy pointer to 0 instead of erroring. The call site must test for null before branching — that's user code's responsibility. |
| 85 | |
| 86 | ## Testing Strategy |
| 87 | - Hello-world staging fixture: a `.o` that calls `_printf` + references `_errno`. Produces `__stubs`, `__la_symbol_ptr`, `__stub_helper`, `__got` in the expected order and sizes. |
| 88 | - Differential: stub/lazy-pointer/GOT layout byte-identical to `ld` on the staging fixture. |
| 89 | - Reloc-rewire test: `BRANCH26` to a dylib-imported function lands in the stub, not in the dylib directly. |
| 90 | - Disassembly test: `otool -v -t` on `__stubs` matches the expected three-instruction sequence for every entry. |
| 91 | |
| 92 | ## Definition of Done |
| 93 | - GOT, stubs, lazy pointers, stub helper all emitted with correct flags and `reserved*` fields. |
| 94 | - Indirect symbol table populated correctly (Sprint 14 consumes it). |
| 95 | - BRANCH26-to-dylib correctly rewired to stubs. |
| 96 | - Differential pass on the staging hello-world fixture. |