Comparing changes

Choose two branches to see what's changed or to start a new pull request.

base: ci-clippy-fixes-20260416
compare: runtime-hello-parity
Create pull request
Able to merge. These branches can be automatically merged.
9 commits 13 files changed 1 contributor

Commits on runtime-hello-parity

src/layout.rsmodified
@@ -10,7 +10,7 @@ use crate::input::ObjectFile;
1010
 use crate::macho::constants::SG_READ_ONLY;
1111
 use crate::resolve::InputId;
1212
 use crate::section::{
13
-    is_zerofill, OutputAtom, OutputSection, OutputSectionId, OutputSegment, Prot,
13
+    is_zerofill, InputSection, OutputAtom, OutputSection, OutputSectionId, OutputSegment, Prot,
1414
 };
1515
 use crate::synth::SyntheticPlan;
1616
 use crate::OutputKind;
@@ -31,6 +31,8 @@ const DYLIB_SEGMENTS: [&str; 4] = ["__TEXT", "__DATA_CONST", "__DATA", "__LINKED
3131
 pub struct LayoutInput<'a> {
3232
     pub id: InputId,
3333
     pub object: &'a ObjectFile,
34
+    pub load_order: usize,
35
+    pub archive_member_offset: Option<u32>,
3436
 }
3537
 
3638
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -46,6 +48,22 @@ struct SectionKey {
4648
     name: String,
4749
 }
4850
 
51
+fn output_section_key(input_section: &InputSection) -> SectionKey {
52
+    match (
53
+        input_section.segname.as_str(),
54
+        input_section.sectname.as_str(),
55
+    ) {
56
+        ("__DATA", "__const") => SectionKey {
57
+            segment: "__DATA_CONST".to_string(),
58
+            name: "__const".to_string(),
59
+        },
60
+        _ => SectionKey {
61
+            segment: input_section.segname.clone(),
62
+            name: input_section.sectname.clone(),
63
+        },
64
+    }
65
+}
66
+
4967
 impl Layout {
5068
     pub fn empty(kind: OutputKind, header_size: u64) -> Self {
5169
         Self::build(kind, &[], &AtomTable::new(), header_size)
@@ -67,34 +85,30 @@ impl Layout {
6785
         header_size: u64,
6886
         synthetic_plan: Option<&SyntheticPlan>,
6987
     ) -> Self {
70
-        let input_map: HashMap<InputId, &ObjectFile> = inputs
71
-            .iter()
72
-            .map(|input| (input.id, input.object))
73
-            .collect();
88
+        let input_map: HashMap<InputId, LayoutInput<'_>> =
89
+            inputs.iter().map(|input| (input.id, *input)).collect();
7490
 
7591
         let mut sections: Vec<OutputSection> = Vec::new();
7692
         let mut section_index: HashMap<SectionKey, usize> = HashMap::new();
7793
 
7894
         for (atom_id, atom) in atoms.iter() {
79
-            let obj = input_map
95
+            let input = input_map
8096
                 .get(&atom.origin)
8197
                 .unwrap_or_else(|| panic!("missing object for input {:?}", atom.origin));
82
-            let input_section = obj
98
+            let input_section = input
99
+                .object
83100
                 .sections
84101
                 .get((atom.input_section as usize).saturating_sub(1))
85102
                 .unwrap_or_else(|| {
86103
                     panic!(
87104
                         "input {} section {} missing for atom {:?}",
88
-                        obj.path.display(),
105
+                        input.object.path.display(),
89106
                         atom.input_section,
90107
                         atom_id
91108
                     )
92109
                 });
93110
 
94
-            let key = SectionKey {
95
-                segment: input_section.segname.clone(),
96
-                name: input_section.sectname.clone(),
97
-            };
111
+            let key = output_section_key(input_section);
98112
             let idx = match section_index.get(&key) {
99113
                 Some(&idx) => idx,
100114
                 None => {
@@ -103,7 +117,10 @@ impl Layout {
103117
                         segment: key.segment.clone(),
104118
                         name: key.name.clone(),
105119
                         kind: input_section.kind,
106
-                        align_pow2: input_section.align_pow2.min(u8::MAX as u32) as u8,
120
+                        align_pow2: normalize_output_alignment(
121
+                            input_section.kind,
122
+                            input_section.align_pow2.min(u8::MAX as u32) as u8,
123
+                        ),
107124
                         flags: input_section.flags,
108125
                         reserved1: input_section.reserved1,
109126
                         reserved2: input_section.reserved2,
@@ -121,7 +138,8 @@ impl Layout {
121138
             };
122139
 
123140
             let out = &mut sections[idx];
124
-            out.align_pow2 = out.align_pow2.max(atom.align_pow2);
141
+            out.align_pow2 =
142
+                normalize_output_alignment(out.kind, out.align_pow2.max(atom.align_pow2));
125143
             out.atoms.push(OutputAtom {
126144
                 atom: atom_id,
127145
                 offset: 0,
@@ -156,8 +174,22 @@ impl Layout {
156174
             section.atoms.sort_by(|a, b| {
157175
                 let lhs = atoms.get(a.atom);
158176
                 let rhs = atoms.get(b.atom);
159
-                lhs.origin
160
-                    .cmp(&rhs.origin)
177
+                let lhs_input = input_map
178
+                    .get(&lhs.origin)
179
+                    .unwrap_or_else(|| panic!("missing object for input {:?}", lhs.origin));
180
+                let rhs_input = input_map
181
+                    .get(&rhs.origin)
182
+                    .unwrap_or_else(|| panic!("missing object for input {:?}", rhs.origin));
183
+                lhs_input
184
+                    .load_order
185
+                    .cmp(&rhs_input.load_order)
186
+                    .then_with(|| {
187
+                        lhs_input
188
+                            .archive_member_offset
189
+                            .unwrap_or(0)
190
+                            .cmp(&rhs_input.archive_member_offset.unwrap_or(0))
191
+                    })
192
+                    .then_with(|| lhs.origin.cmp(&rhs.origin))
161193
                     .then_with(|| lhs.input_offset.cmp(&rhs.input_offset))
162194
                     .then_with(|| a.atom.cmp(&b.atom))
163195
             });
@@ -357,7 +389,8 @@ impl Layout {
357389
 fn merge_synthetic_section(existing: &mut OutputSection, synthetic: OutputSection) {
358390
     debug_assert_eq!(existing.segment, synthetic.segment);
359391
     debug_assert_eq!(existing.name, synthetic.name);
360
-    existing.align_pow2 = existing.align_pow2.max(synthetic.align_pow2);
392
+    existing.align_pow2 =
393
+        normalize_output_alignment(existing.kind, existing.align_pow2.max(synthetic.align_pow2));
361394
     existing.flags = synthetic.flags;
362395
     existing.reserved1 = synthetic.reserved1;
363396
     existing.reserved2 = synthetic.reserved2;
@@ -369,6 +402,17 @@ fn merge_synthetic_section(existing: &mut OutputSection, synthetic: OutputSectio
369402
     }
370403
 }
371404
 
405
+fn normalize_output_alignment(kind: crate::section::SectionKind, align_pow2: u8) -> u8 {
406
+    match kind {
407
+        crate::section::SectionKind::ThreadLocalRegular
408
+        | crate::section::SectionKind::ThreadLocalZeroFill
409
+        | crate::section::SectionKind::ThreadLocalVariables
410
+        | crate::section::SectionKind::ThreadLocalVariablePointers
411
+        | crate::section::SectionKind::ThreadLocalInitPointers => align_pow2.max(3),
412
+        _ => align_pow2,
413
+    }
414
+}
415
+
372416
 fn build_segments(kind: OutputKind, sections: &[OutputSection]) -> Vec<OutputSegment> {
373417
     let names: &[&str] = match kind {
374418
         OutputKind::Executable => &EXEC_SEGMENTS,
@@ -460,6 +504,7 @@ fn section_rank(segment: &str, section: &str) -> usize {
460504
             "__thread_ptrs",
461505
             "__thread_data",
462506
             "__thread_bss",
507
+            "__common",
463508
             "__bss",
464509
         ],
465510
         "__LINKEDIT" => &[],
@@ -647,6 +692,8 @@ mod tests {
647692
             &[LayoutInput {
648693
                 id: InputId(0),
649694
                 object: &object,
695
+                load_order: 0,
696
+                archive_member_offset: None,
650697
             }],
651698
             &atoms,
652699
             0x200,
@@ -668,6 +715,68 @@ mod tests {
668715
         );
669716
     }
670717
 
718
+    #[test]
719
+    fn layout_promotes_data_const_into_data_const_segment() {
720
+        let object = ObjectFile {
721
+            path: PathBuf::from("/tmp/layout-const.o"),
722
+            header: MachHeader64 {
723
+                magic: MH_MAGIC_64,
724
+                cputype: CPU_TYPE_ARM64,
725
+                cpusubtype: CPU_SUBTYPE_ARM64_ALL,
726
+                filetype: MH_OBJECT,
727
+                ncmds: 0,
728
+                sizeofcmds: 0,
729
+                flags: 0,
730
+                reserved: 0,
731
+            },
732
+            commands: Vec::new(),
733
+            sections: vec![input_section(
734
+                "__DATA",
735
+                "__const",
736
+                SectionKind::ConstData,
737
+                3,
738
+                S_REGULAR,
739
+            )],
740
+            symbols: Vec::new(),
741
+            strings: crate::string_table::StringTable::from_bytes(vec![0]),
742
+            symtab: None,
743
+            dysymtab: None,
744
+            data_in_code: Vec::new(),
745
+        };
746
+
747
+        let mut atoms = AtomTable::new();
748
+        atoms.push(atom(
749
+            InputId(0),
750
+            1,
751
+            AtomSection::ConstData,
752
+            0,
753
+            16,
754
+            3,
755
+            vec![1; 16],
756
+        ));
757
+
758
+        let layout = Layout::build(
759
+            OutputKind::Executable,
760
+            &[LayoutInput {
761
+                id: InputId(0),
762
+                object: &object,
763
+                load_order: 0,
764
+                archive_member_offset: None,
765
+            }],
766
+            &atoms,
767
+            0x200,
768
+        );
769
+
770
+        assert!(layout
771
+            .sections
772
+            .iter()
773
+            .any(|section| section.segment == "__DATA_CONST" && section.name == "__const"));
774
+        assert!(!layout
775
+            .sections
776
+            .iter()
777
+            .any(|section| section.segment == "__DATA" && section.name == "__const"));
778
+    }
779
+
671780
     #[test]
672781
     fn executable_layout_has_pagezero_and_zerofill_has_no_file_offset() {
673782
         let object = ObjectFile {
@@ -713,6 +822,8 @@ mod tests {
713822
             &[LayoutInput {
714823
                 id: InputId(0),
715824
                 object: &object,
825
+                load_order: 0,
826
+                archive_member_offset: None,
716827
             }],
717828
             &atoms,
718829
             0x300,
@@ -727,6 +838,72 @@ mod tests {
727838
         assert!(bss.addr >= EXECUTABLE_TEXT_BASE + PAGE_SIZE);
728839
     }
729840
 
841
+    #[test]
842
+    fn layout_orders_common_before_bss() {
843
+        let object = ObjectFile {
844
+            path: PathBuf::from("/tmp/layout-common-bss.o"),
845
+            header: MachHeader64 {
846
+                magic: MH_MAGIC_64,
847
+                cputype: CPU_TYPE_ARM64,
848
+                cpusubtype: CPU_SUBTYPE_ARM64_ALL,
849
+                filetype: MH_OBJECT,
850
+                ncmds: 0,
851
+                sizeofcmds: 0,
852
+                flags: 0,
853
+                reserved: 0,
854
+            },
855
+            commands: Vec::new(),
856
+            sections: vec![
857
+                input_section("__DATA", "__common", SectionKind::ZeroFill, 3, S_ZEROFILL),
858
+                input_section("__DATA", "__bss", SectionKind::ZeroFill, 3, S_ZEROFILL),
859
+            ],
860
+            symbols: Vec::new(),
861
+            strings: crate::string_table::StringTable::from_bytes(vec![0]),
862
+            symtab: None,
863
+            dysymtab: None,
864
+            data_in_code: Vec::new(),
865
+        };
866
+
867
+        let mut atoms = AtomTable::new();
868
+        atoms.push(atom(
869
+            InputId(0),
870
+            1,
871
+            AtomSection::ZeroFill,
872
+            0,
873
+            16,
874
+            3,
875
+            Vec::new(),
876
+        ));
877
+        atoms.push(atom(
878
+            InputId(0),
879
+            2,
880
+            AtomSection::ZeroFill,
881
+            0,
882
+            32,
883
+            3,
884
+            Vec::new(),
885
+        ));
886
+
887
+        let layout = Layout::build(
888
+            OutputKind::Executable,
889
+            &[LayoutInput {
890
+                id: InputId(0),
891
+                object: &object,
892
+                load_order: 0,
893
+                archive_member_offset: None,
894
+            }],
895
+            &atoms,
896
+            0x200,
897
+        );
898
+
899
+        let names: Vec<(&str, &str)> = layout
900
+            .sections
901
+            .iter()
902
+            .map(|section| (section.segment.as_str(), section.name.as_str()))
903
+            .collect();
904
+        assert_eq!(names, vec![("__DATA", "__common"), ("__DATA", "__bss")]);
905
+    }
906
+
730907
     #[test]
731908
     fn file_backed_segments_round_to_page_boundaries() {
732909
         let object = ObjectFile {
@@ -776,6 +953,8 @@ mod tests {
776953
             &[LayoutInput {
777954
                 id: InputId(0),
778955
                 object: &object,
956
+                load_order: 0,
957
+                archive_member_offset: None,
779958
             }],
780959
             &atoms,
781960
             0x200,
@@ -866,6 +1045,8 @@ mod tests {
8661045
             &[LayoutInput {
8671046
                 id: InputId(0),
8681047
                 object: &object,
1048
+                load_order: 0,
1049
+                archive_member_offset: None,
8691050
             }],
8701051
             &atoms,
8711052
             0x200,
@@ -990,6 +1171,8 @@ mod tests {
9901171
             &[LayoutInput {
9911172
                 id: InputId(0),
9921173
                 object: &object,
1174
+                load_order: 0,
1175
+                archive_member_offset: None,
9931176
             }],
9941177
             &atoms,
9951178
             0x200,
@@ -1044,6 +1227,8 @@ mod tests {
10441227
             &[LayoutInput {
10451228
                 id: InputId(0),
10461229
                 object: &object,
1230
+                load_order: 0,
1231
+                archive_member_offset: None,
10471232
             }],
10481233
             &atoms,
10491234
             0x200,
src/lib.rsmodified
@@ -209,8 +209,8 @@ impl Linker {
209209
         }
210210
 
211211
         let mut inputs = Inputs::new();
212
-        for path in &opts.inputs {
213
-            register_input(&mut inputs, path)?;
212
+        for (load_order, path) in opts.inputs.iter().enumerate() {
213
+            register_input(&mut inputs, path, load_order)?;
214214
         }
215215
 
216216
         let mut sym_table = SymbolTable::new();
@@ -261,7 +261,15 @@ impl Linker {
261261
 
262262
         let layout_inputs: Vec<LayoutInput<'_>> = objects
263263
             .iter()
264
-            .map(|(id, object)| LayoutInput { id: *id, object })
264
+            .map(|(id, object)| {
265
+                let input = inputs.object(*id);
266
+                LayoutInput {
267
+                    id: *id,
268
+                    object,
269
+                    load_order: input.load_order,
270
+                    archive_member_offset: input.archive_member_offset,
271
+                }
272
+            })
265273
             .collect();
266274
         let mut dylib_loads = Vec::new();
267275
         let mut seen_ordinals = std::collections::BTreeSet::new();
@@ -357,11 +365,15 @@ fn default_output_path(opts: &LinkOptions) -> PathBuf {
357365
         .unwrap_or_else(|| PathBuf::from("a.out"))
358366
 }
359367
 
360
-fn register_input(inputs: &mut Inputs, path: &std::path::Path) -> Result<(), LinkError> {
368
+fn register_input(
369
+    inputs: &mut Inputs,
370
+    path: &std::path::Path,
371
+    load_order: usize,
372
+) -> Result<(), LinkError> {
361373
     let bytes = fs::read(path)?;
362374
     match path.extension().and_then(|ext| ext.to_str()) {
363375
         Some("a") => {
364
-            let _ = inputs.add_archive(path.to_path_buf(), bytes)?;
376
+            let _ = inputs.add_archive(path.to_path_buf(), bytes, load_order)?;
365377
         }
366378
         Some("dylib") => {
367379
             let _ = inputs.add_dylib(path.to_path_buf(), bytes)?;
@@ -410,7 +422,7 @@ fn register_input(inputs: &mut Inputs, path: &std::path::Path) -> Result<(), Lin
410422
             }
411423
         }
412424
         _ => {
413
-            let _ = inputs.add_object(path.to_path_buf(), bytes)?;
425
+            let _ = inputs.add_object(path.to_path_buf(), bytes, load_order)?;
414426
         }
415427
     }
416428
     Ok(())
@@ -450,6 +462,7 @@ fn resolve_entry_point(
450462
 
451463
 fn symbol_defined(sym_table: &SymbolTable, name: &str) -> bool {
452464
     sym_table.iter().any(|(_, symbol)| {
453
-        sym_table.interner.resolve(symbol.name()) == name && matches!(symbol, Symbol::Defined { .. })
465
+        sym_table.interner.resolve(symbol.name()) == name
466
+            && matches!(symbol, Symbol::Defined { .. })
454467
     })
455468
 }
src/macho/constants.rsmodified
@@ -115,6 +115,10 @@ pub const N_WEAK_DEF: u16 = 0x0080;
115115
 /// `alt_entries` during atomization.
116116
 pub const N_ALT_ENTRY: u16 = 0x0200;
117117
 
118
+// Indirect symbol table sentinels (<mach-o/loader.h>)
119
+pub const INDIRECT_SYMBOL_LOCAL: u32 = 0x8000_0000;
120
+pub const INDIRECT_SYMBOL_ABS: u32 = 0x4000_0000;
121
+
118122
 // ARM64 relocation kinds
119123
 pub const ARM64_RELOC_UNSIGNED: u8 = 0;
120124
 pub const ARM64_RELOC_SUBTRACTOR: u8 = 1;
src/macho/writer.rsmodified
@@ -740,7 +740,6 @@ fn build_linkedit_plan(
740740
         .map(|record| (record.symbol, record))
741741
         .collect();
742742
     let symbol_plan = build_output_symbols(layout, kind, opts.strip_locals, inputs, &imports)?;
743
-
744743
     let mut symtab_bytes = Vec::new();
745744
     write_nlist_table(&symbol_plan.symbols, &mut symtab_bytes);
746745
 
@@ -750,30 +749,25 @@ fn build_linkedit_plan(
750749
         &mut indirect_symbols,
751750
         &mut indirect_starts,
752751
         ("__TEXT", "__stubs"),
753
-        synthetic_plan
754
-            .stubs
755
-            .entries
756
-            .iter()
757
-            .map(|entry| entry.symbol),
758
-        &symbol_plan.symbol_indices,
752
+        synthetic_plan.stubs.entries.iter().map(|entry| {
753
+            indirect_symbol_index(entry.symbol, &import_lookup, &symbol_plan.symbol_indices)
754
+        }),
759755
     );
760756
     push_indirect_section(
761757
         &mut indirect_symbols,
762758
         &mut indirect_starts,
763759
         ("__DATA_CONST", "__got"),
764
-        synthetic_plan.got.entries.iter().map(|entry| entry.symbol),
765
-        &symbol_plan.symbol_indices,
760
+        synthetic_plan.got.entries.iter().map(|entry| {
761
+            indirect_symbol_index(entry.symbol, &import_lookup, &symbol_plan.symbol_indices)
762
+        }),
766763
     );
767764
     push_indirect_section(
768765
         &mut indirect_symbols,
769766
         &mut indirect_starts,
770767
         ("__DATA", "__la_symbol_ptr"),
771
-        synthetic_plan
772
-            .lazy_pointers
773
-            .entries
774
-            .iter()
775
-            .map(|entry| entry.symbol),
776
-        &symbol_plan.symbol_indices,
768
+        synthetic_plan.lazy_pointers.entries.iter().map(|entry| {
769
+            indirect_symbol_index(entry.symbol, &import_lookup, &symbol_plan.symbol_indices)
770
+        }),
777771
     );
778772
 
779773
     let mut indirect_bytes = Vec::with_capacity(indirect_symbols.len() * 4);
@@ -989,6 +983,11 @@ fn collect_rebase_sites(
989983
     inputs: LinkEditInputs<'_>,
990984
 ) -> Result<Vec<RebaseSite>, WriteError> {
991985
     let mut sites = collect_lazy_pointer_rebase_sites(layout, synthetic_plan)?;
986
+    sites.extend(collect_local_got_rebase_sites(
987
+        layout,
988
+        synthetic_plan,
989
+        inputs.0.sym_table,
990
+    )?);
992991
     let mut reloc_cache: HashMap<(InputId, u8), Vec<Reloc>> = HashMap::new();
993992
     let input_map: HashMap<InputId, &ObjectFile> = inputs
994993
         .0
@@ -1082,6 +1081,38 @@ fn collect_lazy_pointer_rebase_sites(
10821081
         .collect())
10831082
 }
10841083
 
1084
+fn collect_local_got_rebase_sites(
1085
+    layout: &Layout,
1086
+    synthetic_plan: &SyntheticPlan,
1087
+    sym_table: &SymbolTable,
1088
+) -> Result<Vec<RebaseSite>, WriteError> {
1089
+    if synthetic_plan.got.entries.is_empty() {
1090
+        return Ok(Vec::new());
1091
+    }
1092
+
1093
+    let segment_index = segment_index(layout, "__DATA_CONST")?;
1094
+    let segment = layout
1095
+        .segment("__DATA_CONST")
1096
+        .ok_or(WriteError::MissingSegment("__DATA_CONST"))?;
1097
+    let section = layout
1098
+        .sections
1099
+        .iter()
1100
+        .find(|section| section.segment == "__DATA_CONST" && section.name == "__got")
1101
+        .ok_or(WriteError::MissingSegment("__DATA_CONST"))?;
1102
+
1103
+    Ok(synthetic_plan
1104
+        .got
1105
+        .entries
1106
+        .iter()
1107
+        .enumerate()
1108
+        .filter(|(_, entry)| !matches!(sym_table.get(entry.symbol), Symbol::DylibImport { .. }))
1109
+        .map(|(idx, _)| RebaseSite {
1110
+            segment_index,
1111
+            segment_offset: section.addr + (idx as u64) * 8 - segment.vm_addr,
1112
+        })
1113
+        .collect())
1114
+}
1115
+
10851116
 fn relocs_for_rebase<'a>(
10861117
     relocs: &'a [Reloc],
10871118
     atom: &crate::atom::Atom,
@@ -1105,15 +1136,20 @@ fn reloc_needs_rebase(obj: &ObjectFile, reloc: Reloc, sym_table: &SymbolTable) -
11051136
 
11061137
     match reloc.referent {
11071138
         Referent::Section(_) => true,
1108
-        Referent::Symbol(_) => match symbol_referent_id(obj, reloc.referent, sym_table) {
1109
-            Some(symbol_id) => match sym_table.get(symbol_id) {
1110
-                Symbol::DylibImport { .. } => false,
1111
-                Symbol::Defined { atom, .. } => atom.0 != 0,
1112
-                Symbol::Common { .. } => true,
1113
-                _ => false,
1114
-            },
1115
-            None => false,
1116
-        },
1139
+        Referent::Symbol(sym_idx) => {
1140
+            let Some(input_sym) = obj.symbols.get(sym_idx as usize) else {
1141
+                return false;
1142
+            };
1143
+            match symbol_referent_id(obj, reloc.referent, sym_table) {
1144
+                Some(symbol_id) => match sym_table.get(symbol_id) {
1145
+                    Symbol::DylibImport { .. } => false,
1146
+                    Symbol::Defined { atom, .. } => atom.0 != 0,
1147
+                    Symbol::Common { .. } => true,
1148
+                    _ => false,
1149
+                },
1150
+                None => matches!(input_sym.kind(), SymKind::Sect),
1151
+            }
1152
+        }
11171153
     }
11181154
 }
11191155
 
@@ -1227,6 +1263,7 @@ fn build_data_in_code(
12271263
         kind: u16,
12281264
     }
12291265
 
1266
+    let atoms_by_input_section = atom_table.by_input_section();
12301267
     let mut remapped = Vec::new();
12311268
     for (input_order, input) in inputs.iter().enumerate() {
12321269
         for (input_entry_index, entry) in input.object.data_in_code.iter().copied().enumerate() {
@@ -1234,6 +1271,7 @@ fn build_data_in_code(
12341271
                 remap_data_in_code_to_section(input.object, entry)?;
12351272
             let (atom_id, atom_delta) = find_containing_atom_range(
12361273
                 atom_table,
1274
+                &atoms_by_input_section,
12371275
                 input.id,
12381276
                 section_index,
12391277
                 section_relative,
@@ -1373,7 +1411,7 @@ fn collect_imports(
13731411
             ..
13741412
         } = symbol
13751413
         else {
1376
-            return Err(WriteError::ImportSymbolWrongKind(id));
1414
+            continue;
13771415
         };
13781416
         out.push(ImportSymbolRecord {
13791417
             symbol: id,
@@ -1395,6 +1433,7 @@ fn build_output_symbols(
13951433
 ) -> Result<SymbolTablePlan, WriteError> {
13961434
     let sym_table = inputs.0.sym_table;
13971435
     let atom_sections = atom_section_ordinals(layout);
1436
+    let atoms_by_input_section = inputs.0.atom_table.by_input_section();
13981437
     let image_base = layout.segment("__TEXT").map(|seg| seg.vm_addr).unwrap_or(0);
13991438
     let mut locals = Vec::new();
14001439
     let mut external_defineds = Vec::new();
@@ -1420,6 +1459,7 @@ fn build_output_symbols(
14201459
         collect_local_symbols(
14211460
             layout,
14221461
             inputs.0.atom_table,
1462
+            &atoms_by_input_section,
14231463
             &atom_sections,
14241464
             input.id,
14251465
             input.object,
@@ -1427,6 +1467,7 @@ fn build_output_symbols(
14271467
         )?;
14281468
     }
14291469
     collect_synthetic_local_symbols(layout, inputs.0.synthetic_plan, &mut locals)?;
1470
+    sort_local_symbols(&mut locals);
14301471
 
14311472
     for (symbol_id, symbol) in sym_table.iter() {
14321473
         let Symbol::Defined {
@@ -1573,6 +1614,16 @@ fn build_output_symbols(
15731614
     })
15741615
 }
15751616
 
1617
+fn sort_local_symbols(locals: &mut [OutputSymbolSpec]) {
1618
+    locals.sort_by(|lhs, rhs| {
1619
+        lhs.n_sect
1620
+            .cmp(&rhs.n_sect)
1621
+            .then_with(|| lhs.n_value.cmp(&rhs.n_value))
1622
+            .then_with(|| lhs.n_type.cmp(&rhs.n_type))
1623
+            .then_with(|| lhs.name.cmp(&rhs.name))
1624
+    });
1625
+}
1626
+
15761627
 fn collect_synthetic_local_symbols(
15771628
     layout: &Layout,
15781629
     synthetic_plan: &SyntheticPlan,
@@ -1606,6 +1657,7 @@ fn collect_synthetic_local_symbols(
16061657
 fn collect_local_symbols(
16071658
     layout: &Layout,
16081659
     atom_table: &AtomTable,
1660
+    atoms_by_input_section: &HashMap<(InputId, u8), Vec<crate::resolve::AtomId>>,
16091661
     atom_sections: &HashMap<crate::resolve::AtomId, u8>,
16101662
     input_id: InputId,
16111663
     object: &ObjectFile,
@@ -1628,9 +1680,14 @@ fn collect_local_symbols(
16281680
                     .section_for_symbol(input_sym)
16291681
                     .expect("section symbol without section");
16301682
                 let offset = input_sym.value().saturating_sub(section.addr) as u32;
1631
-                let (atom_id, delta) =
1632
-                    find_containing_atom(atom_table, input_id, input_sym.sect_idx(), offset)
1633
-                        .ok_or(WriteError::MissingSegment("__UNKNOWN"))?;
1683
+                let (atom_id, delta) = find_containing_atom(
1684
+                    atom_table,
1685
+                    atoms_by_input_section,
1686
+                    input_id,
1687
+                    input_sym.sect_idx(),
1688
+                    offset,
1689
+                )
1690
+                .ok_or(WriteError::MissingSegment("__UNKNOWN"))?;
16341691
                 let addr =
16351692
                     layout
16361693
                         .atom_addr(atom_id)
@@ -1679,30 +1736,40 @@ fn is_assembler_temporary_symbol(name: &str) -> bool {
16791736
 
16801737
 fn find_containing_atom(
16811738
     atom_table: &AtomTable,
1739
+    atoms_by_input_section: &HashMap<(InputId, u8), Vec<crate::resolve::AtomId>>,
16821740
     input_id: InputId,
16831741
     input_section: u8,
16841742
     offset: u32,
16851743
 ) -> Option<(crate::resolve::AtomId, u32)> {
1686
-    find_containing_atom_range(atom_table, input_id, input_section, offset, 1)
1744
+    find_containing_atom_range(
1745
+        atom_table,
1746
+        atoms_by_input_section,
1747
+        input_id,
1748
+        input_section,
1749
+        offset,
1750
+        1,
1751
+    )
16871752
 }
16881753
 
16891754
 fn find_containing_atom_range(
16901755
     atom_table: &AtomTable,
1756
+    atoms_by_input_section: &HashMap<(InputId, u8), Vec<crate::resolve::AtomId>>,
16911757
     input_id: InputId,
16921758
     input_section: u8,
16931759
     offset: u32,
16941760
     len: u32,
16951761
 ) -> Option<(crate::resolve::AtomId, u32)> {
1696
-    let atoms = atom_table.by_input_section();
1697
-    atoms.get(&(input_id, input_section)).and_then(|ids| {
1698
-        ids.iter().find_map(|atom_id| {
1699
-            let atom = atom_table.get(*atom_id);
1700
-            let start = atom.input_offset;
1701
-            let end = atom.input_offset.saturating_add(atom.size);
1702
-            let range_end = offset.checked_add(len)?;
1703
-            (start <= offset && range_end <= end).then_some((*atom_id, offset - start))
1762
+    atoms_by_input_section
1763
+        .get(&(input_id, input_section))
1764
+        .and_then(|ids| {
1765
+            ids.iter().find_map(|atom_id| {
1766
+                let atom = atom_table.get(*atom_id);
1767
+                let start = atom.input_offset;
1768
+                let end = atom.input_offset.saturating_add(atom.size);
1769
+                let range_end = offset.checked_add(len)?;
1770
+                (start <= offset && range_end <= end).then_some((*atom_id, offset - start))
1771
+            })
17041772
         })
1705
-    })
17061773
 }
17071774
 
17081775
 fn input_symbol_type(input_sym: &InputSymbol) -> u8 {
@@ -1836,20 +1903,34 @@ fn push_indirect_section(
18361903
     indirect_symbols: &mut Vec<u32>,
18371904
     indirect_starts: &mut HashMap<(String, String), u32>,
18381905
     key: (&str, &str),
1839
-    symbols: impl Iterator<Item = SymbolId>,
1840
-    symbol_indices: &HashMap<SymbolId, u32>,
1906
+    symbols: impl Iterator<Item = u32>,
18411907
 ) {
18421908
     let start = indirect_symbols.len() as u32;
18431909
     let mut saw_any = false;
18441910
     for symbol in symbols {
18451911
         saw_any = true;
1846
-        indirect_symbols.push(*symbol_indices.get(&symbol).expect("symbol index missing"));
1912
+        indirect_symbols.push(symbol);
18471913
     }
18481914
     if saw_any {
18491915
         indirect_starts.insert((key.0.to_string(), key.1.to_string()), start);
18501916
     }
18511917
 }
18521918
 
1919
+fn indirect_symbol_index(
1920
+    symbol: SymbolId,
1921
+    import_lookup: &HashMap<SymbolId, &ImportSymbolRecord>,
1922
+    symbol_indices: &HashMap<SymbolId, u32>,
1923
+) -> u32 {
1924
+    if import_lookup.contains_key(&symbol) {
1925
+        symbol_indices
1926
+            .get(&symbol)
1927
+            .copied()
1928
+            .unwrap_or(INDIRECT_SYMBOL_LOCAL)
1929
+    } else {
1930
+        INDIRECT_SYMBOL_LOCAL
1931
+    }
1932
+}
1933
+
18531934
 fn build_bind_streams(
18541935
     layout: &Layout,
18551936
     synthetic_plan: &SyntheticPlan,
@@ -1860,34 +1941,6 @@ fn build_bind_streams(
18601941
     let mut lazy_bind = OpcodeStream::new();
18611942
     let mut lazy_offsets = HashMap::new();
18621943
 
1863
-    if !synthetic_plan.got.entries.is_empty() {
1864
-        let segment_index = segment_index(layout, "__DATA_CONST")?;
1865
-        let segment = layout
1866
-            .segment("__DATA_CONST")
1867
-            .ok_or(WriteError::MissingSegment("__DATA_CONST"))?;
1868
-        let section = layout
1869
-            .sections
1870
-            .iter()
1871
-            .find(|section| section.segment == "__DATA_CONST" && section.name == "__got")
1872
-            .ok_or(WriteError::MissingSegment("__DATA_CONST"))?;
1873
-        for (idx, entry) in synthetic_plan.got.entries.iter().enumerate() {
1874
-            let import = imports
1875
-                .get(&entry.symbol)
1876
-                .copied()
1877
-                .ok_or(WriteError::ImportSymbolMissing(entry.symbol))?;
1878
-            let slot_addr = section.addr + (idx as u64) * 8;
1879
-            bind_specs.push(BindRecordSpec {
1880
-                segment_index,
1881
-                segment_offset: slot_addr - segment.vm_addr,
1882
-                ordinal: import.ordinal,
1883
-                name: &import.name,
1884
-                weak_import: import.weak_import,
1885
-                addend: 0,
1886
-                terminate: false,
1887
-            });
1888
-        }
1889
-    }
1890
-
18911944
     if let Some(tlv_bootstrap) = synthetic_plan.tlv_bootstrap_symbol {
18921945
         let segment_index = segment_index(layout, "__DATA")?;
18931946
         let segment = layout
@@ -1921,6 +1974,33 @@ fn build_bind_streams(
19211974
         }
19221975
     }
19231976
 
1977
+    if !synthetic_plan.got.entries.is_empty() {
1978
+        let segment_index = segment_index(layout, "__DATA_CONST")?;
1979
+        let segment = layout
1980
+            .segment("__DATA_CONST")
1981
+            .ok_or(WriteError::MissingSegment("__DATA_CONST"))?;
1982
+        let section = layout
1983
+            .sections
1984
+            .iter()
1985
+            .find(|section| section.segment == "__DATA_CONST" && section.name == "__got")
1986
+            .ok_or(WriteError::MissingSegment("__DATA_CONST"))?;
1987
+        for (idx, entry) in synthetic_plan.got.entries.iter().enumerate() {
1988
+            let Some(import) = imports.get(&entry.symbol).copied() else {
1989
+                continue;
1990
+            };
1991
+            let slot_addr = section.addr + (idx as u64) * 8;
1992
+            bind_specs.push(BindRecordSpec {
1993
+                segment_index,
1994
+                segment_offset: slot_addr - segment.vm_addr,
1995
+                ordinal: import.ordinal,
1996
+                name: &import.name,
1997
+                weak_import: import.weak_import,
1998
+                addend: 0,
1999
+                terminate: false,
2000
+            });
2001
+        }
2002
+    }
2003
+
19242004
     for entry in &synthetic_plan.direct_binds {
19252005
         let import = imports
19262006
             .get(&entry.symbol)
@@ -1934,6 +2014,13 @@ fn build_bind_streams(
19342014
             .iter()
19352015
             .find(|section| section.atoms.iter().any(|placed| placed.atom == entry.atom))
19362016
             .ok_or(WriteError::DirectBindSectionMissing(entry.atom))?;
2017
+        if section.segment == "__DATA" && section.name == "__thread_vars" {
2018
+            // `__thread_vars` starts are emitted through the dedicated
2019
+            // `__tlv_bootstrap` pass above. Descriptor tails are rewritten to
2020
+            // template offsets before write, so any generic direct bind landing
2021
+            // back in this section is stale and would override the TLV bind.
2022
+            continue;
2023
+        }
19372024
         let segment_index = segment_index(layout, &section.segment)?;
19382025
         let segment = layout
19392026
             .segment(&section.segment)
@@ -2342,4 +2429,47 @@ mod tests {
23422429
             vec![0x1000, 0x1008, 0x1040]
23432430
         );
23442431
     }
2432
+
2433
+    #[test]
2434
+    fn containing_atom_lookup_reuses_precomputed_section_index() {
2435
+        let mut atoms = AtomTable::new();
2436
+        let first = atoms.push(Atom {
2437
+            id: AtomId(0),
2438
+            origin: InputId(7),
2439
+            input_section: 3,
2440
+            section: AtomSection::Text,
2441
+            input_offset: 0,
2442
+            size: 8,
2443
+            align_pow2: 2,
2444
+            owner: None,
2445
+            alt_entries: Vec::new(),
2446
+            data: vec![0; 8],
2447
+            flags: AtomFlags::NONE,
2448
+            parent_of: None,
2449
+        });
2450
+        let second = atoms.push(Atom {
2451
+            id: AtomId(0),
2452
+            origin: InputId(7),
2453
+            input_section: 3,
2454
+            section: AtomSection::Text,
2455
+            input_offset: 8,
2456
+            size: 12,
2457
+            align_pow2: 2,
2458
+            owner: None,
2459
+            alt_entries: Vec::new(),
2460
+            data: vec![0; 12],
2461
+            flags: AtomFlags::NONE,
2462
+            parent_of: None,
2463
+        });
2464
+
2465
+        let by_input_section = atoms.by_input_section();
2466
+        assert_eq!(
2467
+            find_containing_atom(&atoms, &by_input_section, InputId(7), 3, 4),
2468
+            Some((first, 4))
2469
+        );
2470
+        assert_eq!(
2471
+            find_containing_atom_range(&atoms, &by_input_section, InputId(7), 3, 10, 2),
2472
+            Some((second, 2))
2473
+        );
2474
+    }
23452475
 }
src/reloc/arm64.rsmodified
@@ -42,7 +42,9 @@ impl std::error::Error for RelocError {}
4242
 
4343
 struct ResolveView<'a> {
4444
     sym_table: &'a SymbolTable,
45
+    atom_table: &'a AtomTable,
4546
     atom_addrs: &'a HashMap<crate::resolve::AtomId, u64>,
47
+    atoms_by_input_section: &'a HashMap<(InputId, u8), Vec<crate::resolve::AtomId>>,
4648
     section_addrs: &'a HashMap<(InputId, u8), u64>,
4749
     stub_addrs: &'a HashMap<SymbolId, u64>,
4850
     got_addrs: &'a HashMap<SymbolId, u64>,
@@ -104,11 +106,14 @@ pub fn apply_layout(
104106
     }
105107
 
106108
     let atom_addrs = atom_address_map(layout);
109
+    let atoms_by_input_section = atoms.by_input_section();
107110
     let section_addrs = input_section_address_map(layout, atoms);
108111
     let synth_addrs = synthetic_address_maps(layout, synthetic_plan);
109112
     let resolve = ResolveView {
110113
         sym_table,
114
+        atom_table: atoms,
111115
         atom_addrs: &atom_addrs,
116
+        atoms_by_input_section: &atoms_by_input_section,
112117
         section_addrs: &section_addrs,
113118
         stub_addrs: &synth_addrs.stub_addrs,
114119
         got_addrs: &synth_addrs.got_addrs,
@@ -145,7 +150,15 @@ pub fn apply_layout(
145150
     }
146151
 
147152
     if let Some(plan) = synthetic_plan {
148
-        synthesize_thread_variable_section(layout, plan)?;
153
+        synthesize_thread_variable_section(
154
+            layout,
155
+            plan,
156
+            atoms,
157
+            &input_map,
158
+            &reloc_cache,
159
+            &resolve,
160
+        )?;
161
+        synthesize_got_section(layout, plan, &resolve)?;
149162
         synthesize_stub_section(layout, plan, &resolve)?;
150163
         synthesize_lazy_pointer_section(layout, plan, &resolve)?;
151164
         synthesize_stub_helper_section(layout, plan, &resolve, linkedit)?;
@@ -367,16 +380,24 @@ fn apply_one(
367380
             local_offset,
368381
             reloc,
369382
             place,
370
-            resolve_got_target(obj, atom, reloc, resolve)?,
371
-        ),
372
-        RelocKind::GotLoadPageOff12 => patch_pageoff12(
373
-            bytes,
374
-            atom,
375
-            obj,
376
-            local_offset,
377
-            reloc,
378
-            resolve_got_target(obj, atom, reloc, resolve)?,
383
+            if got_reloc_relaxes_locally(obj, reloc, resolve) {
384
+                resolve_referent(obj, atom, reloc.kind, reloc.referent, resolve)?
385
+            } else {
386
+                resolve_got_target(obj, atom, reloc, resolve)?
387
+            },
379388
         ),
389
+        RelocKind::GotLoadPageOff12 => {
390
+            let target = if got_reloc_relaxes_locally(obj, reloc, resolve) {
391
+                resolve_referent(obj, atom, reloc.kind, reloc.referent, resolve)?
392
+            } else {
393
+                resolve_got_target(obj, atom, reloc, resolve)?
394
+            };
395
+            if got_reloc_relaxes_locally(obj, reloc, resolve) {
396
+                patch_got_pageoff12_relaxed(bytes, atom, obj, local_offset, reloc, target)
397
+            } else {
398
+                patch_pageoff12(bytes, atom, obj, local_offset, reloc, target)
399
+            }
400
+        }
380401
         RelocKind::PointerToGot => patch_unsigned(
381402
             bytes,
382403
             atom,
@@ -432,14 +453,14 @@ fn resolve_got_target(
432453
     reloc: Reloc,
433454
     resolve: &ResolveView<'_>,
434455
 ) -> Result<u64, RelocError> {
435
-    let Some(symbol_id) = dylib_import_symbol_id(obj, reloc.referent, resolve.sym_table) else {
456
+    let Some(symbol_id) = symbol_referent_id(obj, reloc.referent, resolve.sym_table) else {
436457
         return Err(reloc_error(
437458
             atom,
438459
             &obj.path,
439460
             reloc.offset.saturating_sub(atom.input_offset),
440461
             reloc.kind,
441462
             &describe_referent(obj, reloc.referent),
442
-            "GOT relocations currently require a dylib import target".to_string(),
463
+            "GOT relocations require a symbol target".to_string(),
443464
         ));
444465
     };
445466
     resolve.got_addrs.get(&symbol_id).copied().ok_or_else(|| {
@@ -449,11 +470,17 @@ fn resolve_got_target(
449470
             reloc.offset.saturating_sub(atom.input_offset),
450471
             reloc.kind,
451472
             &describe_referent(obj, reloc.referent),
452
-            "dylib import is missing synthetic GOT slot".to_string(),
473
+            "symbol is missing synthetic GOT slot".to_string(),
453474
         )
454475
     })
455476
 }
456477
 
478
+fn got_reloc_relaxes_locally(obj: &ObjectFile, reloc: Reloc, resolve: &ResolveView<'_>) -> bool {
479
+    symbol_referent_id(obj, reloc.referent, resolve.sym_table)
480
+        .map(|symbol_id| !matches!(resolve.sym_table.get(symbol_id), Symbol::DylibImport { .. }))
481
+        .unwrap_or(false)
482
+}
483
+
457484
 fn resolve_tlvp_target(
458485
     obj: &ObjectFile,
459486
     atom: &Atom,
@@ -541,6 +568,15 @@ fn dylib_import_symbol_id(
541568
     obj: &ObjectFile,
542569
     referent: Referent,
543570
     sym_table: &SymbolTable,
571
+) -> Option<SymbolId> {
572
+    let symbol_id = symbol_referent_id(obj, referent, sym_table)?;
573
+    matches!(sym_table.get(symbol_id), Symbol::DylibImport { .. }).then_some(symbol_id)
574
+}
575
+
576
+fn symbol_referent_id(
577
+    obj: &ObjectFile,
578
+    referent: Referent,
579
+    sym_table: &SymbolTable,
544580
 ) -> Option<SymbolId> {
545581
     let Referent::Symbol(sym_idx) = referent else {
546582
         return None;
@@ -550,7 +586,8 @@ fn dylib_import_symbol_id(
550586
     let (symbol_id, symbol) = sym_table
551587
         .iter()
552588
         .find(|(_, symbol)| sym_table.interner.resolve(symbol.name()) == name)?;
553
-    matches!(symbol, Symbol::DylibImport { .. }).then_some(symbol_id)
589
+    let _ = symbol;
590
+    Some(symbol_id)
554591
 }
555592
 
556593
 fn resolve_global_symbol(
@@ -607,6 +644,17 @@ fn resolve_input_symbol(
607644
     kind: RelocKind,
608645
     input_sym: &InputSymbol,
609646
     resolve: &ResolveView<'_>,
647
+) -> Result<u64, RelocError> {
648
+    resolve_input_symbol_at_origin(atom.origin, obj, atom, kind, input_sym, resolve)
649
+}
650
+
651
+fn resolve_input_symbol_at_origin(
652
+    origin: InputId,
653
+    obj: &ObjectFile,
654
+    atom: &Atom,
655
+    kind: RelocKind,
656
+    input_sym: &InputSymbol,
657
+    resolve: &ResolveView<'_>,
610658
 ) -> Result<u64, RelocError> {
611659
     match input_sym.kind() {
612660
         SymKind::Abs => Ok(input_sym.value()),
@@ -621,21 +669,17 @@ fn resolve_input_symbol(
621669
                     "section-backed symbol did not resolve to an input section".to_string(),
622670
                 )
623671
             })?;
624
-            let section_addr = resolve
625
-                .section_addrs
626
-                .get(&(atom.origin, input_sym.sect_idx()))
627
-                .copied()
628
-                .ok_or_else(|| {
629
-                    reloc_error(
630
-                        atom,
631
-                        &obj.path,
632
-                        0,
633
-                        kind,
634
-                        &describe_input_symbol(obj, input_sym),
635
-                        "section-backed symbol's output section is missing".to_string(),
636
-                    )
637
-                })?;
638
-            Ok(section_addr + input_sym.value().saturating_sub(section.addr))
672
+            let section_offset = input_sym.value().saturating_sub(section.addr) as u32;
673
+            resolve_input_section_offset(
674
+                origin,
675
+                obj,
676
+                atom,
677
+                kind,
678
+                input_sym.sect_idx(),
679
+                section_offset,
680
+                &describe_input_symbol(obj, input_sym),
681
+                resolve,
682
+            )
639683
         }
640684
         SymKind::Undef => Err(reloc_error(
641685
             atom,
@@ -656,6 +700,65 @@ fn resolve_input_symbol(
656700
     }
657701
 }
658702
 
703
+fn resolve_input_section_offset(
704
+    origin: InputId,
705
+    obj: &ObjectFile,
706
+    atom: &Atom,
707
+    kind: RelocKind,
708
+    input_section: u8,
709
+    input_offset: u32,
710
+    referent: &str,
711
+    resolve: &ResolveView<'_>,
712
+) -> Result<u64, RelocError> {
713
+    if let Some(atom_ids) = resolve.atoms_by_input_section.get(&(origin, input_section)) {
714
+        if let Some((target_atom, delta)) = atom_ids.iter().find_map(|atom_id| {
715
+            let candidate = resolve.atom_table.get(*atom_id);
716
+            let start = candidate.input_offset;
717
+            let end = candidate.input_offset.saturating_add(candidate.size);
718
+            if start <= input_offset && input_offset < end {
719
+                Some((*atom_id, input_offset - start))
720
+            } else if input_offset == end {
721
+                Some((*atom_id, candidate.size))
722
+            } else {
723
+                None
724
+            }
725
+        }) {
726
+            let atom_addr = resolve
727
+                .atom_addrs
728
+                .get(&target_atom)
729
+                .copied()
730
+                .ok_or_else(|| {
731
+                    reloc_error(
732
+                        atom,
733
+                        &obj.path,
734
+                        0,
735
+                        kind,
736
+                        referent,
737
+                        "section-backed symbol's containing atom is missing a final address"
738
+                            .to_string(),
739
+                    )
740
+                })?;
741
+            return Ok(atom_addr + delta as u64);
742
+        }
743
+    }
744
+
745
+    let section_addr = resolve
746
+        .section_addrs
747
+        .get(&(origin, input_section))
748
+        .copied()
749
+        .ok_or_else(|| {
750
+            reloc_error(
751
+                atom,
752
+                &obj.path,
753
+                0,
754
+                kind,
755
+                referent,
756
+                "section-backed symbol's output section is missing".to_string(),
757
+            )
758
+        })?;
759
+    Ok(section_addr + input_offset as u64)
760
+}
761
+
659762
 fn patch_unsigned(
660763
     bytes: &mut [u8],
661764
     atom: &Atom,
@@ -911,7 +1014,7 @@ fn patch_pageoff12(
9111014
     let imm = if is_add_immediate(insn) {
9121015
         pageoff
9131016
     } else {
914
-        let shift = ((insn >> 30) & 0b11) as u64;
1017
+        let shift = pageoff_load_store_shift(insn);
9151018
         let scale = 1u64 << shift;
9161019
         if !pageoff.is_multiple_of(scale) {
9171020
             return Err(reloc_error(
@@ -947,6 +1050,28 @@ fn patch_pageoff12(
9471050
     )
9481051
 }
9491052
 
1053
+fn pageoff_load_store_shift(insn: u32) -> u64 {
1054
+    if is_simd_fp_pageoff(insn) {
1055
+        simd_fp_pageoff_shift(insn)
1056
+    } else {
1057
+        ((insn >> 30) & 0b11) as u64
1058
+    }
1059
+}
1060
+
1061
+fn is_simd_fp_pageoff(insn: u32) -> bool {
1062
+    ((insn >> 24) & 0b111) == 0b101
1063
+}
1064
+
1065
+fn simd_fp_pageoff_shift(insn: u32) -> u64 {
1066
+    let size = ((insn >> 30) & 0b11) as u64;
1067
+    let opc = ((insn >> 22) & 0b11) as u64;
1068
+    if size == 0 && (opc & 0b10) != 0 {
1069
+        4
1070
+    } else {
1071
+        size
1072
+    }
1073
+}
1074
+
9501075
 fn read_implicit_addend(
9511076
     bytes: &[u8],
9521077
     local_offset: u32,
@@ -1026,9 +1151,55 @@ fn patch_tlvp_pageoff12(
10261151
     )
10271152
 }
10281153
 
1154
+fn patch_got_pageoff12_relaxed(
1155
+    bytes: &mut [u8],
1156
+    atom: &Atom,
1157
+    obj: &ObjectFile,
1158
+    local_offset: u32,
1159
+    reloc: Reloc,
1160
+    target: u64,
1161
+) -> Result<(), RelocError> {
1162
+    let pageoff = target.wrapping_add_signed(reloc.addend) & 0xfff;
1163
+    if pageoff > 0xfff {
1164
+        return Err(reloc_error(
1165
+            atom,
1166
+            &obj.path,
1167
+            local_offset,
1168
+            reloc.kind,
1169
+            &describe_referent(obj, reloc.referent),
1170
+            format!("pageoff immediate 0x{pageoff:x} exceeds 12 bits"),
1171
+        ));
1172
+    }
1173
+
1174
+    let insn = read_u32(
1175
+        bytes,
1176
+        local_offset,
1177
+        atom,
1178
+        obj,
1179
+        reloc.kind,
1180
+        &describe_referent(obj, reloc.referent),
1181
+    )?;
1182
+    let rd = insn & 0x1f;
1183
+    let rn = (insn >> 5) & 0x1f;
1184
+    let patched = 0x9100_0000 | ((pageoff as u32) << 10) | (rn << 5) | rd;
1185
+    write_u32(
1186
+        bytes,
1187
+        local_offset,
1188
+        patched,
1189
+        atom,
1190
+        obj,
1191
+        reloc.kind,
1192
+        &describe_referent(obj, reloc.referent),
1193
+    )
1194
+}
1195
+
10291196
 fn synthesize_thread_variable_section(
10301197
     layout: &mut Layout,
10311198
     plan: &SyntheticPlan,
1199
+    atoms: &AtomTable,
1200
+    input_map: &HashMap<InputId, &ObjectFile>,
1201
+    reloc_cache: &HashMap<(InputId, u8), Vec<Reloc>>,
1202
+    resolve: &ResolveView<'_>,
10321203
 ) -> Result<(), RelocError> {
10331204
     let Some(_bootstrap_symbol) = plan.tlv_bootstrap_symbol else {
10341205
         return Ok(());
@@ -1057,6 +1228,19 @@ fn synthesize_thread_variable_section(
10571228
     };
10581229
 
10591230
     for placed in &mut section.atoms {
1231
+        let atom = atoms.get(placed.atom);
1232
+        let obj = input_map.get(&atom.origin).ok_or_else(|| RelocError {
1233
+            input: PathBuf::from("<missing object>"),
1234
+            atom: placed.atom,
1235
+            atom_offset: 0,
1236
+            kind: RelocKind::Unsigned,
1237
+            referent: "__thread_vars".to_string(),
1238
+            detail: "missing parsed object for TLV descriptor atom".to_string(),
1239
+        })?;
1240
+        let relocs = reloc_cache
1241
+            .get(&(atom.origin, atom.input_section))
1242
+            .map(Vec::as_slice)
1243
+            .unwrap_or(&[]);
10601244
         if placed.size % THREAD_VARIABLE_DESCRIPTOR_SIZE as u64 != 0 {
10611245
             return Err(RelocError {
10621246
                 input: PathBuf::from("<synthetic tlv>"),
@@ -1074,6 +1258,7 @@ fn synthesize_thread_variable_section(
10741258
         for descriptor_offset in
10751259
             (0..placed.size as usize).step_by(THREAD_VARIABLE_DESCRIPTOR_SIZE as usize)
10761260
         {
1261
+            let descriptor_offset_u32 = descriptor_offset as u32;
10771262
             let start = descriptor_offset;
10781263
             let end = start + THREAD_VARIABLE_DESCRIPTOR_SIZE as usize;
10791264
             let descriptor = placed.data.get_mut(start..end).ok_or_else(|| RelocError {
@@ -1086,11 +1271,15 @@ fn synthesize_thread_variable_section(
10861271
             })?;
10871272
 
10881273
             descriptor[0..8].fill(0);
1089
-            let init_addr = u64::from_le_bytes(
1090
-                descriptor[16..24]
1091
-                    .try_into()
1092
-                    .expect("8-byte descriptor tail"),
1093
-            );
1274
+            let init_addr = resolve_tlv_init_address(
1275
+                descriptor,
1276
+                atom,
1277
+                obj,
1278
+                relocs,
1279
+                descriptor_offset_u32,
1280
+                input_map,
1281
+                resolve,
1282
+            )?;
10941283
             if init_addr < template_base {
10951284
                 return Err(RelocError {
10961285
                     input: PathBuf::from("<synthetic tlv>"),
@@ -1111,6 +1300,180 @@ fn synthesize_thread_variable_section(
11111300
     Ok(())
11121301
 }
11131302
 
1303
+fn resolve_tlv_init_address(
1304
+    descriptor: &[u8],
1305
+    atom: &Atom,
1306
+    obj: &ObjectFile,
1307
+    relocs: &[Reloc],
1308
+    descriptor_offset: u32,
1309
+    input_map: &HashMap<InputId, &ObjectFile>,
1310
+    resolve: &ResolveView<'_>,
1311
+) -> Result<u64, RelocError> {
1312
+    for owner in descriptor_owner_symbols(obj, atom, descriptor_offset) {
1313
+        if let Some(init_addr) = resolve_named_tlv_init(owner, atom, input_map, resolve)? {
1314
+            return Ok(init_addr);
1315
+        }
1316
+    }
1317
+
1318
+    let field_offset = atom.input_offset + descriptor_offset + 16;
1319
+    if let Some(reloc) = relocs_for_atom(relocs, atom).find(|reloc| reloc.offset == field_offset) {
1320
+        let target = resolve_tlv_descriptor_referent(obj, atom, reloc, resolve)?;
1321
+        return Ok(target.wrapping_add_signed(reloc.addend));
1322
+    }
1323
+
1324
+    Ok(u64::from_le_bytes(
1325
+        descriptor[16..24]
1326
+            .try_into()
1327
+            .expect("8-byte descriptor tail"),
1328
+    ))
1329
+}
1330
+
1331
+fn descriptor_owner_symbols<'a>(
1332
+    obj: &'a ObjectFile,
1333
+    atom: &'a Atom,
1334
+    descriptor_offset: u32,
1335
+) -> impl Iterator<Item = &'a InputSymbol> + 'a {
1336
+    let descriptor_start = atom.input_offset as u64 + descriptor_offset as u64;
1337
+    obj.symbols.iter().filter(move |input_sym| {
1338
+        input_sym.kind() == SymKind::Sect
1339
+            && input_sym.sect_idx() == atom.input_section
1340
+            && obj.section_for_symbol(input_sym).is_some_and(|section| {
1341
+                input_sym.value().saturating_sub(section.addr) == descriptor_start
1342
+            })
1343
+    })
1344
+}
1345
+
1346
+fn matching_tlv_init_symbol<'a>(
1347
+    obj: &'a ObjectFile,
1348
+    owner: &InputSymbol,
1349
+) -> Result<Option<&'a InputSymbol>, RelocError> {
1350
+    let owner_name = match obj.symbol_name(owner) {
1351
+        Ok(name) => name,
1352
+        Err(_) => return Ok(None),
1353
+    };
1354
+    let init_name = format!("{owner_name}$tlv$init");
1355
+    Ok(obj.symbols.iter().find(|input_sym| {
1356
+        obj.symbol_name(input_sym)
1357
+            .is_ok_and(|name| name == init_name)
1358
+    }))
1359
+}
1360
+
1361
+fn resolve_named_tlv_init(
1362
+    owner: &InputSymbol,
1363
+    atom: &Atom,
1364
+    input_map: &HashMap<InputId, &ObjectFile>,
1365
+    resolve: &ResolveView<'_>,
1366
+) -> Result<Option<u64>, RelocError> {
1367
+    for (&origin, obj) in input_map {
1368
+        let Some(init_symbol) = matching_tlv_init_symbol(obj, owner)? else {
1369
+            continue;
1370
+        };
1371
+        return Ok(Some(resolve_input_symbol_at_origin(
1372
+            origin,
1373
+            obj,
1374
+            atom,
1375
+            RelocKind::Unsigned,
1376
+            init_symbol,
1377
+            resolve,
1378
+        )?));
1379
+    }
1380
+    Ok(None)
1381
+}
1382
+
1383
+fn resolve_tlv_descriptor_referent(
1384
+    obj: &ObjectFile,
1385
+    atom: &Atom,
1386
+    reloc: Reloc,
1387
+    resolve: &ResolveView<'_>,
1388
+) -> Result<u64, RelocError> {
1389
+    match reloc.referent {
1390
+        Referent::Section(section_idx) => resolve
1391
+            .section_addrs
1392
+            .get(&(atom.origin, section_idx))
1393
+            .copied()
1394
+            .ok_or_else(|| {
1395
+                reloc_error(
1396
+                    atom,
1397
+                    &obj.path,
1398
+                    reloc.offset.saturating_sub(atom.input_offset),
1399
+                    reloc.kind,
1400
+                    &format!("section #{section_idx}"),
1401
+                    "TLV descriptor referent section was not laid out".to_string(),
1402
+                )
1403
+            }),
1404
+        Referent::Symbol(sym_idx) => {
1405
+            let input_sym = obj.symbols.get(sym_idx as usize).ok_or_else(|| {
1406
+                reloc_error(
1407
+                    atom,
1408
+                    &obj.path,
1409
+                    reloc.offset.saturating_sub(atom.input_offset),
1410
+                    reloc.kind,
1411
+                    &format!("symbol #{sym_idx}"),
1412
+                    "TLV descriptor symbol index is out of range".to_string(),
1413
+                )
1414
+            })?;
1415
+            resolve_input_symbol_at_origin(atom.origin, obj, atom, reloc.kind, input_sym, resolve)
1416
+        }
1417
+    }
1418
+}
1419
+
1420
+fn synthesize_got_section(
1421
+    layout: &mut Layout,
1422
+    plan: &SyntheticPlan,
1423
+    resolve: &ResolveView<'_>,
1424
+) -> Result<(), RelocError> {
1425
+    let Some(section) = layout
1426
+        .sections
1427
+        .iter_mut()
1428
+        .find(|section| section.segment == "__DATA_CONST" && section.name == "__got")
1429
+    else {
1430
+        return Ok(());
1431
+    };
1432
+
1433
+    for (idx, entry) in plan.got.entries.iter().enumerate() {
1434
+        let start = idx * 8;
1435
+        let end = start + 8;
1436
+        let value = match resolve.sym_table.get(entry.symbol) {
1437
+            Symbol::DylibImport { .. } => 0,
1438
+            Symbol::Defined {
1439
+                atom: target_atom,
1440
+                value,
1441
+                ..
1442
+            } => {
1443
+                resolve
1444
+                    .atom_addrs
1445
+                    .get(target_atom)
1446
+                    .copied()
1447
+                    .ok_or_else(|| RelocError {
1448
+                        input: PathBuf::from("<synthetic got>"),
1449
+                        atom: crate::resolve::AtomId(0),
1450
+                        atom_offset: start as u32,
1451
+                        kind: RelocKind::PointerToGot,
1452
+                        referent: format!("symbol {:?}", entry.symbol),
1453
+                        detail: "defined GOT target is missing final address".to_string(),
1454
+                    })?
1455
+                    + *value
1456
+            }
1457
+            other => {
1458
+                return Err(RelocError {
1459
+                    input: PathBuf::from("<synthetic got>"),
1460
+                    atom: crate::resolve::AtomId(0),
1461
+                    atom_offset: start as u32,
1462
+                    kind: RelocKind::PointerToGot,
1463
+                    referent: format!("symbol {:?}", entry.symbol),
1464
+                    detail: format!(
1465
+                        "synthetic GOT currently does not support symbol kind {:?}",
1466
+                        other.kind()
1467
+                    ),
1468
+                });
1469
+            }
1470
+        };
1471
+        section.synthetic_data[start..end].copy_from_slice(&value.to_le_bytes());
1472
+    }
1473
+
1474
+    Ok(())
1475
+}
1476
+
11141477
 fn synthesize_stub_section(
11151478
     layout: &mut Layout,
11161479
     plan: &SyntheticPlan,
@@ -1587,7 +1950,7 @@ mod tests {
15871950
     fn load_store_pageoff_uses_size_scaling() {
15881951
         let insn = 0xf940_0000u32;
15891952
         assert!(!is_add_immediate(insn));
1590
-        let shift = (insn >> 30) & 0b11;
1953
+        let shift = pageoff_load_store_shift(insn);
15911954
         assert_eq!(shift, 0b11);
15921955
         let pageoff = 0x3f8u64;
15931956
         let imm = pageoff >> shift;
@@ -1595,6 +1958,20 @@ mod tests {
15951958
         assert_eq!((patched >> 10) & 0xfff, 0x7f);
15961959
     }
15971960
 
1961
+    #[test]
1962
+    fn simd_q_pageoff_uses_16_byte_scaling() {
1963
+        let insn = 0x3dc0_0100u32;
1964
+        assert!(!is_add_immediate(insn));
1965
+        assert!(is_simd_fp_pageoff(insn));
1966
+        let shift = pageoff_load_store_shift(insn);
1967
+        assert_eq!(shift, 4);
1968
+        let pageoff = 0x690u64;
1969
+        let imm = pageoff >> shift;
1970
+        let patched = (insn & !(0xfff << 10)) | ((imm as u32) << 10);
1971
+        assert_eq!((patched >> 10) & 0xfff, 0x69);
1972
+        assert_eq!(patched, 0x3dc1_a500);
1973
+    }
1974
+
15981975
     #[test]
15991976
     fn signed_fit_helper_matches_branch_range() {
16001977
         assert!(fits_signed((1 << 25) - 1, 26));
src/resolve.rsmodified
@@ -134,6 +134,8 @@ opaque_id!(
134134
 #[derive(Debug)]
135135
 pub struct ObjectInput {
136136
     pub path: PathBuf,
137
+    pub load_order: usize,
138
+    pub archive_member_offset: Option<u32>,
137139
     /// Raw bytes; `ObjectFile::parse` re-runs cheaply against this on
138140
     /// demand. We don't cache a parsed view because `ObjectFile` copies
139141
     /// the fields it needs on construction, so re-parse is idempotent.
@@ -143,6 +145,7 @@ pub struct ObjectInput {
143145
 #[derive(Debug)]
144146
 pub struct ArchiveInput {
145147
     pub path: PathBuf,
148
+    pub load_order: usize,
146149
     pub bytes: Vec<u8>,
147150
     /// Members we've already fetched (keyed by `ar_hdr` offset). Prevents
148151
     /// the fixed-point loop from re-ingesting the same object twice —
@@ -225,11 +228,21 @@ impl Inputs {
225228
     /// Register an `.o` file. Validates the Mach-O header by parsing once,
226229
     /// then keeps only the raw bytes (re-parsing on demand is cheap and
227230
     /// sidesteps borrow-lifetime headaches).
228
-    pub fn add_object(&mut self, path: PathBuf, bytes: Vec<u8>) -> Result<InputId, InputAddError> {
231
+    pub fn add_object(
232
+        &mut self,
233
+        path: PathBuf,
234
+        bytes: Vec<u8>,
235
+        load_order: usize,
236
+    ) -> Result<InputId, InputAddError> {
229237
         // Validate now — we'd rather catch a bad object at the add site.
230238
         ObjectFile::parse(&path, &bytes)?;
231239
         let id = InputId(self.objects.len() as u32);
232
-        self.objects.push(ObjectInput { path, bytes });
240
+        self.objects.push(ObjectInput {
241
+            path,
242
+            load_order,
243
+            archive_member_offset: None,
244
+            bytes,
245
+        });
233246
         Ok(id)
234247
     }
235248
 
@@ -238,11 +251,13 @@ impl Inputs {
238251
         &mut self,
239252
         path: PathBuf,
240253
         bytes: Vec<u8>,
254
+        load_order: usize,
241255
     ) -> Result<ArchiveId, InputAddError> {
242256
         Archive::open(&path, &bytes)?; // validate
243257
         let id = ArchiveId(self.archives.len() as u32);
244258
         self.archives.push(ArchiveInput {
245259
             path,
260
+            load_order,
246261
             bytes,
247262
             fetched: std::collections::HashSet::new(),
248263
         });
@@ -1134,6 +1149,7 @@ fn ingest_member_bytes(
11341149
     member_id: MemberId,
11351150
     report: &mut DrainReport,
11361151
 ) -> Result<Vec<PendingFetch>, FetchError> {
1152
+    let archive_load_order = inputs.archives[archive_id.0 as usize].load_order;
11371153
     let ai = &inputs.archives[archive_id.0 as usize];
11381154
     if ai.fetched.contains(&member_id.0) {
11391155
         return Ok(Vec::new());
@@ -1158,6 +1174,8 @@ fn ingest_member_bytes(
11581174
     let input_id = InputId(inputs.objects.len() as u32);
11591175
     inputs.objects.push(ObjectInput {
11601176
         path: PathBuf::from(logical_path),
1177
+        load_order: archive_load_order,
1178
+        archive_member_offset: Some(member_id.0),
11611179
         bytes: member_bytes,
11621180
     });
11631181
     report.fetched_members += 1;
src/section.rsmodified
@@ -18,7 +18,7 @@ pub enum SectionKind {
1818
     Text,
1919
     /// Regular data (`S_REGULAR` with none of the attribute markers).
2020
     Data,
21
-    /// `__TEXT,__const` — immutable data.
21
+    /// Immutable data such as `__TEXT,__const` or `__DATA_CONST,__const`.
2222
     ConstData,
2323
     /// `__TEXT,__cstring` (`S_CSTRING_LITERALS`).
2424
     CStringLiterals,
@@ -94,7 +94,7 @@ fn classify_regular(segname: &str, sectname: &str, flags: u32) -> SectionKind {
9494
     if flags & S_ATTR_PURE_INSTRUCTIONS != 0 {
9595
         return SectionKind::Text;
9696
     }
97
-    if segname == "__TEXT" && sectname == "__const" {
97
+    if sectname == "__const" && matches!(segname, "__TEXT" | "__DATA" | "__DATA_CONST") {
9898
         return SectionKind::ConstData;
9999
     }
100100
     SectionKind::Data
src/synth/dyld_info.rsmodified
@@ -3,11 +3,12 @@ use std::collections::BTreeMap;
33
 use crate::leb::{write_sleb, write_uleb};
44
 use crate::macho::constants::{
55
     BIND_IMMEDIATE_MASK, BIND_OPCODE_ADD_ADDR_ULEB, BIND_OPCODE_DO_BIND,
6
-    BIND_OPCODE_SET_ADDEND_SLEB, BIND_OPCODE_SET_DYLIB_ORDINAL_IMM,
7
-    BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB, BIND_OPCODE_SET_DYLIB_SPECIAL_IMM,
8
-    BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB, BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM,
9
-    BIND_OPCODE_SET_TYPE_IMM, BIND_SYMBOL_FLAGS_WEAK_IMPORT, BIND_TYPE_POINTER,
10
-    REBASE_IMMEDIATE_MASK, REBASE_OPCODE_DO_REBASE_IMM_TIMES, REBASE_OPCODE_DO_REBASE_ULEB_TIMES,
6
+    BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB, BIND_OPCODE_SET_ADDEND_SLEB,
7
+    BIND_OPCODE_SET_DYLIB_ORDINAL_IMM, BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB,
8
+    BIND_OPCODE_SET_DYLIB_SPECIAL_IMM, BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB,
9
+    BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM, BIND_OPCODE_SET_TYPE_IMM,
10
+    BIND_SYMBOL_FLAGS_WEAK_IMPORT, BIND_TYPE_POINTER, REBASE_IMMEDIATE_MASK,
11
+    REBASE_OPCODE_DO_REBASE_IMM_TIMES, REBASE_OPCODE_DO_REBASE_ULEB_TIMES,
1112
 };
1213
 use crate::macho::exports::{ExportEntry, ExportKind};
1314
 
@@ -253,7 +254,9 @@ pub fn emit_bind_records(specs: &[BindRecordSpec<'_>]) -> Vec<u8> {
253254
     let mut state = BindState::default();
254255
     let mut current_symbol: Option<String> = None;
255256
 
256
-    for spec in specs {
257
+    let mut idx = 0usize;
258
+    while idx < specs.len() {
259
+        let spec = specs[idx];
257260
         if state.ordinal != Some(spec.ordinal) {
258261
             emit_bind_ordinal(&mut out, spec.ordinal);
259262
             state.ordinal = Some(spec.ordinal);
@@ -301,9 +304,21 @@ pub fn emit_bind_records(specs: &[BindRecordSpec<'_>]) -> Vec<u8> {
301304
             }
302305
         }
303306
 
304
-        out.byte(BIND_OPCODE_DO_BIND);
307
+        let run_len = bind_run_len(specs, idx);
308
+        if run_len > 1 {
309
+            let stride = specs[idx + 1].segment_offset - spec.segment_offset;
310
+            let skip = stride - 8;
311
+            out.byte(BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB);
312
+            out.uleb(run_len as u64);
313
+            out.uleb(skip);
314
+            state.next_segment_offset = Some(spec.segment_offset + (run_len as u64) * stride);
315
+            idx += run_len;
316
+        } else {
317
+            out.byte(BIND_OPCODE_DO_BIND);
318
+            state.next_segment_offset = Some(spec.segment_offset + 8);
319
+            idx += 1;
320
+        }
305321
         state.segment_index = Some(spec.segment_index);
306
-        state.next_segment_offset = Some(spec.segment_offset + 8);
307322
     }
308323
 
309324
     if specs.last().is_some_and(|spec| spec.terminate) {
@@ -351,6 +366,41 @@ fn bind_symbol_flags(weak_import: bool) -> u8 {
351366
     }
352367
 }
353368
 
369
+fn bind_run_len(specs: &[BindRecordSpec<'_>], start: usize) -> usize {
370
+    let Some(next) = specs.get(start + 1) else {
371
+        return 1;
372
+    };
373
+    let first = specs[start];
374
+    if first.segment_index != next.segment_index
375
+        || first.ordinal != next.ordinal
376
+        || first.name != next.name
377
+        || first.weak_import != next.weak_import
378
+        || first.addend != next.addend
379
+    {
380
+        return 1;
381
+    }
382
+    let stride = next.segment_offset.saturating_sub(first.segment_offset);
383
+    if stride < 8 {
384
+        return 1;
385
+    }
386
+
387
+    let mut len = 2usize;
388
+    while let Some(spec) = specs.get(start + len) {
389
+        let expected_offset = first.segment_offset + (len as u64) * stride;
390
+        if spec.segment_index != first.segment_index
391
+            || spec.ordinal != first.ordinal
392
+            || spec.name != first.name
393
+            || spec.weak_import != first.weak_import
394
+            || spec.addend != first.addend
395
+            || spec.segment_offset != expected_offset
396
+        {
397
+            break;
398
+        }
399
+        len += 1;
400
+    }
401
+    len
402
+}
403
+
354404
 #[cfg(test)]
355405
 mod tests {
356406
     use crate::leb::{read_sleb, read_uleb};
@@ -528,4 +578,46 @@ mod tests {
528578
             .any(|window| window == [BIND_OPCODE_SET_ADDEND_SLEB, 0]);
529579
         assert!(zero_reset, "expected explicit addend reset back to zero");
530580
     }
581
+
582
+    #[test]
583
+    fn bind_encoder_batches_constant_stride_runs() {
584
+        let stream = emit_bind_records(&[
585
+            BindRecordSpec {
586
+                segment_index: 3,
587
+                segment_offset: 0x98,
588
+                ordinal: 1,
589
+                name: "__tlv_bootstrap",
590
+                weak_import: false,
591
+                addend: 0,
592
+                terminate: false,
593
+            },
594
+            BindRecordSpec {
595
+                segment_index: 3,
596
+                segment_offset: 0xb0,
597
+                ordinal: 1,
598
+                name: "__tlv_bootstrap",
599
+                weak_import: false,
600
+                addend: 0,
601
+                terminate: false,
602
+            },
603
+            BindRecordSpec {
604
+                segment_index: 3,
605
+                segment_offset: 0xc8,
606
+                ordinal: 1,
607
+                name: "__tlv_bootstrap",
608
+                weak_import: false,
609
+                addend: 0,
610
+                terminate: true,
611
+            },
612
+        ]);
613
+
614
+        assert!(stream.contains(&BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB));
615
+        let idx = stream
616
+            .iter()
617
+            .position(|byte| *byte == BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB)
618
+            .unwrap();
619
+        assert_eq!(stream[idx + 1], 3);
620
+        assert_eq!(stream[idx + 2], 0x10);
621
+        assert_eq!(stream.last().copied(), Some(0));
622
+    }
531623
 }
src/synth/mod.rsmodified
@@ -166,10 +166,28 @@ impl SyntheticPlan {
166166
                     RelocKind::GotLoadPage21
167167
                     | RelocKind::GotLoadPageOff12
168168
                     | RelocKind::PointerToGot => {
169
-                        let Some(symbol_id) = dylib_import_referent(obj, reloc.referent, sym_table)
169
+                        if matches!(
170
+                            reloc.kind,
171
+                            RelocKind::GotLoadPage21 | RelocKind::GotLoadPageOff12
172
+                        ) {
173
+                            let Some(symbol_id) =
174
+                                dylib_import_referent(obj, reloc.referent, sym_table)
175
+                            else {
176
+                                continue;
177
+                            };
178
+                            got.intern(symbol_id, dylib_import_is_weak(sym_table, symbol_id));
179
+                            continue;
180
+                        }
181
+                        let Some(symbol_id) = symbol_referent_id(obj, reloc.referent, sym_table)
170182
                         else {
171183
                             continue;
172184
                         };
185
+                        if matches!(reloc.kind, RelocKind::PointerToGot)
186
+                            && matches!(sym_table.get(symbol_id), Symbol::DylibImport { .. })
187
+                        {
188
+                            got.intern(symbol_id, dylib_import_is_weak(sym_table, symbol_id));
189
+                            continue;
190
+                        }
173191
                         got.intern(symbol_id, dylib_import_is_weak(sym_table, symbol_id));
174192
                     }
175193
                     RelocKind::Branch26 => {
@@ -705,6 +723,8 @@ mod tests {
705723
             &[LayoutInput {
706724
                 id: input_id,
707725
                 object: &object,
726
+                load_order: 0,
727
+                archive_member_offset: None,
708728
             }],
709729
             &atoms,
710730
             &mut sym_table,
@@ -775,6 +795,8 @@ mod tests {
775795
             &[LayoutInput {
776796
                 id: input_id,
777797
                 object: &object,
798
+                load_order: 0,
799
+                archive_member_offset: None,
778800
             }],
779801
             &atoms,
780802
             &mut sym_table,
@@ -849,6 +871,8 @@ mod tests {
849871
             &[LayoutInput {
850872
                 id: input_id,
851873
                 object: &object,
874
+                load_order: 0,
875
+                archive_member_offset: None,
852876
             }],
853877
             &atoms,
854878
             &mut sym_table,
@@ -926,6 +950,8 @@ mod tests {
926950
             &[LayoutInput {
927951
                 id: input_id,
928952
                 object: &object,
953
+                load_order: 0,
954
+                archive_member_offset: None,
929955
             }],
930956
             &atoms,
931957
             &mut sym_table,
@@ -989,6 +1015,8 @@ mod tests {
9891015
             &[LayoutInput {
9901016
                 id: input_id,
9911017
                 object: &object,
1018
+                load_order: 0,
1019
+                archive_member_offset: None,
9921020
             }],
9931021
             &atoms,
9941022
             &mut sym_table,
@@ -1052,6 +1080,8 @@ mod tests {
10521080
             &[LayoutInput {
10531081
                 id: input_id,
10541082
                 object: &object,
1083
+                load_order: 0,
1084
+                archive_member_offset: None,
10551085
             }],
10561086
             &atoms,
10571087
             &mut sym_table,
src/synth/unwind.rsmodified
@@ -1300,12 +1300,21 @@ mod tests {
13001300
             .chunks_exact(4)
13011301
             .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
13021302
             .collect();
1303
-        assert_eq!(words[2], 0, "all encodings are unique so nothing should be common");
1304
-        assert_eq!(words[6], 3, "expected the encoding pressure to force a second page");
1303
+        assert_eq!(
1304
+            words[2], 0,
1305
+            "all encodings are unique so nothing should be common"
1306
+        );
1307
+        assert_eq!(
1308
+            words[6], 3,
1309
+            "expected the encoding pressure to force a second page"
1310
+        );
13051311
         let decoded = decode_unwind_info(&bytes).unwrap();
13061312
         assert_eq!(decoded.records.len(), records.len());
13071313
         assert_eq!(decoded.records[0].function_offset, 0x400);
1308
-        assert_eq!(decoded.records.last().unwrap().function_offset, 0x400 + 299 * 4);
1314
+        assert_eq!(
1315
+            decoded.records.last().unwrap().function_offset,
1316
+            0x400 + 299 * 4
1317
+        );
13091318
     }
13101319
 
13111320
     #[test]
tests/atom_integration.rsmodified
@@ -97,7 +97,7 @@ fn atomize_splits_text_at_symbol_boundaries_and_backpatches_symbols() {
9797
 
9898
     let bytes = fs::read(&obj_path).unwrap();
9999
     let mut inputs = Inputs::new();
100
-    let input_id = inputs.add_object(obj_path.clone(), bytes).unwrap();
100
+    let input_id = inputs.add_object(obj_path.clone(), bytes, 0).unwrap();
101101
 
102102
     // Seed the symbol table (produces Defined entries with AtomId(0)
103103
     // placeholders).
@@ -204,7 +204,7 @@ fn atomize_cstring_splits_at_null_terminators() {
204204
 
205205
     let bytes = fs::read(&obj_path).unwrap();
206206
     let mut inputs = Inputs::new();
207
-    let input_id = inputs.add_object(obj_path.clone(), bytes).unwrap();
207
+    let input_id = inputs.add_object(obj_path.clone(), bytes, 0).unwrap();
208208
     let mut sym_table = SymbolTable::new();
209209
     let _ = seed_all(&inputs, &mut sym_table).expect("seed_all");
210210
     let obj = inputs.object_file(input_id).unwrap();
tests/linker_run.rsmodified
@@ -16,8 +16,9 @@ use afs_ld::macho::constants::{
1616
     BIND_OPCODE_SET_DYLIB_ORDINAL_IMM, BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB,
1717
     BIND_OPCODE_SET_DYLIB_SPECIAL_IMM, BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB,
1818
     BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM, BIND_OPCODE_SET_TYPE_IMM,
19
-    BIND_SYMBOL_FLAGS_WEAK_IMPORT, DICE_KIND_JUMP_TABLE32, LC_BUILD_VERSION, LC_DATA_IN_CODE,
20
-    LC_DYLD_INFO_ONLY, LC_DYSYMTAB, LC_FUNCTION_STARTS, LC_SEGMENT_64, LC_SYMTAB,
19
+    BIND_SYMBOL_FLAGS_WEAK_IMPORT, DICE_KIND_JUMP_TABLE32, INDIRECT_SYMBOL_ABS,
20
+    INDIRECT_SYMBOL_LOCAL, LC_BUILD_VERSION, LC_DATA_IN_CODE, LC_DYLD_INFO_ONLY, LC_DYSYMTAB,
21
+    LC_FUNCTION_STARTS, LC_SEGMENT_64, LC_SYMTAB,
2122
     REBASE_IMMEDIATE_MASK, REBASE_OPCODE_ADD_ADDR_IMM_SCALED, REBASE_OPCODE_ADD_ADDR_ULEB,
2223
     REBASE_OPCODE_DONE, REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB, REBASE_OPCODE_DO_REBASE_IMM_TIMES,
2324
     REBASE_OPCODE_DO_REBASE_ULEB_TIMES, REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB,
@@ -64,6 +65,20 @@ fn sdk_version() -> Option<String> {
6465
     Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
6566
 }
6667
 
68
+fn find_runtime_archive() -> Option<PathBuf> {
69
+    let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
70
+    for profile in ["debug", "release"] {
71
+        let candidate = workspace
72
+            .join("target")
73
+            .join(profile)
74
+            .join("libarmfortas_rt.a");
75
+        if candidate.is_file() {
76
+            return Some(candidate);
77
+        }
78
+    }
79
+    None
80
+}
81
+
6782
 fn have_xcrun_tool(tool: &str) -> bool {
6883
     Command::new("xcrun")
6984
         .arg("-f")
@@ -246,6 +261,28 @@ fn segment_vmaddr(bytes: &[u8], segname: &str) -> Option<u64> {
246261
     None
247262
 }
248263
 
264
+fn symbol_values(bytes: &[u8]) -> HashMap<String, u64> {
265
+    let header = parse_header(bytes).unwrap();
266
+    let commands = parse_commands(&header, bytes).unwrap();
267
+    let symtab = commands
268
+        .iter()
269
+        .find_map(|cmd| match cmd {
270
+            LoadCommand::Symtab(cmd) => Some(*cmd),
271
+            _ => None,
272
+        })
273
+        .unwrap();
274
+    let symbols = parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).unwrap();
275
+    let strings = StringTable::from_file(bytes, symtab.stroff, symtab.strsize).unwrap();
276
+    let mut out = HashMap::new();
277
+    for symbol in symbols {
278
+        let Ok(name) = strings.get(symbol.strx()) else {
279
+            continue;
280
+        };
281
+        out.insert(name.to_string(), symbol.value());
282
+    }
283
+    out
284
+}
285
+
249286
 fn symtab_and_dysymtab(
250287
     bytes: &[u8],
251288
 ) -> (
@@ -445,6 +482,29 @@ fn indirect_symbol_table(bytes: &[u8]) -> Vec<u32> {
445482
         .collect()
446483
 }
447484
 
485
+fn indirect_symbol_identities(bytes: &[u8]) -> Vec<String> {
486
+    let (symtab, _) = symtab_and_dysymtab(bytes);
487
+    let symbols = parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).unwrap();
488
+    let strings = StringTable::from_file(bytes, symtab.stroff, symtab.strsize).unwrap();
489
+    indirect_symbol_table(bytes)
490
+        .into_iter()
491
+        .map(|index| {
492
+            if index & INDIRECT_SYMBOL_LOCAL != 0 {
493
+                if index & INDIRECT_SYMBOL_ABS != 0 {
494
+                    "<LOCAL|ABS>".to_string()
495
+                } else {
496
+                    "<LOCAL>".to_string()
497
+                }
498
+            } else if index & INDIRECT_SYMBOL_ABS != 0 {
499
+                "<ABS>".to_string()
500
+            } else {
501
+                let symbol = &symbols[index as usize];
502
+                strings.get(symbol.strx()).unwrap().to_string()
503
+            }
504
+        })
505
+        .collect()
506
+}
507
+
448508
 fn raw_linkedit_data_cmd(bytes: &[u8], expected_cmd: u32) -> (u32, u32) {
449509
     let header = parse_header(bytes).unwrap();
450510
     let commands = parse_commands(&header, bytes).unwrap();
@@ -3590,6 +3650,184 @@ fn linker_run_rebases_local_absolute_pointers_like_ld() {
35903650
     let _ = fs::remove_file(apple_out);
35913651
 }
35923652
 
3653
+#[test]
3654
+fn linker_run_routes_local_got_loads_through_rebased_slots() {
3655
+    if !have_xcrun() || !have_tool("codesign") {
3656
+        eprintln!("skipping: xcrun or codesign unavailable");
3657
+        return;
3658
+    }
3659
+    let Some(sdk) = sdk_path() else {
3660
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
3661
+        return;
3662
+    };
3663
+    let Some(sdk_ver) = sdk_version() else {
3664
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
3665
+        return;
3666
+    };
3667
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
3668
+    if !tbd.exists() {
3669
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
3670
+        return;
3671
+    }
3672
+
3673
+    let obj = scratch("local-got.o");
3674
+    let our_out = scratch("local-got-ours.out");
3675
+    let apple_out = scratch("local-got-apple.out");
3676
+    let src = r#"
3677
+        .section __TEXT,__text,regular,pure_instructions
3678
+        .globl _main
3679
+        _main:
3680
+            adrp x8, _value@GOTPAGE
3681
+            ldr x8, [x8, _value@GOTPAGEOFF]
3682
+            ldr w0, [x8]
3683
+            ret
3684
+
3685
+        .section __DATA,__data
3686
+        .globl _value
3687
+        .p2align 2
3688
+        _value:
3689
+            .long 7
3690
+        .subsections_via_symbols
3691
+    "#;
3692
+    if let Err(e) = assemble(src, &obj) {
3693
+        eprintln!("skipping: assemble failed: {e}");
3694
+        return;
3695
+    }
3696
+
3697
+    let opts = LinkOptions {
3698
+        inputs: vec![obj.clone(), tbd.clone()],
3699
+        output: Some(our_out.clone()),
3700
+        kind: OutputKind::Executable,
3701
+        ..LinkOptions::default()
3702
+    };
3703
+    Linker::run(&opts).unwrap();
3704
+    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
3705
+
3706
+    let our_bytes = fs::read(&our_out).unwrap();
3707
+    let apple_bytes = fs::read(&apple_out).unwrap();
3708
+    let our_binds = decode_bind_records(&our_bytes, false).unwrap();
3709
+    let apple_binds = decode_bind_records(&apple_bytes, false).unwrap();
3710
+    assert_eq!(our_binds, apple_binds);
3711
+    assert!(
3712
+        our_binds.iter().all(|record| record.symbol != "_value"),
3713
+        "local GOT target should not be emitted as a dylib bind: {our_binds:#?}"
3714
+    );
3715
+    assert_eq!(
3716
+        output_section(&our_bytes, "__DATA_CONST", "__got")
3717
+            .expect("missing __got section")
3718
+            .1
3719
+            .len(),
3720
+        8
3721
+    );
3722
+    let verify = Command::new("codesign")
3723
+        .arg("-v")
3724
+        .arg(&our_out)
3725
+        .output()
3726
+        .unwrap();
3727
+    assert!(
3728
+        verify.status.success(),
3729
+        "codesign verify failed: {}",
3730
+        String::from_utf8_lossy(&verify.stderr)
3731
+    );
3732
+    let status = Command::new(&our_out).status().unwrap();
3733
+    assert_eq!(
3734
+        status.code(),
3735
+        Some(7),
3736
+        "expected local GOT executable to exit 7"
3737
+    );
3738
+
3739
+    let _ = fs::remove_file(obj);
3740
+    let _ = fs::remove_file(our_out);
3741
+    let _ = fs::remove_file(apple_out);
3742
+}
3743
+
3744
+#[test]
3745
+fn linker_run_relaxes_hidden_got_loads_like_apple_ld() {
3746
+    if !have_xcrun() || !have_tool("codesign") {
3747
+        eprintln!("skipping: xcrun or codesign unavailable");
3748
+        return;
3749
+    }
3750
+    let Some(sdk) = sdk_path() else {
3751
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
3752
+        return;
3753
+    };
3754
+    let Some(sdk_ver) = sdk_version() else {
3755
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
3756
+        return;
3757
+    };
3758
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
3759
+    if !tbd.exists() {
3760
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
3761
+        return;
3762
+    }
3763
+
3764
+    let obj = scratch("hidden-got.o");
3765
+    let our_out = scratch("hidden-got-ours.out");
3766
+    let apple_out = scratch("hidden-got-apple.out");
3767
+    let src = r#"
3768
+        .section __TEXT,__text,regular,pure_instructions
3769
+        .globl _main
3770
+        _main:
3771
+            adrp x8, _value@GOTPAGE
3772
+            ldr x8, [x8, _value@GOTPAGEOFF]
3773
+            ldr w0, [x8]
3774
+            ret
3775
+
3776
+        .private_extern _value
3777
+        .section __DATA,__data
3778
+        .p2align 2
3779
+        _value:
3780
+            .long 7
3781
+        .subsections_via_symbols
3782
+    "#;
3783
+    if let Err(e) = assemble(src, &obj) {
3784
+        eprintln!("skipping: assemble failed: {e}");
3785
+        return;
3786
+    }
3787
+
3788
+    let opts = LinkOptions {
3789
+        inputs: vec![obj.clone(), tbd.clone()],
3790
+        output: Some(our_out.clone()),
3791
+        kind: OutputKind::Executable,
3792
+        ..LinkOptions::default()
3793
+    };
3794
+    Linker::run(&opts).unwrap();
3795
+    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
3796
+
3797
+    let our_bytes = fs::read(&our_out).unwrap();
3798
+    let apple_bytes = fs::read(&apple_out).unwrap();
3799
+    let (our_text_addr, our_text) = output_section(&our_bytes, "__TEXT", "__text").unwrap();
3800
+    let (apple_text_addr, apple_text) = output_section(&apple_bytes, "__TEXT", "__text").unwrap();
3801
+    assert_eq!(
3802
+        decode_page_reference(&our_text, our_text_addr, 0, &PageRefKind::Add).unwrap(),
3803
+        decode_page_reference(&apple_text, apple_text_addr, 0, &PageRefKind::Add).unwrap()
3804
+    );
3805
+    assert_eq!(our_text, apple_text);
3806
+    assert!(output_section(&our_bytes, "__DATA_CONST", "__got").is_none());
3807
+    assert!(output_section(&apple_bytes, "__DATA_CONST", "__got").is_none());
3808
+
3809
+    let verify = Command::new("codesign")
3810
+        .arg("-v")
3811
+        .arg(&our_out)
3812
+        .output()
3813
+        .unwrap();
3814
+    assert!(
3815
+        verify.status.success(),
3816
+        "codesign verify failed: {}",
3817
+        String::from_utf8_lossy(&verify.stderr)
3818
+    );
3819
+    let status = Command::new(&our_out).status().unwrap();
3820
+    assert_eq!(
3821
+        status.code(),
3822
+        Some(7),
3823
+        "expected hidden GOT executable to exit 7"
3824
+    );
3825
+
3826
+    let _ = fs::remove_file(obj);
3827
+    let _ = fs::remove_file(our_out);
3828
+    let _ = fs::remove_file(apple_out);
3829
+}
3830
+
35933831
 #[test]
35943832
 fn linker_run_partitions_symtab_like_ld() {
35953833
     if !have_xcrun() {
@@ -4252,8 +4490,14 @@ fn linker_run_resolves_backtrace_symbols_at_runtime() {
42524490
 
42534491
     assert_eq!(our_output.status.code(), Some(0));
42544492
     assert_eq!(apple_output.status.code(), Some(0));
4255
-    assert!(our_stdout.contains("helper"), "expected helper in output: {our_stdout}");
4256
-    assert!(our_stdout.contains("main"), "expected main in output: {our_stdout}");
4493
+    assert!(
4494
+        our_stdout.contains("helper"),
4495
+        "expected helper in output: {our_stdout}"
4496
+    );
4497
+    assert!(
4498
+        our_stdout.contains("main"),
4499
+        "expected main in output: {our_stdout}"
4500
+    );
42574501
     assert!(
42584502
         apple_stdout.contains("helper"),
42594503
         "expected helper in apple output: {apple_stdout}"
@@ -5056,3 +5300,301 @@ fn linker_run_routes_imported_tlv_through_got() {
50565300
     let _ = fs::remove_file(our_out);
50575301
     let _ = fs::remove_file(apple_out);
50585302
 }
5303
+
5304
+#[test]
5305
+fn linker_run_preserves_runtime_tlv_descriptor_offsets() {
5306
+    if !have_xcrun() || !have_tool("codesign") {
5307
+        eprintln!("skipping: xcrun or codesign unavailable");
5308
+        return;
5309
+    }
5310
+    let Some(runtime) = find_runtime_archive() else {
5311
+        eprintln!("skipping: libarmfortas_rt.a not built");
5312
+        return;
5313
+    };
5314
+    let Some(sdk) = sdk_path() else {
5315
+        eprintln!("skipping: no macOS SDK path");
5316
+        return;
5317
+    };
5318
+    let Some(sdk_ver) = sdk_version() else {
5319
+        eprintln!("skipping: no macOS SDK version");
5320
+        return;
5321
+    };
5322
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5323
+    if !tbd.exists() {
5324
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5325
+        return;
5326
+    }
5327
+
5328
+    let obj = scratch("runtime-hello.o");
5329
+    let out = scratch("runtime-hello.out");
5330
+    let apple_out = scratch("runtime-hello-apple.out");
5331
+    let src = r#"
5332
+        extern void afs_program_init(void);
5333
+        extern void afs_program_finalize(void);
5334
+        extern void afs_write_string(int, const char *, long);
5335
+        extern void afs_write_newline(int);
5336
+
5337
+        int main(void) {
5338
+            afs_program_init();
5339
+            afs_write_string(6, "Hello, World!", 13);
5340
+            afs_write_newline(6);
5341
+            afs_program_finalize();
5342
+            return 0;
5343
+        }
5344
+    "#;
5345
+    if let Err(e) = compile_c(src, &obj) {
5346
+        eprintln!("skipping: compile failed: {e}");
5347
+        return;
5348
+    }
5349
+
5350
+    let opts = LinkOptions {
5351
+        inputs: vec![obj.clone(), runtime.clone(), tbd],
5352
+        output: Some(out.clone()),
5353
+        kind: OutputKind::Executable,
5354
+        ..LinkOptions::default()
5355
+    };
5356
+    Linker::run(&opts).unwrap();
5357
+
5358
+    let apple = Command::new("xcrun")
5359
+        .args([
5360
+            "ld",
5361
+            "-arch",
5362
+            "arm64",
5363
+            "-platform_version",
5364
+            "macos",
5365
+            &sdk_ver,
5366
+            &sdk_ver,
5367
+            "-syslibroot",
5368
+            &sdk,
5369
+            "-lSystem",
5370
+            "-e",
5371
+            "_main",
5372
+            "-no_fixup_chains",
5373
+            "-o",
5374
+        ])
5375
+        .arg(&apple_out)
5376
+        .arg(&obj)
5377
+        .arg(&runtime)
5378
+        .output()
5379
+        .unwrap();
5380
+    assert!(
5381
+        apple.status.success(),
5382
+        "xcrun ld failed: {}",
5383
+        String::from_utf8_lossy(&apple.stderr)
5384
+    );
5385
+
5386
+    let verify = Command::new("codesign")
5387
+        .arg("-v")
5388
+        .arg(&out)
5389
+        .output()
5390
+        .unwrap();
5391
+    assert!(
5392
+        verify.status.success(),
5393
+        "codesign verify failed: {}",
5394
+        String::from_utf8_lossy(&verify.stderr)
5395
+    );
5396
+
5397
+    let bytes = fs::read(&out).unwrap();
5398
+    let apple_bytes = fs::read(&apple_out).unwrap();
5399
+    assert!(
5400
+        output_section(&bytes, "__DATA_CONST", "__const").is_some(),
5401
+        "runtime hello should promote file-backed __const data into __DATA_CONST"
5402
+    );
5403
+    assert!(
5404
+        output_section(&bytes, "__DATA", "__const").is_none(),
5405
+        "runtime hello should not leave file-backed __const data in __DATA"
5406
+    );
5407
+    let (thread_vars_addr, thread_vars) =
5408
+        output_section(&bytes, "__DATA", "__thread_vars").unwrap();
5409
+    let (thread_data_addr, _) = output_section(&bytes, "__DATA", "__thread_data").unwrap();
5410
+    let symbols = symbol_values(&bytes);
5411
+    let tlv_binds: Vec<_> = decode_bind_records(&bytes, false)
5412
+        .unwrap()
5413
+        .into_iter()
5414
+        .filter(|record| record.section == "__thread_vars")
5415
+        .collect();
5416
+    assert_eq!(
5417
+        tlv_binds.len(),
5418
+        thread_vars.len() / 24,
5419
+        "every TLV descriptor should carry exactly one bootstrap bind"
5420
+    );
5421
+    assert!(tlv_binds
5422
+        .iter()
5423
+        .all(|record| record.symbol == "__tlv_bootstrap"));
5424
+
5425
+    assert_eq!(
5426
+        decode_bind_records(&bytes, false).unwrap(),
5427
+        decode_bind_records(&apple_bytes, false).unwrap(),
5428
+        "runtime hello bind records diverged from Apple ld"
5429
+    );
5430
+    assert_eq!(
5431
+        decode_bind_records(&bytes, true).unwrap(),
5432
+        decode_bind_records(&apple_bytes, true).unwrap(),
5433
+        "runtime hello lazy-bind records diverged from Apple ld"
5434
+    );
5435
+    assert_eq!(
5436
+        decode_rebase_records(&bytes).unwrap(),
5437
+        decode_rebase_records(&apple_bytes).unwrap(),
5438
+        "runtime hello rebase records diverged from Apple ld"
5439
+    );
5440
+    assert_eq!(
5441
+        indirect_symbol_identities(&bytes),
5442
+        indirect_symbol_identities(&apple_bytes),
5443
+        "runtime hello indirect symbol identities diverged from Apple ld"
5444
+    );
5445
+
5446
+    for (name, descriptor_addr) in symbols.iter().filter(|(name, value)| {
5447
+        !name.ends_with("$tlv$init")
5448
+            && **value >= thread_vars_addr
5449
+            && **value < thread_vars_addr + thread_vars.len() as u64
5450
+    }) {
5451
+        let init_name = format!("{name}$tlv$init");
5452
+        let Some(init_addr) = symbols.get(&init_name) else {
5453
+            continue;
5454
+        };
5455
+        let offset = (*descriptor_addr - thread_vars_addr) as usize;
5456
+        let actual = u64::from_le_bytes(thread_vars[offset + 16..offset + 24].try_into().unwrap());
5457
+        let expected = init_addr - thread_data_addr;
5458
+        assert_eq!(
5459
+            actual, expected,
5460
+            "TLV descriptor {} should point at {} via template offset",
5461
+            name, init_name
5462
+        );
5463
+    }
5464
+
5465
+    let output = Command::new(&out).output().unwrap();
5466
+    let apple_output = Command::new(&apple_out).output().unwrap();
5467
+    assert_eq!(
5468
+        output.status.code(),
5469
+        Some(0),
5470
+        "expected runtime hello executable to exit 0, stderr={}",
5471
+        String::from_utf8_lossy(&output.stderr)
5472
+    );
5473
+    assert_eq!(
5474
+        apple_output.status.code(),
5475
+        Some(0),
5476
+        "expected Apple-linked runtime hello executable to exit 0, stderr={}",
5477
+        String::from_utf8_lossy(&apple_output.stderr)
5478
+    );
5479
+    assert_eq!(
5480
+        String::from_utf8_lossy(&output.stdout),
5481
+        String::from_utf8_lossy(&apple_output.stdout)
5482
+    );
5483
+
5484
+    let _ = fs::remove_file(obj);
5485
+    let _ = fs::remove_file(out);
5486
+    let _ = fs::remove_file(apple_out);
5487
+}
5488
+
5489
+#[test]
5490
+fn linker_run_rebases_runtime_init_metadata_like_apple_ld() {
5491
+    if !have_xcrun() || !have_tool("codesign") {
5492
+        eprintln!("skipping: xcrun or codesign unavailable");
5493
+        return;
5494
+    }
5495
+    let Some(runtime) = find_runtime_archive() else {
5496
+        eprintln!("skipping: libarmfortas_rt.a not built");
5497
+        return;
5498
+    };
5499
+    let Some(sdk) = sdk_path() else {
5500
+        eprintln!("skipping: no macOS SDK path");
5501
+        return;
5502
+    };
5503
+    let Some(sdk_ver) = sdk_version() else {
5504
+        eprintln!("skipping: no macOS SDK version");
5505
+        return;
5506
+    };
5507
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5508
+    if !tbd.exists() {
5509
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5510
+        return;
5511
+    }
5512
+
5513
+    let obj = scratch("runtime-init-only.o");
5514
+    let our_out = scratch("runtime-init-only-ours.out");
5515
+    let apple_out = scratch("runtime-init-only-apple.out");
5516
+    let src = r#"
5517
+        extern void afs_program_init(void);
5518
+
5519
+        int main(void) {
5520
+            afs_program_init();
5521
+            return 0;
5522
+        }
5523
+    "#;
5524
+    if let Err(e) = compile_c(src, &obj) {
5525
+        eprintln!("skipping: compile failed: {e}");
5526
+        return;
5527
+    }
5528
+
5529
+    let opts = LinkOptions {
5530
+        inputs: vec![obj.clone(), runtime.clone(), tbd],
5531
+        output: Some(our_out.clone()),
5532
+        kind: OutputKind::Executable,
5533
+        ..LinkOptions::default()
5534
+    };
5535
+    Linker::run(&opts).unwrap();
5536
+
5537
+    let apple = Command::new("xcrun")
5538
+        .args([
5539
+            "ld",
5540
+            "-arch",
5541
+            "arm64",
5542
+            "-platform_version",
5543
+            "macos",
5544
+            &sdk_ver,
5545
+            &sdk_ver,
5546
+            "-syslibroot",
5547
+            &sdk,
5548
+            "-lSystem",
5549
+            "-e",
5550
+            "_main",
5551
+            "-no_fixup_chains",
5552
+            "-o",
5553
+        ])
5554
+        .arg(&apple_out)
5555
+        .arg(&obj)
5556
+        .arg(&runtime)
5557
+        .output()
5558
+        .unwrap();
5559
+    assert!(
5560
+        apple.status.success(),
5561
+        "xcrun ld failed: {}",
5562
+        String::from_utf8_lossy(&apple.stderr)
5563
+    );
5564
+
5565
+    let our_bytes = fs::read(&our_out).unwrap();
5566
+    let apple_bytes = fs::read(&apple_out).unwrap();
5567
+    let our_rebases = decode_rebase_records(&our_bytes).unwrap();
5568
+    let apple_rebases = decode_rebase_records(&apple_bytes).unwrap();
5569
+    assert_eq!(
5570
+        our_rebases
5571
+            .iter()
5572
+            .filter(|record| record.section == "__const")
5573
+            .count(),
5574
+        apple_rebases
5575
+            .iter()
5576
+            .filter(|record| record.section == "__const")
5577
+            .count(),
5578
+        "runtime init const rebases diverged from Apple ld"
5579
+    );
5580
+    assert_eq!(
5581
+        our_rebases
5582
+            .iter()
5583
+            .filter(|record| record.section == "__la_symbol_ptr")
5584
+            .count(),
5585
+        apple_rebases
5586
+            .iter()
5587
+            .filter(|record| record.section == "__la_symbol_ptr")
5588
+            .count(),
5589
+        "runtime init lazy-pointer rebases diverged from Apple ld"
5590
+    );
5591
+
5592
+    let our_status = Command::new(&our_out).status().unwrap();
5593
+    let apple_status = Command::new(&apple_out).status().unwrap();
5594
+    assert_eq!(our_status.code(), Some(0));
5595
+    assert_eq!(apple_status.code(), Some(0));
5596
+
5597
+    let _ = fs::remove_file(obj);
5598
+    let _ = fs::remove_file(our_out);
5599
+    let _ = fs::remove_file(apple_out);
5600
+}
tests/resolve_integration.rsmodified
@@ -146,13 +146,13 @@ fn resolve_pipeline_pulls_archive_member_and_flags_missing() {
146146
     // Register the inputs with the resolver.
147147
     let mut inputs = Inputs::new();
148148
     let a_id = inputs
149
-        .add_object(a_o.clone(), fs::read(&a_o).unwrap())
149
+        .add_object(a_o.clone(), fs::read(&a_o).unwrap(), 0)
150150
         .unwrap();
151151
     inputs
152
-        .add_object(b_o.clone(), fs::read(&b_o).unwrap())
152
+        .add_object(b_o.clone(), fs::read(&b_o).unwrap(), 1)
153153
         .unwrap();
154154
     let archive_id = inputs
155
-        .add_archive(libtest.clone(), fs::read(&libtest).unwrap())
155
+        .add_archive(libtest.clone(), fs::read(&libtest).unwrap(), 2)
156156
         .unwrap();
157157
     let _ = archive_id;
158158