markdown · 4685 bytes Raw Blame History

Sprint 15.5: Chained Fixups (LC_DYLD_CHAINED_FIXUPS)

Prerequisites

Sprint 15 — classic dyld-info format working.

Goals

Emit the modern LC_DYLD_CHAINED_FIXUPS format, introduced in macOS 12 and mandatory on arm64e. LC_DYLD_EXPORTS_TRIE pairs with it for the export side. Coexists with Sprint 15 under -fixup_chains / -no_fixup_chains; chained becomes default once Sprint 27's parity gate clears.

Deliverables

1. Chained-fixups header

__LINKEDIT blob pointed at by LC_DYLD_CHAINED_FIXUPS:

struct dyld_chained_fixups_header {
    uint32 fixups_version;     // 0
    uint32 starts_offset;      // offset of dyld_chained_starts_in_image
    uint32 imports_offset;     // offset of imports table
    uint32 symbols_offset;     // offset of symbol strings
    uint32 imports_count;
    uint32 imports_format;     // DYLD_CHAINED_IMPORT (1), _ADDEND (2), or _ADDEND64 (3)
    uint32 symbols_format;     // 0 = uncompressed
}

2. Per-segment fixup starts

struct dyld_chained_starts_in_image {
    uint32 seg_count;
    uint32 seg_info_offset[seg_count];   // 0 = no fixups in this segment
}

struct dyld_chained_starts_in_segment {
    uint32 size;
    uint16 page_size;               // 0x4000 on arm64 Apple Silicon
    uint16 pointer_format;          // DYLD_CHAINED_PTR_64 (2) or _64_OFFSET (6)
    uint64 segment_offset;
    uint32 max_valid_pointer;       // 0 for 64-bit
    uint16 page_count;
    uint16 page_start[page_count];   // offset of first chain within each page (0xFFFF = no chain)
}

3. Pointer formats

arm64 uses DYLD_CHAINED_PTR_64 (plain 64-bit) or DYLD_CHAINED_PTR_64_OFFSET (offsets from image base). arm64e uses DYLD_CHAINED_PTR_ARM64E (with auth bits); skip arm64e for now. Each chained pointer is a 64-bit word with fields:

bind:  31-bit ordinal | 1-bit bind | 8-bit next | 1-bit auth=0
rebase: 36-bit target | 19-bit high | 1-bit bind=0 | 8-bit next | 1-bit auth=0

The next field is the distance in 4-byte units from this fixup to the next one within the page (0 = end of chain). Rebuilding chains after layout is the bulk of this sprint.

4. Imports table

One entry per imported symbol. DYLD_CHAINED_IMPORT format:

uint32 lib_ordinal : 8;        // dylib ordinal
uint32 weak_import : 1;
uint32 name_offset : 23;       // into the symbol strings blob

_ADDEND (32-bit) and _ADDEND64 (64-bit) formats add an explicit addend — we pick the smallest that fits our inputs (ADDEND64 only if any addend exceeds i32 range).

5. Chain construction

Walk every output fixup (rebase or bind), grouped by segment and page. Within a page, chain them in ascending file offset; the next field of each points at the next. Pages with no fixups set page_start = 0xFFFF. Validate that no chain ever crosses a page boundary.

6. Exports trie → LC_DYLD_EXPORTS_TRIE

The export trie is unchanged from Sprint 15's format. In chained-fixups mode the trie lives under LC_DYLD_EXPORTS_TRIE instead of inside LC_DYLD_INFO_ONLY.

7. CLI flag wiring

-fixup_chains forces chained, -no_fixup_chains forces classic. Default policy:

  • If -platform_version macos minimum ≥ 12.0: chained.
  • Otherwise: classic.

Sprint 19's CLI sprint consumes these flags; this sprint just implements both paths.

8. Removing __stub_helper under chained

Chained fixups don't use lazy binding — __stub_helper is unnecessary, __la_symbol_ptr becomes an ordinary bind slot in __DATA or __AUTH_DATA. Under chained mode the writer skips emitting __stub_helper and wires BRANCH26 through a different stub that loads directly from the bind slot.

Modified ARM64 stub for chained:

ADRP x16, _symbol@PAGE
LDR  x16, [x16, _symbol@PAGEOFF]
BR   x16

Where _symbol@PAGE/PAGEOFF resolves to the bind slot in __DATA. Dyld has already bound it by the time the stub runs.

Testing Strategy

  • Parity vs ld -fixup_chains on staging fixtures: byte-identical chain layout and imports table.
  • Parity vs ld -no_fixup_chains: retains Sprint 15 byte-identical output.
  • Page-boundary test: a fixup at the last 4 bytes of a page followed by one at byte 0 of the next page — each in its own chain, both reachable.
  • Default-policy test: -platform_version macos 11.0 → classic, -platform_version macos 12.0 → chained.
  • Runtime test: binaries linked both ways load and execute correctly on an M-series Mac.

Definition of Done

  • Both -fixup_chains and -no_fixup_chains produce runnable binaries.
  • Chain layout byte-identical to ld on 10+ staging fixtures.
  • Default format switches on -platform_version.
  • __stub_helper correctly omitted in chained mode.
View source
1 # Sprint 15.5: Chained Fixups (LC_DYLD_CHAINED_FIXUPS)
2
3 ## Prerequisites
4 Sprint 15 — classic dyld-info format working.
5
6 ## Goals
7 Emit the modern `LC_DYLD_CHAINED_FIXUPS` format, introduced in macOS 12 and mandatory on arm64e. `LC_DYLD_EXPORTS_TRIE` pairs with it for the export side. Coexists with Sprint 15 under `-fixup_chains` / `-no_fixup_chains`; chained becomes default once Sprint 27's parity gate clears.
8
9 ## Deliverables
10
11 ### 1. Chained-fixups header
12 `__LINKEDIT` blob pointed at by `LC_DYLD_CHAINED_FIXUPS`:
13
14 ```
15 struct dyld_chained_fixups_header {
16 uint32 fixups_version; // 0
17 uint32 starts_offset; // offset of dyld_chained_starts_in_image
18 uint32 imports_offset; // offset of imports table
19 uint32 symbols_offset; // offset of symbol strings
20 uint32 imports_count;
21 uint32 imports_format; // DYLD_CHAINED_IMPORT (1), _ADDEND (2), or _ADDEND64 (3)
22 uint32 symbols_format; // 0 = uncompressed
23 }
24 ```
25
26 ### 2. Per-segment fixup starts
27 ```
28 struct dyld_chained_starts_in_image {
29 uint32 seg_count;
30 uint32 seg_info_offset[seg_count]; // 0 = no fixups in this segment
31 }
32
33 struct dyld_chained_starts_in_segment {
34 uint32 size;
35 uint16 page_size; // 0x4000 on arm64 Apple Silicon
36 uint16 pointer_format; // DYLD_CHAINED_PTR_64 (2) or _64_OFFSET (6)
37 uint64 segment_offset;
38 uint32 max_valid_pointer; // 0 for 64-bit
39 uint16 page_count;
40 uint16 page_start[page_count]; // offset of first chain within each page (0xFFFF = no chain)
41 }
42 ```
43
44 ### 3. Pointer formats
45 arm64 uses `DYLD_CHAINED_PTR_64` (plain 64-bit) or `DYLD_CHAINED_PTR_64_OFFSET` (offsets from image base). arm64e uses `DYLD_CHAINED_PTR_ARM64E` (with auth bits); skip arm64e for now. Each chained pointer is a 64-bit word with fields:
46
47 ```
48 bind: 31-bit ordinal | 1-bit bind | 8-bit next | 1-bit auth=0
49 rebase: 36-bit target | 19-bit high | 1-bit bind=0 | 8-bit next | 1-bit auth=0
50 ```
51
52 The `next` field is the distance in 4-byte units from this fixup to the next one within the page (0 = end of chain). Rebuilding chains after layout is the bulk of this sprint.
53
54 ### 4. Imports table
55 One entry per imported symbol. `DYLD_CHAINED_IMPORT` format:
56 ```
57 uint32 lib_ordinal : 8; // dylib ordinal
58 uint32 weak_import : 1;
59 uint32 name_offset : 23; // into the symbol strings blob
60 ```
61
62 `_ADDEND` (32-bit) and `_ADDEND64` (64-bit) formats add an explicit addend — we pick the smallest that fits our inputs (ADDEND64 only if any addend exceeds i32 range).
63
64 ### 5. Chain construction
65 Walk every output fixup (rebase or bind), grouped by segment and page. Within a page, chain them in ascending file offset; the `next` field of each points at the next. Pages with no fixups set `page_start = 0xFFFF`. Validate that no chain ever crosses a page boundary.
66
67 ### 6. Exports trie → LC_DYLD_EXPORTS_TRIE
68 The export trie is unchanged from Sprint 15's format. In chained-fixups mode the trie lives under `LC_DYLD_EXPORTS_TRIE` instead of inside `LC_DYLD_INFO_ONLY`.
69
70 ### 7. CLI flag wiring
71 `-fixup_chains` forces chained, `-no_fixup_chains` forces classic. Default policy:
72 - If `-platform_version macos` minimum ≥ 12.0: chained.
73 - Otherwise: classic.
74
75 Sprint 19's CLI sprint consumes these flags; this sprint just implements both paths.
76
77 ### 8. Removing `__stub_helper` under chained
78 Chained fixups don't use lazy binding — `__stub_helper` is unnecessary, `__la_symbol_ptr` becomes an ordinary bind slot in `__DATA` or `__AUTH_DATA`. Under chained mode the writer skips emitting `__stub_helper` and wires `BRANCH26` through a different stub that loads directly from the bind slot.
79
80 Modified ARM64 stub for chained:
81 ```
82 ADRP x16, _symbol@PAGE
83 LDR x16, [x16, _symbol@PAGEOFF]
84 BR x16
85 ```
86
87 Where `_symbol@PAGE/PAGEOFF` resolves to the bind slot in `__DATA`. Dyld has already bound it by the time the stub runs.
88
89 ## Testing Strategy
90 - Parity vs `ld -fixup_chains` on staging fixtures: byte-identical chain layout and imports table.
91 - Parity vs `ld -no_fixup_chains`: retains Sprint 15 byte-identical output.
92 - Page-boundary test: a fixup at the last 4 bytes of a page followed by one at byte 0 of the next page — each in its own chain, both reachable.
93 - Default-policy test: `-platform_version macos 11.0` → classic, `-platform_version macos 12.0` → chained.
94 - Runtime test: binaries linked both ways load and execute correctly on an M-series Mac.
95
96 ## Definition of Done
97 - Both `-fixup_chains` and `-no_fixup_chains` produce runnable binaries.
98 - Chain layout byte-identical to `ld` on 10+ staging fixtures.
99 - Default format switches on `-platform_version`.
100 - `__stub_helper` correctly omitted in chained mode.