Comparing changes

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

base: ci-submodule-refs-20260416
compare: runtime-hello-parity
Create pull request
Able to merge. These branches can be automatically merged.
16 commits 15 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
@@ -35,6 +35,8 @@ use resolve::{
3535
     seed_all, DylibLoadMeta, InputAddError, Inputs, Symbol, SymbolTable, UndefinedTreatment,
3636
 };
3737
 
38
+const DEFAULT_TBD_VERSION: u32 = 1 << 16;
39
+
3840
 /// What kind of Mach-O file the linker is producing.
3941
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
4042
 pub enum OutputKind {
@@ -207,8 +209,8 @@ impl Linker {
207209
         }
208210
 
209211
         let mut inputs = Inputs::new();
210
-        for path in &opts.inputs {
211
-            register_input(&mut inputs, path)?;
212
+        for (load_order, path) in opts.inputs.iter().enumerate() {
213
+            register_input(&mut inputs, path, load_order)?;
212214
         }
213215
 
214216
         let mut sym_table = SymbolTable::new();
@@ -259,7 +261,15 @@ impl Linker {
259261
 
260262
         let layout_inputs: Vec<LayoutInput<'_>> = objects
261263
             .iter()
262
-            .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
+            })
263273
             .collect();
264274
         let mut dylib_loads = Vec::new();
265275
         let mut seen_ordinals = std::collections::BTreeSet::new();
@@ -355,11 +365,15 @@ fn default_output_path(opts: &LinkOptions) -> PathBuf {
355365
         .unwrap_or_else(|| PathBuf::from("a.out"))
356366
 }
357367
 
358
-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> {
359373
     let bytes = fs::read(path)?;
360374
     match path.extension().and_then(|ext| ext.to_str()) {
361375
         Some("a") => {
362
-            let _ = inputs.add_archive(path.to_path_buf(), bytes)?;
376
+            let _ = inputs.add_archive(path.to_path_buf(), bytes, load_order)?;
363377
         }
364378
         Some("dylib") => {
365379
             let _ = inputs.add_dylib(path.to_path_buf(), bytes)?;
@@ -385,12 +399,12 @@ fn register_input(inputs: &mut Inputs, path: &std::path::Path) -> Result<(), Lin
385399
                     .current_version
386400
                     .as_deref()
387401
                     .map(parse_version)
388
-                    .unwrap_or(0),
402
+                    .unwrap_or(DEFAULT_TBD_VERSION),
389403
                 compatibility_version: canonical
390404
                     .compatibility_version
391405
                     .as_deref()
392406
                     .map(parse_version)
393
-                    .unwrap_or(0),
407
+                    .unwrap_or(DEFAULT_TBD_VERSION),
394408
                 ordinal: inputs.next_dylib_ordinal(),
395409
             };
396410
             let mut loaded = false;
@@ -408,7 +422,7 @@ fn register_input(inputs: &mut Inputs, path: &std::path::Path) -> Result<(), Lin
408422
             }
409423
         }
410424
         _ => {
411
-            let _ = inputs.add_object(path.to_path_buf(), bytes)?;
425
+            let _ = inputs.add_object(path.to_path_buf(), bytes, load_order)?;
412426
         }
413427
     }
414428
     Ok(())
@@ -418,20 +432,37 @@ fn resolve_entry_point(
418432
     opts: &LinkOptions,
419433
     sym_table: &SymbolTable,
420434
 ) -> Result<Option<macho::writer::EntryPoint>, LinkError> {
421
-    let Some(name) = &opts.entry else {
435
+    let name = if let Some(name) = &opts.entry {
436
+        name.as_str()
437
+    } else if opts.kind == OutputKind::Executable {
438
+        if symbol_defined(sym_table, "_main") {
439
+            "_main"
440
+        } else if symbol_defined(sym_table, "_start") {
441
+            "_start"
442
+        } else {
443
+            return Ok(None);
444
+        }
445
+    } else {
422446
         return Ok(None);
423447
     };
424448
     let Some((symbol_id, _)) = sym_table
425449
         .iter()
426450
         .find(|(_, symbol)| sym_table.interner.resolve(symbol.name()) == name)
427451
     else {
428
-        return Err(LinkError::EntrySymbolNotFound(name.clone()));
452
+        return Err(LinkError::EntrySymbolNotFound(name.to_string()));
429453
     };
430454
     let Symbol::Defined { atom, value, .. } = sym_table.get(symbol_id) else {
431
-        return Err(LinkError::EntrySymbolNotFound(name.clone()));
455
+        return Err(LinkError::EntrySymbolNotFound(name.to_string()));
432456
     };
433457
     Ok(Some(macho::writer::EntryPoint {
434458
         atom: *atom,
435459
         atom_value: *value,
436460
     }))
437461
 }
462
+
463
+fn symbol_defined(sym_table: &SymbolTable, name: &str) -> bool {
464
+    sym_table.iter().any(|(_, symbol)| {
465
+        sym_table.interner.resolve(symbol.name()) == name
466
+            && matches!(symbol, Symbol::Defined { .. })
467
+    })
468
+}
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/dylib.rsmodified
@@ -16,6 +16,8 @@ use super::reader::{
1616
 };
1717
 use super::tbd::{parse_version, SymbolLists, Target, Tbd};
1818
 
19
+const DEFAULT_TBD_VERSION: u32 = 1 << 16;
20
+
1921
 /// How a consumer loaded this dylib. The filetype of the dylib itself is
2022
 /// always `MH_DYLIB`; this kind captures the *relationship*.
2123
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -222,12 +224,12 @@ impl DylibFile {
222224
                 .current_version
223225
                 .as_deref()
224226
                 .map(parse_version)
225
-                .unwrap_or(0),
227
+                .unwrap_or(DEFAULT_TBD_VERSION),
226228
             compatibility_version: tbd
227229
                 .compatibility_version
228230
                 .as_deref()
229231
                 .map(parse_version)
230
-                .unwrap_or(0),
232
+                .unwrap_or(DEFAULT_TBD_VERSION),
231233
             dependencies,
232234
             rpaths: Vec::new(),
233235
             symtab: None,
src/macho/writer.rsmodified
@@ -337,29 +337,44 @@ fn build_commands(
337337
         )?));
338338
     }
339339
 
340
-    commands.push(LoadCommand::BuildVersion(BuildVersionCmd {
341
-        platform: PLATFORM_MACOS,
342
-        minos: pack_version(11, 0, 0),
343
-        sdk: pack_version(11, 0, 0),
344
-        tools: vec![BuildTool {
345
-            tool: 3,
346
-            version: pack_version(0, 1, 0),
347
-        }],
348
-    }));
349
-    commands.push(raw_uuid_command(stable_uuid(layout, kind)));
350
-
351340
     match kind {
352341
         OutputKind::Executable => {
342
+            commands.push(LoadCommand::DyldInfoOnly(linkedit.dyld_info));
343
+            commands.push(LoadCommand::Symtab(linkedit.symtab));
344
+            commands.push(LoadCommand::Dysymtab(linkedit.dysymtab));
353345
             commands.push(raw_dylinker_command("/usr/lib/dyld"));
346
+            commands.push(raw_uuid_command(stable_uuid(layout, kind)));
347
+            commands.push(LoadCommand::BuildVersion(BuildVersionCmd {
348
+                platform: PLATFORM_MACOS,
349
+                minos: pack_version(11, 0, 0),
350
+                sdk: pack_version(11, 0, 0),
351
+                tools: vec![BuildTool {
352
+                    tool: 3,
353
+                    version: pack_version(0, 1, 0),
354
+                }],
355
+            }));
356
+            commands.push(raw_source_version_command(0));
354357
             commands.push(raw_entry_point(resolve_entryoff(layout, entry_point)?, 0));
355358
         }
356
-        OutputKind::Dylib => commands.push(LoadCommand::Dylib(DylibCmd {
357
-            cmd: LC_ID_DYLIB,
358
-            name: dylib_install_name(opts),
359
-            timestamp: 2,
360
-            current_version: pack_version(1, 0, 0),
361
-            compatibility_version: pack_version(1, 0, 0),
362
-        })),
359
+        OutputKind::Dylib => {
360
+            commands.push(LoadCommand::BuildVersion(BuildVersionCmd {
361
+                platform: PLATFORM_MACOS,
362
+                minos: pack_version(11, 0, 0),
363
+                sdk: pack_version(11, 0, 0),
364
+                tools: vec![BuildTool {
365
+                    tool: 3,
366
+                    version: pack_version(0, 1, 0),
367
+                }],
368
+            }));
369
+            commands.push(raw_uuid_command(stable_uuid(layout, kind)));
370
+            commands.push(LoadCommand::Dylib(DylibCmd {
371
+                cmd: LC_ID_DYLIB,
372
+                name: dylib_install_name(opts),
373
+                timestamp: 2,
374
+                current_version: pack_version(1, 0, 0),
375
+                compatibility_version: pack_version(1, 0, 0),
376
+            }));
377
+        }
363378
     }
364379
 
365380
     for dylib in dylibs {
@@ -372,8 +387,6 @@ fn build_commands(
372387
         }));
373388
     }
374389
 
375
-    commands.push(LoadCommand::Symtab(linkedit.symtab));
376
-    commands.push(LoadCommand::Dysymtab(linkedit.dysymtab));
377390
     commands.push(raw_linkedit_command(
378391
         LC_FUNCTION_STARTS,
379392
         linkedit.function_starts.dataoff,
@@ -393,7 +406,11 @@ fn build_commands(
393406
     } else {
394407
         commands.push(raw_linkedit_command(LC_CODE_SIGNATURE, 0, 0));
395408
     }
396
-    commands.push(LoadCommand::DyldInfoOnly(linkedit.dyld_info));
409
+    if kind == OutputKind::Dylib {
410
+        commands.push(LoadCommand::DyldInfoOnly(linkedit.dyld_info));
411
+        commands.push(LoadCommand::Symtab(linkedit.symtab));
412
+        commands.push(LoadCommand::Dysymtab(linkedit.dysymtab));
413
+    }
397414
 
398415
     Ok(commands)
399416
 }
@@ -421,7 +438,11 @@ fn estimate_header_size(
421438
     .wire_size() as u64;
422439
     size += 24;
423440
     size += match kind {
424
-        OutputKind::Executable => raw_dylinker_command("/usr/lib/dyld").cmdsize() as u64 + 24,
441
+        OutputKind::Executable => {
442
+            raw_dylinker_command("/usr/lib/dyld").cmdsize() as u64
443
+                + 24
444
+                + raw_source_version_command(0).cmdsize() as u64
445
+        }
425446
         OutputKind::Dylib => DylibCmd {
426447
             cmd: LC_ID_DYLIB,
427448
             name: dylib_install_name(opts),
@@ -530,6 +551,14 @@ fn raw_uuid_command(uuid: [u8; 16]) -> LoadCommand {
530551
     }
531552
 }
532553
 
554
+fn raw_source_version_command(version: u64) -> LoadCommand {
555
+    LoadCommand::Raw {
556
+        cmd: LC_SOURCE_VERSION,
557
+        cmdsize: 16,
558
+        data: version.to_le_bytes().to_vec(),
559
+    }
560
+}
561
+
533562
 fn raw_linkedit_command(cmd: u32, dataoff: u32, datasize: u32) -> LoadCommand {
534563
     let mut data = Vec::with_capacity(8);
535564
     data.extend_from_slice(&dataoff.to_le_bytes());
@@ -711,7 +740,6 @@ fn build_linkedit_plan(
711740
         .map(|record| (record.symbol, record))
712741
         .collect();
713742
     let symbol_plan = build_output_symbols(layout, kind, opts.strip_locals, inputs, &imports)?;
714
-
715743
     let mut symtab_bytes = Vec::new();
716744
     write_nlist_table(&symbol_plan.symbols, &mut symtab_bytes);
717745
 
@@ -721,30 +749,25 @@ fn build_linkedit_plan(
721749
         &mut indirect_symbols,
722750
         &mut indirect_starts,
723751
         ("__TEXT", "__stubs"),
724
-        synthetic_plan
725
-            .stubs
726
-            .entries
727
-            .iter()
728
-            .map(|entry| entry.symbol),
729
-        &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
+        }),
730755
     );
731756
     push_indirect_section(
732757
         &mut indirect_symbols,
733758
         &mut indirect_starts,
734759
         ("__DATA_CONST", "__got"),
735
-        synthetic_plan.got.entries.iter().map(|entry| entry.symbol),
736
-        &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
+        }),
737763
     );
738764
     push_indirect_section(
739765
         &mut indirect_symbols,
740766
         &mut indirect_starts,
741767
         ("__DATA", "__la_symbol_ptr"),
742
-        synthetic_plan
743
-            .lazy_pointers
744
-            .entries
745
-            .iter()
746
-            .map(|entry| entry.symbol),
747
-        &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
+        }),
748771
     );
749772
 
750773
     let mut indirect_bytes = Vec::with_capacity(indirect_symbols.len() * 4);
@@ -758,7 +781,8 @@ fn build_linkedit_plan(
758781
     let weak_bind_bytes = pad_dyld_info_stream(bind_streams.weak_bind);
759782
     let lazy_bind_bytes = pad_dyld_info_stream(bind_streams.lazy_bind);
760783
     let export_bytes = pad_dyld_info_stream(build_export_trie(&symbol_plan.exports));
761
-    let function_starts_bytes = build_function_starts(layout, inputs.0.atom_table)?;
784
+    let function_starts_bytes =
785
+        build_function_starts(layout, inputs.0.layout_inputs, inputs.0.atom_table)?;
762786
     let data_in_code_bytes =
763787
         build_data_in_code(layout, inputs.0.layout_inputs, inputs.0.atom_table)?;
764788
 
@@ -959,6 +983,11 @@ fn collect_rebase_sites(
959983
     inputs: LinkEditInputs<'_>,
960984
 ) -> Result<Vec<RebaseSite>, WriteError> {
961985
     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
+    )?);
962991
     let mut reloc_cache: HashMap<(InputId, u8), Vec<Reloc>> = HashMap::new();
963992
     let input_map: HashMap<InputId, &ObjectFile> = inputs
964993
         .0
@@ -1052,6 +1081,38 @@ fn collect_lazy_pointer_rebase_sites(
10521081
         .collect())
10531082
 }
10541083
 
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
+
10551116
 fn relocs_for_rebase<'a>(
10561117
     relocs: &'a [Reloc],
10571118
     atom: &crate::atom::Atom,
@@ -1075,15 +1136,20 @@ fn reloc_needs_rebase(obj: &ObjectFile, reloc: Reloc, sym_table: &SymbolTable) -
10751136
 
10761137
     match reloc.referent {
10771138
         Referent::Section(_) => true,
1078
-        Referent::Symbol(_) => match symbol_referent_id(obj, reloc.referent, sym_table) {
1079
-            Some(symbol_id) => match sym_table.get(symbol_id) {
1080
-                Symbol::DylibImport { .. } => false,
1081
-                Symbol::Defined { atom, .. } => atom.0 != 0,
1082
-                Symbol::Common { .. } => true,
1083
-                _ => false,
1084
-            },
1085
-            None => false,
1086
-        },
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
+        }
10871153
     }
10881154
 }
10891155
 
@@ -1103,11 +1169,19 @@ fn symbol_referent_id(
11031169
     Some(symbol_id)
11041170
 }
11051171
 
1106
-fn build_function_starts(layout: &Layout, atom_table: &AtomTable) -> Result<Vec<u8>, WriteError> {
1172
+fn build_function_starts(
1173
+    layout: &Layout,
1174
+    inputs: &[LayoutInput<'_>],
1175
+    atom_table: &AtomTable,
1176
+) -> Result<Vec<u8>, WriteError> {
11071177
     let image_base = layout
11081178
         .segment("__TEXT")
11091179
         .ok_or(WriteError::MissingSegment("__TEXT"))?
11101180
         .vm_addr;
1181
+    let input_map: HashMap<InputId, &ObjectFile> = inputs
1182
+        .iter()
1183
+        .map(|input| (input.id, input.object))
1184
+        .collect();
11111185
     let mut starts = Vec::new();
11121186
 
11131187
     for section in &layout.sections {
@@ -1122,6 +1196,37 @@ fn build_function_starts(layout: &Layout, atom_table: &AtomTable) -> Result<Vec<
11221196
                     section.addr + placed.offset + alt.offset_within_atom as u64 - image_base,
11231197
                 );
11241198
             }
1199
+            let Some(object) = input_map.get(&atom.origin) else {
1200
+                continue;
1201
+            };
1202
+            let Some(input_section) = object
1203
+                .sections
1204
+                .get((atom.input_section as usize).saturating_sub(1))
1205
+            else {
1206
+                continue;
1207
+            };
1208
+            let atom_start = input_section.addr + atom.input_offset as u64;
1209
+            let atom_end = atom_start + atom.size as u64;
1210
+            for input_sym in &object.symbols {
1211
+                if input_sym.stab_kind().is_some()
1212
+                    || input_sym.kind() != SymKind::Sect
1213
+                    || input_sym.alt_entry()
1214
+                    || input_sym.sect_idx() != atom.input_section
1215
+                {
1216
+                    continue;
1217
+                }
1218
+                let Ok(name) = object.symbol_name(input_sym) else {
1219
+                    continue;
1220
+                };
1221
+                if is_assembler_temporary_symbol(name) {
1222
+                    continue;
1223
+                }
1224
+                let value = input_sym.value();
1225
+                if !(atom_start < value && value < atom_end) {
1226
+                    continue;
1227
+                }
1228
+                starts.push(section.addr + placed.offset + (value - atom_start) - image_base);
1229
+            }
11251230
         }
11261231
     }
11271232
 
@@ -1158,6 +1263,7 @@ fn build_data_in_code(
11581263
         kind: u16,
11591264
     }
11601265
 
1266
+    let atoms_by_input_section = atom_table.by_input_section();
11611267
     let mut remapped = Vec::new();
11621268
     for (input_order, input) in inputs.iter().enumerate() {
11631269
         for (input_entry_index, entry) in input.object.data_in_code.iter().copied().enumerate() {
@@ -1165,6 +1271,7 @@ fn build_data_in_code(
11651271
                 remap_data_in_code_to_section(input.object, entry)?;
11661272
             let (atom_id, atom_delta) = find_containing_atom_range(
11671273
                 atom_table,
1274
+                &atoms_by_input_section,
11681275
                 input.id,
11691276
                 section_index,
11701277
                 section_relative,
@@ -1304,7 +1411,7 @@ fn collect_imports(
13041411
             ..
13051412
         } = symbol
13061413
         else {
1307
-            return Err(WriteError::ImportSymbolWrongKind(id));
1414
+            continue;
13081415
         };
13091416
         out.push(ImportSymbolRecord {
13101417
             symbol: id,
@@ -1326,6 +1433,7 @@ fn build_output_symbols(
13261433
 ) -> Result<SymbolTablePlan, WriteError> {
13271434
     let sym_table = inputs.0.sym_table;
13281435
     let atom_sections = atom_section_ordinals(layout);
1436
+    let atoms_by_input_section = inputs.0.atom_table.by_input_section();
13291437
     let image_base = layout.segment("__TEXT").map(|seg| seg.vm_addr).unwrap_or(0);
13301438
     let mut locals = Vec::new();
13311439
     let mut external_defineds = Vec::new();
@@ -1351,6 +1459,7 @@ fn build_output_symbols(
13511459
         collect_local_symbols(
13521460
             layout,
13531461
             inputs.0.atom_table,
1462
+            &atoms_by_input_section,
13541463
             &atom_sections,
13551464
             input.id,
13561465
             input.object,
@@ -1358,6 +1467,7 @@ fn build_output_symbols(
13581467
         )?;
13591468
     }
13601469
     collect_synthetic_local_symbols(layout, inputs.0.synthetic_plan, &mut locals)?;
1470
+    sort_local_symbols(&mut locals);
13611471
 
13621472
     for (symbol_id, symbol) in sym_table.iter() {
13631473
         let Symbol::Defined {
@@ -1504,6 +1614,16 @@ fn build_output_symbols(
15041614
     })
15051615
 }
15061616
 
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
+
15071627
 fn collect_synthetic_local_symbols(
15081628
     layout: &Layout,
15091629
     synthetic_plan: &SyntheticPlan,
@@ -1537,6 +1657,7 @@ fn collect_synthetic_local_symbols(
15371657
 fn collect_local_symbols(
15381658
     layout: &Layout,
15391659
     atom_table: &AtomTable,
1660
+    atoms_by_input_section: &HashMap<(InputId, u8), Vec<crate::resolve::AtomId>>,
15401661
     atom_sections: &HashMap<crate::resolve::AtomId, u8>,
15411662
     input_id: InputId,
15421663
     object: &ObjectFile,
@@ -1559,9 +1680,14 @@ fn collect_local_symbols(
15591680
                     .section_for_symbol(input_sym)
15601681
                     .expect("section symbol without section");
15611682
                 let offset = input_sym.value().saturating_sub(section.addr) as u32;
1562
-                let (atom_id, delta) =
1563
-                    find_containing_atom(atom_table, input_id, input_sym.sect_idx(), offset)
1564
-                        .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"))?;
15651691
                 let addr =
15661692
                     layout
15671693
                         .atom_addr(atom_id)
@@ -1610,30 +1736,40 @@ fn is_assembler_temporary_symbol(name: &str) -> bool {
16101736
 
16111737
 fn find_containing_atom(
16121738
     atom_table: &AtomTable,
1739
+    atoms_by_input_section: &HashMap<(InputId, u8), Vec<crate::resolve::AtomId>>,
16131740
     input_id: InputId,
16141741
     input_section: u8,
16151742
     offset: u32,
16161743
 ) -> Option<(crate::resolve::AtomId, u32)> {
1617
-    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
+    )
16181752
 }
16191753
 
16201754
 fn find_containing_atom_range(
16211755
     atom_table: &AtomTable,
1756
+    atoms_by_input_section: &HashMap<(InputId, u8), Vec<crate::resolve::AtomId>>,
16221757
     input_id: InputId,
16231758
     input_section: u8,
16241759
     offset: u32,
16251760
     len: u32,
16261761
 ) -> Option<(crate::resolve::AtomId, u32)> {
1627
-    let atoms = atom_table.by_input_section();
1628
-    atoms.get(&(input_id, input_section)).and_then(|ids| {
1629
-        ids.iter().find_map(|atom_id| {
1630
-            let atom = atom_table.get(*atom_id);
1631
-            let start = atom.input_offset;
1632
-            let end = atom.input_offset.saturating_add(atom.size);
1633
-            let range_end = offset.checked_add(len)?;
1634
-            (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
+            })
16351772
         })
1636
-    })
16371773
 }
16381774
 
16391775
 fn input_symbol_type(input_sym: &InputSymbol) -> u8 {
@@ -1767,20 +1903,34 @@ fn push_indirect_section(
17671903
     indirect_symbols: &mut Vec<u32>,
17681904
     indirect_starts: &mut HashMap<(String, String), u32>,
17691905
     key: (&str, &str),
1770
-    symbols: impl Iterator<Item = SymbolId>,
1771
-    symbol_indices: &HashMap<SymbolId, u32>,
1906
+    symbols: impl Iterator<Item = u32>,
17721907
 ) {
17731908
     let start = indirect_symbols.len() as u32;
17741909
     let mut saw_any = false;
17751910
     for symbol in symbols {
17761911
         saw_any = true;
1777
-        indirect_symbols.push(*symbol_indices.get(&symbol).expect("symbol index missing"));
1912
+        indirect_symbols.push(symbol);
17781913
     }
17791914
     if saw_any {
17801915
         indirect_starts.insert((key.0.to_string(), key.1.to_string()), start);
17811916
     }
17821917
 }
17831918
 
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
+
17841934
 fn build_bind_streams(
17851935
     layout: &Layout,
17861936
     synthetic_plan: &SyntheticPlan,
@@ -1791,34 +1941,6 @@ fn build_bind_streams(
17911941
     let mut lazy_bind = OpcodeStream::new();
17921942
     let mut lazy_offsets = HashMap::new();
17931943
 
1794
-    if !synthetic_plan.got.entries.is_empty() {
1795
-        let segment_index = segment_index(layout, "__DATA_CONST")?;
1796
-        let segment = layout
1797
-            .segment("__DATA_CONST")
1798
-            .ok_or(WriteError::MissingSegment("__DATA_CONST"))?;
1799
-        let section = layout
1800
-            .sections
1801
-            .iter()
1802
-            .find(|section| section.segment == "__DATA_CONST" && section.name == "__got")
1803
-            .ok_or(WriteError::MissingSegment("__DATA_CONST"))?;
1804
-        for (idx, entry) in synthetic_plan.got.entries.iter().enumerate() {
1805
-            let import = imports
1806
-                .get(&entry.symbol)
1807
-                .copied()
1808
-                .ok_or(WriteError::ImportSymbolMissing(entry.symbol))?;
1809
-            let slot_addr = section.addr + (idx as u64) * 8;
1810
-            bind_specs.push(BindRecordSpec {
1811
-                segment_index,
1812
-                segment_offset: slot_addr - segment.vm_addr,
1813
-                ordinal: import.ordinal,
1814
-                name: &import.name,
1815
-                weak_import: import.weak_import,
1816
-                addend: 0,
1817
-                terminate: false,
1818
-            });
1819
-        }
1820
-    }
1821
-
18221944
     if let Some(tlv_bootstrap) = synthetic_plan.tlv_bootstrap_symbol {
18231945
         let segment_index = segment_index(layout, "__DATA")?;
18241946
         let segment = layout
@@ -1852,6 +1974,33 @@ fn build_bind_streams(
18521974
         }
18531975
     }
18541976
 
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
+
18552004
     for entry in &synthetic_plan.direct_binds {
18562005
         let import = imports
18572006
             .get(&entry.symbol)
@@ -1865,6 +2014,13 @@ fn build_bind_streams(
18652014
             .iter()
18662015
             .find(|section| section.atoms.iter().any(|placed| placed.atom == entry.atom))
18672016
             .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
+        }
18682024
         let segment_index = segment_index(layout, &section.segment)?;
18692025
         let segment = layout
18702026
             .segment(&section.segment)
@@ -2267,10 +2423,53 @@ mod tests {
22672423
             ],
22682424
         };
22692425
 
2270
-        let blob = build_function_starts(&layout, &atoms).unwrap();
2426
+        let blob = build_function_starts(&layout, &[], &atoms).unwrap();
22712427
         assert_eq!(
22722428
             decode_function_starts_blob(&blob),
22732429
             vec![0x1000, 0x1008, 0x1040]
22742430
         );
22752431
     }
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
+    }
22762475
 }
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
@@ -14,6 +14,8 @@ const PAGE_SIZE: usize = 4096;
1414
 const UNWIND_INFO_VERSION: u32 = 1;
1515
 const UNWIND_SECOND_LEVEL_REGULAR: u32 = 2;
1616
 const UNWIND_SECOND_LEVEL_COMPRESSED: u32 = 3;
17
+const MAX_COMPRESSED_FUNCTION_DELTA: u32 = 0x00ff_ffff;
18
+const MAX_COMPRESSED_ENCODING_INDEX: usize = 0xff;
1719
 const FIRST_LEVEL_ENTRY_SIZE: usize = 12;
1820
 const FIRST_LEVEL_INDEX_GAP_SIZE: usize = FIRST_LEVEL_ENTRY_SIZE;
1921
 const COMPRESSED_PAGE_HEADER_SIZE: usize = 12;
@@ -539,9 +541,10 @@ fn read_u64(atom: &Atom, offset: usize) -> Result<u64, UnwindError> {
539541
 
540542
 fn serialize_unwind_info(records: &[UnwindRecord]) -> Result<Vec<u8>, UnwindReadError> {
541543
     let (records, personalities, lsdas) = finalize_unwind_records(records)?;
542
-    let pages = build_pages(&records);
544
+    let common_encodings = select_common_encodings(&records);
545
+    let pages = build_pages(&records, &common_encodings);
543546
     let common_encodings_offset = 7 * 4;
544
-    let common_encodings_count = 0u32;
547
+    let common_encodings_count = common_encodings.len() as u32;
545548
     let personalities_offset = common_encodings_offset + common_encodings_count as usize * 4;
546549
     let indices_offset = personalities_offset + personalities.len() * 4;
547550
     let indices_count = (pages.len() + 1) as u32;
@@ -564,6 +567,9 @@ fn serialize_unwind_info(records: &[UnwindRecord]) -> Result<Vec<u8>, UnwindRead
564567
     out.extend_from_slice(&(indices_offset as u32).to_le_bytes());
565568
     out.extend_from_slice(&indices_count.to_le_bytes());
566569
 
570
+    for encoding in &common_encodings {
571
+        out.extend_from_slice(&encoding.to_le_bytes());
572
+    }
567573
     for personality in &personalities {
568574
         out.extend_from_slice(&personality.to_le_bytes());
569575
     }
@@ -825,51 +831,87 @@ fn finalize_unwind_records(
825831
     Ok((finalized, personalities, lsdas))
826832
 }
827833
 
828
-fn build_pages(records: &[UnwindRecord]) -> Vec<CompressedPage> {
834
+fn select_common_encodings(records: &[UnwindRecord]) -> Vec<u32> {
835
+    let mut stats: HashMap<u32, (u32, usize)> = HashMap::new();
836
+    for (idx, record) in records.iter().enumerate() {
837
+        stats
838
+            .entry(record.encoding)
839
+            .and_modify(|(count, _)| *count += 1)
840
+            .or_insert((1, idx));
841
+    }
842
+
843
+    let mut encodings: Vec<(u32, u32, usize)> = stats
844
+        .into_iter()
845
+        .filter_map(|(encoding, (count, first_seen))| {
846
+            (count > 1).then_some((encoding, count, first_seen))
847
+        })
848
+        .collect();
849
+    encodings.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.2.cmp(&b.2)));
850
+    encodings.truncate(127);
851
+    encodings
852
+        .into_iter()
853
+        .map(|(encoding, _, _)| encoding)
854
+        .collect()
855
+}
856
+
857
+fn build_pages(records: &[UnwindRecord], common_encodings: &[u32]) -> Vec<CompressedPage> {
829858
     let mut pages = Vec::new();
830859
     let mut current: Option<CompressedPage> = None;
860
+    let common_indices: HashMap<u32, usize> = common_encodings
861
+        .iter()
862
+        .copied()
863
+        .enumerate()
864
+        .map(|(idx, encoding)| (encoding, idx))
865
+        .collect();
831866
 
832867
     for record in records {
833
-        let page = current.get_or_insert_with(|| CompressedPage {
834
-            start_function_offset: record.function_offset,
835
-            entries: Vec::new(),
836
-            local_encodings: Vec::new(),
837
-        });
838
-
839
-        let mut encodings = page.local_encodings.clone();
840
-        if encodings
841
-            .iter()
842
-            .all(|encoding| *encoding != record.encoding)
843
-        {
844
-            encodings.push(record.encoding);
845
-        }
846
-        let delta = record
847
-            .function_offset
848
-            .saturating_sub(page.start_function_offset);
849
-        let projected_size =
850
-            COMPRESSED_PAGE_HEADER_SIZE + (page.entries.len() + 1) * 4 + encodings.len() * 4;
851
-        if projected_size > PAGE_SIZE && !page.entries.is_empty() {
852
-            pages.push(current.take().unwrap());
853
-            current = Some(CompressedPage {
868
+        loop {
869
+            let page = current.get_or_insert_with(|| CompressedPage {
854870
                 start_function_offset: record.function_offset,
855871
                 entries: Vec::new(),
856872
                 local_encodings: Vec::new(),
857873
             });
858
-        }
859874
 
860
-        let page = current.as_mut().unwrap();
861
-        let encoding_index = if let Some(index) = page
862
-            .local_encodings
863
-            .iter()
864
-            .position(|encoding| *encoding == record.encoding)
865
-        {
866
-            index
867
-        } else {
868
-            page.local_encodings.push(record.encoding);
869
-            page.local_encodings.len() - 1
870
-        };
871
-        page.entries
872
-            .push(((encoding_index as u32) << 24) | (delta & 0x00ff_ffff));
875
+            let needs_local_encoding = !common_indices.contains_key(&record.encoding)
876
+                && page
877
+                    .local_encodings
878
+                    .iter()
879
+                    .all(|encoding| *encoding != record.encoding);
880
+            let prospective_local_count =
881
+                page.local_encodings.len() + usize::from(needs_local_encoding);
882
+            let delta = record
883
+                .function_offset
884
+                .saturating_sub(page.start_function_offset);
885
+            let projected_size = COMPRESSED_PAGE_HEADER_SIZE
886
+                + (page.entries.len() + 1) * 4
887
+                + prospective_local_count * 4;
888
+            let projected_encoding_count = common_encodings.len() + prospective_local_count;
889
+
890
+            if !page.entries.is_empty()
891
+                && (projected_size > PAGE_SIZE
892
+                    || delta > MAX_COMPRESSED_FUNCTION_DELTA
893
+                    || projected_encoding_count > MAX_COMPRESSED_ENCODING_INDEX + 1)
894
+            {
895
+                pages.push(current.take().unwrap());
896
+                continue;
897
+            }
898
+
899
+            let page = current.as_mut().unwrap();
900
+            let encoding_index = if let Some(index) = common_indices.get(&record.encoding) {
901
+                *index
902
+            } else if let Some(index) = page
903
+                .local_encodings
904
+                .iter()
905
+                .position(|encoding| *encoding == record.encoding)
906
+            {
907
+                common_encodings.len() + index
908
+            } else {
909
+                page.local_encodings.push(record.encoding);
910
+                common_encodings.len() + page.local_encodings.len() - 1
911
+            };
912
+            page.entries.push(((encoding_index as u32) << 24) | delta);
913
+            break;
914
+        }
873915
     }
874916
 
875917
     if let Some(page) = current {
@@ -1149,6 +1191,132 @@ mod tests {
11491191
         );
11501192
     }
11511193
 
1194
+    #[test]
1195
+    fn repeated_encodings_promote_to_common_table() {
1196
+        let bytes = serialize_unwind_info(&[
1197
+            UnwindRecord {
1198
+                function_offset: 0x348,
1199
+                code_len: 0x18,
1200
+                encoding: 0x0400_0000,
1201
+                personality_offset: None,
1202
+                lsda_offset: None,
1203
+            },
1204
+            UnwindRecord {
1205
+                function_offset: 0x360,
1206
+                code_len: 0x20,
1207
+                encoding: 0x0200_2000,
1208
+                personality_offset: None,
1209
+                lsda_offset: None,
1210
+            },
1211
+            UnwindRecord {
1212
+                function_offset: 0x390,
1213
+                code_len: 0x10,
1214
+                encoding: 0x0400_0000,
1215
+                personality_offset: None,
1216
+                lsda_offset: None,
1217
+            },
1218
+        ])
1219
+        .unwrap();
1220
+        let words: Vec<u32> = bytes
1221
+            .chunks_exact(4)
1222
+            .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
1223
+            .collect();
1224
+        assert_eq!(words[2], 1, "expected one promoted common encoding");
1225
+        assert_eq!(words[7], 0x0400_0000);
1226
+
1227
+        let decoded = decode_unwind_info(&bytes).unwrap();
1228
+        assert_eq!(
1229
+            decoded.records,
1230
+            vec![
1231
+                DecodedUnwindRecord {
1232
+                    function_offset: 0x348,
1233
+                    encoding: 0x0400_0000,
1234
+                },
1235
+                DecodedUnwindRecord {
1236
+                    function_offset: 0x360,
1237
+                    encoding: 0x0200_2000,
1238
+                },
1239
+                DecodedUnwindRecord {
1240
+                    function_offset: 0x390,
1241
+                    encoding: 0x0400_0000,
1242
+                },
1243
+            ]
1244
+        );
1245
+    }
1246
+
1247
+    #[test]
1248
+    fn large_function_gaps_start_new_pages_before_delta_overflow() {
1249
+        let bytes = serialize_unwind_info(&[
1250
+            UnwindRecord {
1251
+                function_offset: 0x348,
1252
+                code_len: 0x14,
1253
+                encoding: 0x0200_0000,
1254
+                personality_offset: None,
1255
+                lsda_offset: None,
1256
+            },
1257
+            UnwindRecord {
1258
+                function_offset: 0x0100_0360,
1259
+                code_len: 0x14,
1260
+                encoding: 0x0200_0000,
1261
+                personality_offset: None,
1262
+                lsda_offset: None,
1263
+            },
1264
+        ])
1265
+        .unwrap();
1266
+        let words: Vec<u32> = bytes
1267
+            .chunks_exact(4)
1268
+            .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
1269
+            .collect();
1270
+        assert_eq!(words[6], 3, "expected two pages plus the sentinel index");
1271
+        let decoded = decode_unwind_info(&bytes).unwrap();
1272
+        assert_eq!(
1273
+            decoded.records,
1274
+            vec![
1275
+                DecodedUnwindRecord {
1276
+                    function_offset: 0x348,
1277
+                    encoding: 0x0200_0000,
1278
+                },
1279
+                DecodedUnwindRecord {
1280
+                    function_offset: 0x0100_0360,
1281
+                    encoding: 0x0200_0000,
1282
+                },
1283
+            ]
1284
+        );
1285
+    }
1286
+
1287
+    #[test]
1288
+    fn pages_split_before_encoding_index_overflow() {
1289
+        let records = (0..300u32)
1290
+            .map(|idx| UnwindRecord {
1291
+                function_offset: 0x400 + idx * 4,
1292
+                code_len: 4,
1293
+                encoding: 0x0200_0000 | idx,
1294
+                personality_offset: None,
1295
+                lsda_offset: None,
1296
+            })
1297
+            .collect::<Vec<_>>();
1298
+        let bytes = serialize_unwind_info(&records).unwrap();
1299
+        let words: Vec<u32> = bytes
1300
+            .chunks_exact(4)
1301
+            .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
1302
+            .collect();
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
+        );
1311
+        let decoded = decode_unwind_info(&bytes).unwrap();
1312
+        assert_eq!(decoded.records.len(), records.len());
1313
+        assert_eq!(decoded.records[0].function_offset, 0x400);
1314
+        assert_eq!(
1315
+            decoded.records.last().unwrap().function_offset,
1316
+            0x400 + 299 * 4
1317
+        );
1318
+    }
1319
+
11521320
     #[test]
11531321
     fn decode_rejects_bad_encoding_index() {
11541322
         let mut bytes = serialize_unwind_info(&[UnwindRecord {
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
1389 lines changed — click to load
@@ -16,7 +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_DATA_IN_CODE, LC_FUNCTION_STARTS,
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,
2022
     REBASE_IMMEDIATE_MASK, REBASE_OPCODE_ADD_ADDR_IMM_SCALED, REBASE_OPCODE_ADD_ADDR_ULEB,
2123
     REBASE_OPCODE_DONE, REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB, REBASE_OPCODE_DO_REBASE_IMM_TIMES,
2224
     REBASE_OPCODE_DO_REBASE_ULEB_TIMES, REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB,
@@ -63,6 +65,20 @@ fn sdk_version() -> Option<String> {
6365
     Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
6466
 }
6567
 
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
+
6682
 fn have_xcrun_tool(tool: &str) -> bool {
6783
     Command::new("xcrun")
6884
         .arg("-f")
@@ -245,6 +261,28 @@ fn segment_vmaddr(bytes: &[u8], segname: &str) -> Option<u64> {
245261
     None
246262
 }
247263
 
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
+
248286
 fn symtab_and_dysymtab(
249287
     bytes: &[u8],
250288
 ) -> (
@@ -444,6 +482,29 @@ fn indirect_symbol_table(bytes: &[u8]) -> Vec<u32> {
444482
         .collect()
445483
 }
446484
 
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
+
447508
 fn raw_linkedit_data_cmd(bytes: &[u8], expected_cmd: u32) -> (u32, u32) {
448509
     let header = parse_header(bytes).unwrap();
449510
     let commands = parse_commands(&header, bytes).unwrap();
@@ -482,6 +543,31 @@ fn decode_function_starts(bytes: &[u8]) -> Vec<u64> {
482543
     offsets
483544
 }
484545
 
546
+fn command_ids(bytes: &[u8]) -> Vec<u32> {
547
+    let header = parse_header(bytes).unwrap();
548
+    let commands = parse_commands(&header, bytes).unwrap();
549
+    commands
550
+        .into_iter()
551
+        .map(|cmd| match cmd {
552
+            LoadCommand::Segment64(_) => LC_SEGMENT_64,
553
+            LoadCommand::Symtab(_) => LC_SYMTAB,
554
+            LoadCommand::Dysymtab(_) => LC_DYSYMTAB,
555
+            LoadCommand::BuildVersion(_) => LC_BUILD_VERSION,
556
+            LoadCommand::Dylib(d) => d.cmd,
557
+            LoadCommand::DyldInfoOnly(_) => LC_DYLD_INFO_ONLY,
558
+            LoadCommand::Raw { cmd, .. } => cmd,
559
+            other => panic!("unexpected load command in command_ids helper: {other:?}"),
560
+        })
561
+        .collect()
562
+}
563
+
564
+fn normalize_function_start_offsets(starts: &[u64]) -> Vec<u64> {
565
+    let Some(&base) = starts.first() else {
566
+        return Vec::new();
567
+    };
568
+    starts.iter().map(|offset| offset - base).collect()
569
+}
570
+
485571
 #[derive(Debug, Clone, PartialEq, Eq)]
486572
 struct DataInCodeRecord {
487573
     offset: u32,
@@ -489,30 +575,68 @@ struct DataInCodeRecord {
489575
     kind: u16,
490576
 }
491577
 
492
-fn normalized_unwind_words(bytes: &[u8]) -> Vec<u32> {
578
+fn rebased_unwind_bytes(bytes: &[u8]) -> Vec<u8> {
579
+    let header_base = segment_vmaddr(bytes, "__TEXT").unwrap_or(0);
580
+    let text_base = output_section(bytes, "__TEXT", "__text").unwrap().0 - header_base;
581
+    let got_range = output_section(bytes, "__DATA_CONST", "__got")
582
+        .map(|(addr, data)| (addr - header_base, addr - header_base + data.len() as u64));
583
+    let lsda_base =
584
+        output_section(bytes, "__TEXT", "__gcc_except_tab").map(|(addr, _)| addr - header_base);
493585
     let (_, unwind) = output_section(bytes, "__TEXT", "__unwind_info").unwrap();
494
-    let decoded = decode_unwind_info(&unwind).unwrap();
495
-    let mut words: Vec<u32> = unwind
496
-        .chunks_exact(4)
497
-        .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
498
-        .collect();
499
-    if words.len() < 7 {
500
-        return words;
501
-    }
502
-    let indices_offset_words = words[5] as usize / 4;
503
-    let indices_count = words[6] as usize;
504
-    if indices_count == 0 || words.len() < indices_offset_words + indices_count * 3 {
505
-        return words;
586
+    let mut out = unwind;
587
+    if out.len() < 28 {
588
+        return out;
589
+    }
590
+
591
+    let personalities_offset = u32_le(&out[12..16]) as usize;
592
+    let personalities_count = u32_le(&out[16..20]) as usize;
593
+    let indices_offset = u32_le(&out[20..24]) as usize;
594
+    let indices_count = u32_le(&out[24..28]) as usize;
595
+
596
+    for idx in 0..personalities_count {
597
+        let off = personalities_offset + idx * 4;
598
+        let value = u32_le(&out[off..off + 4]) as u64;
599
+        let rebased = if let Some((got_start, got_end)) = got_range {
600
+            if got_start <= value && value < got_end {
601
+                value - got_start
602
+            } else if value >= text_base {
603
+                value - text_base
604
+            } else {
605
+                value
606
+            }
607
+        } else if value >= text_base {
608
+            value - text_base
609
+        } else {
610
+            value
611
+        };
612
+        out[off..off + 4].copy_from_slice(&(rebased as u32).to_le_bytes());
506613
     }
507
-    let base = words[indices_offset_words];
614
+
615
+    let mut lsda_offsets = Vec::with_capacity(indices_count);
508616
     for idx in 0..indices_count {
509
-        let word = indices_offset_words + idx * 3;
510
-        words[word] = words[word].saturating_sub(base);
617
+        let entry_off = indices_offset + idx * 12;
618
+        let function_offset = u32_le(&out[entry_off..entry_off + 4]) as u64;
619
+        let rebased = function_offset.saturating_sub(text_base);
620
+        out[entry_off..entry_off + 4].copy_from_slice(&(rebased as u32).to_le_bytes());
621
+        lsda_offsets.push(u32_le(&out[entry_off + 8..entry_off + 12]) as usize);
511622
     }
512
-    if !decoded.records.is_empty() {
513
-        assert_eq!(decoded.records[0].function_offset, base);
623
+
624
+    if let (Some(lsda_base), Some(&start), Some(&end)) =
625
+        (lsda_base, lsda_offsets.first(), lsda_offsets.last())
626
+    {
627
+        let mut entry_off = start;
628
+        while entry_off < end {
629
+            let function_offset = u32_le(&out[entry_off..entry_off + 4]) as u64;
630
+            let lsda_offset = u32_le(&out[entry_off + 4..entry_off + 8]) as u64;
631
+            out[entry_off..entry_off + 4]
632
+                .copy_from_slice(&(function_offset.saturating_sub(text_base) as u32).to_le_bytes());
633
+            out[entry_off + 4..entry_off + 8]
634
+                .copy_from_slice(&(lsda_offset.saturating_sub(lsda_base) as u32).to_le_bytes());
635
+            entry_off += 8;
636
+        }
514637
     }
515
-    words
638
+
639
+    out
516640
 }
517641
 
518642
 fn normalized_eh_frame_dump(path: &PathBuf, text_base: u64) -> Result<String, String> {
@@ -1679,9 +1803,18 @@ fn linker_run_emits_non_empty_executable_from_real_object() {
16791803
         eprintln!("skipping: xcrun as or codesign unavailable");
16801804
         return;
16811805
     }
1806
+    let Some(sdk) = sdk_path() else {
1807
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
1808
+        return;
1809
+    };
1810
+    let Some(sdk_ver) = sdk_version() else {
1811
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
1812
+        return;
1813
+    };
16821814
 
16831815
     let obj = scratch("main.o");
16841816
     let out = scratch("a.out");
1817
+    let apple_out = scratch("a-apple.out");
16851818
     let src = r#"
16861819
         .section __TEXT,__text,regular,pure_instructions
16871820
         .globl _main
@@ -1702,13 +1835,16 @@ fn linker_run_emits_non_empty_executable_from_real_object() {
17021835
         ..LinkOptions::default()
17031836
     };
17041837
     Linker::run(&opts).unwrap();
1838
+    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
17051839
 
17061840
     let bytes = fs::read(&out).unwrap();
1841
+    let apple_bytes = fs::read(&apple_out).unwrap();
17071842
     let header = parse_header(&bytes).unwrap();
17081843
     let commands = parse_commands(&header, &bytes).unwrap();
17091844
     let mut text_size = 0u64;
17101845
     let mut has_dylinker = false;
17111846
     let mut has_uuid = false;
1847
+    let mut has_source_version = false;
17121848
     for cmd in commands {
17131849
         match cmd {
17141850
             LoadCommand::Segment64(seg) => {
@@ -1728,6 +1864,9 @@ fn linker_run_emits_non_empty_executable_from_real_object() {
17281864
             LoadCommand::Raw { cmd, data, .. } if cmd == afs_ld::macho::constants::LC_UUID => {
17291865
                 has_uuid = data.len() == 16 && data.iter().any(|byte| *byte != 0);
17301866
             }
1867
+            LoadCommand::Raw { cmd, .. } if cmd == afs_ld::macho::constants::LC_SOURCE_VERSION => {
1868
+                has_source_version = true;
1869
+            }
17311870
             _ => {}
17321871
         }
17331872
     }
@@ -1737,6 +1876,19 @@ fn linker_run_emits_non_empty_executable_from_real_object() {
17371876
         "expected LC_LOAD_DYLINKER in executable output"
17381877
     );
17391878
     assert!(has_uuid, "expected LC_UUID in executable output");
1879
+    assert!(
1880
+        has_source_version,
1881
+        "expected LC_SOURCE_VERSION in executable output"
1882
+    );
1883
+    let our_cmds: Vec<u32> = command_ids(&bytes)
1884
+        .into_iter()
1885
+        .filter(|cmd| *cmd != afs_ld::macho::constants::LC_LOAD_DYLIB)
1886
+        .collect();
1887
+    let apple_cmds: Vec<u32> = command_ids(&apple_bytes)
1888
+        .into_iter()
1889
+        .filter(|cmd| *cmd != afs_ld::macho::constants::LC_LOAD_DYLIB)
1890
+        .collect();
1891
+    assert_eq!(our_cmds, apple_cmds);
17401892
     assert!(
17411893
         fs::metadata(&out).unwrap().permissions().mode() & 0o111 != 0,
17421894
         "expected executable output mode"
@@ -1756,6 +1908,7 @@ fn linker_run_emits_non_empty_executable_from_real_object() {
17561908
 
17571909
     let _ = fs::remove_file(obj);
17581910
     let _ = fs::remove_file(out);
1911
+    let _ = fs::remove_file(apple_out);
17591912
 }
17601913
 
17611914
 #[test]
@@ -2357,6 +2510,51 @@ fn linker_run_uses_requested_entry_symbol() {
23572510
     let _ = fs::remove_file(out);
23582511
 }
23592512
 
2513
+#[test]
2514
+fn linker_run_defaults_entry_to_main_symbol() {
2515
+    if !have_xcrun() {
2516
+        eprintln!("skipping: xcrun as unavailable");
2517
+        return;
2518
+    }
2519
+
2520
+    let obj = scratch("default-entry.o");
2521
+    let out = scratch("default-entry.out");
2522
+    let src = r#"
2523
+        .section __TEXT,__text,regular,pure_instructions
2524
+        .globl _helper
2525
+        _helper:
2526
+            mov w0, #7
2527
+            ret
2528
+        .globl _main
2529
+        _main:
2530
+            mov w0, #0
2531
+            ret
2532
+        .subsections_via_symbols
2533
+    "#;
2534
+    if let Err(e) = assemble(src, &obj) {
2535
+        eprintln!("skipping: assemble failed: {e}");
2536
+        return;
2537
+    }
2538
+
2539
+    let opts = LinkOptions {
2540
+        inputs: vec![obj.clone()],
2541
+        output: Some(out.clone()),
2542
+        kind: OutputKind::Executable,
2543
+        ..LinkOptions::default()
2544
+    };
2545
+    Linker::run(&opts).unwrap();
2546
+
2547
+    let status = Command::new(&out).status().unwrap();
2548
+    assert_eq!(
2549
+        status.code(),
2550
+        Some(0),
2551
+        "default executable entry should prefer _main over the first text atom"
2552
+    );
2553
+
2554
+    let _ = fs::remove_file(obj);
2555
+    let _ = fs::remove_file(out);
2556
+}
2557
+
23602558
 #[test]
23612559
 fn linker_run_applies_core_arm64_relocations() {
23622560
     if !have_xcrun() {
@@ -2913,6 +3111,18 @@ fn linker_run_routes_dylib_imports_through_synthetic_sections() {
29133111
             _ => None,
29143112
         })
29153113
         .unwrap();
3114
+    let libsystem_load = commands
3115
+        .iter()
3116
+        .find_map(|cmd| match cmd {
3117
+            LoadCommand::Dylib(cmd)
3118
+                if cmd.cmd == afs_ld::macho::constants::LC_LOAD_DYLIB
3119
+                    && cmd.name == "/usr/lib/libSystem.B.dylib" =>
3120
+            {
3121
+                Some(cmd.clone())
3122
+            }
3123
+            _ => None,
3124
+        })
3125
+        .unwrap();
29163126
     let symbols = parse_nlist_table(&bytes, symtab.symoff, symtab.nsyms).unwrap();
29173127
     let strings = StringTable::from_file(&bytes, symtab.stroff, symtab.strsize).unwrap();
29183128
     let symbol_names: Vec<&str> = symbols
@@ -2933,6 +3143,8 @@ fn linker_run_routes_dylib_imports_through_synthetic_sections() {
29333143
     assert_eq!(got_hdr.reserved1, 1);
29343144
     assert_eq!(lazy_hdr.reserved1, 3);
29353145
     assert_eq!(stubs_hdr.reserved2, 12);
3146
+    assert!(libsystem_load.current_version >= (1 << 16));
3147
+    assert_eq!(libsystem_load.compatibility_version, 1 << 16);
29363148
     assert!(dyld_info.rebase_size > 0);
29373149
     assert!(dyld_info.bind_size > 0);
29383150
     assert!(dyld_info.lazy_bind_size > 0);
@@ -3439,126 +3651,304 @@ fn linker_run_rebases_local_absolute_pointers_like_ld() {
34393651
 }
34403652
 
34413653
 #[test]
3442
-fn linker_run_partitions_symtab_like_ld() {
3443
-    if !have_xcrun() {
3444
-        eprintln!("skipping: xcrun unavailable");
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");
34453657
         return;
34463658
     }
3447
-
3448
-    let dylib = scratch("symtab-partition.dylib");
3449
-    let obj = scratch("symtab-partition.o");
3450
-    let our_out = scratch("symtab-partition-ours.out");
3451
-    let apple_out = scratch("symtab-partition-apple.out");
3452
-
3453
-    let dylib_src = r#"
3454
-        int ext_data = 5;
3455
-    "#;
3456
-    if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
3457
-        eprintln!("skipping: dylib compile failed: {e}");
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());
34583670
         return;
34593671
     }
34603672
 
3461
-    let asm = r#"
3462
-        .text
3463
-        .private_extern _hidden
3464
-        .globl _visible
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
34653678
         .globl _main
3466
-        .p2align 2
3467
-    _local:
3468
-        ret
3469
-    _hidden:
3470
-        ret
3471
-    _visible:
3472
-        ret
3473
-    _main:
3474
-        ret
3679
+        _main:
3680
+            adrp x8, _value@GOTPAGE
3681
+            ldr x8, [x8, _value@GOTPAGEOFF]
3682
+            ldr w0, [x8]
3683
+            ret
34753684
 
3476
-        .data
3477
-        .quad _ext_data
3685
+        .section __DATA,__data
3686
+        .globl _value
3687
+        .p2align 2
3688
+        _value:
3689
+            .long 7
34783690
         .subsections_via_symbols
34793691
     "#;
3480
-    if let Err(e) = assemble(asm, &obj) {
3692
+    if let Err(e) = assemble(src, &obj) {
34813693
         eprintln!("skipping: assemble failed: {e}");
34823694
         return;
34833695
     }
34843696
 
34853697
     let opts = LinkOptions {
3486
-        inputs: vec![obj.clone(), dylib.clone()],
3698
+        inputs: vec![obj.clone(), tbd.clone()],
34873699
         output: Some(our_out.clone()),
34883700
         kind: OutputKind::Executable,
34893701
         ..LinkOptions::default()
34903702
     };
34913703
     Linker::run(&opts).unwrap();
3492
-
3493
-    let apple = Command::new("xcrun")
3494
-        .args(["ld", "-arch", "arm64", "-e", "_main", "-o"])
3495
-        .arg(&apple_out)
3496
-        .arg(&obj)
3497
-        .arg(&dylib)
3498
-        .output()
3499
-        .unwrap();
3500
-    assert!(
3501
-        apple.status.success(),
3502
-        "xcrun ld failed: {}",
3503
-        String::from_utf8_lossy(&apple.stderr)
3504
-    );
3704
+    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
35053705
 
35063706
     let our_bytes = fs::read(&our_out).unwrap();
35073707
     let apple_bytes = fs::read(&apple_out).unwrap();
3508
-    let (our_symtab, our_dysymtab) = symtab_and_dysymtab(&our_bytes);
3509
-    let (apple_symtab, apple_dysymtab) = symtab_and_dysymtab(&apple_bytes);
3510
-
3511
-    assert_eq!(our_symtab.nsyms, apple_symtab.nsyms);
3512
-    assert_eq!(our_dysymtab.ilocalsym, apple_dysymtab.ilocalsym);
3513
-    assert_eq!(our_dysymtab.nlocalsym, apple_dysymtab.nlocalsym);
3514
-    assert_eq!(our_dysymtab.iextdefsym, apple_dysymtab.iextdefsym);
3515
-    assert_eq!(our_dysymtab.nextdefsym, apple_dysymtab.nextdefsym);
3516
-    assert_eq!(our_dysymtab.iundefsym, apple_dysymtab.iundefsym);
3517
-    assert_eq!(our_dysymtab.nundefsym, apple_dysymtab.nundefsym);
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
+    );
35183715
     assert_eq!(
3519
-        canonical_symbol_records(&our_bytes),
3520
-        canonical_symbol_records(&apple_bytes)
3716
+        output_section(&our_bytes, "__DATA_CONST", "__got")
3717
+            .expect("missing __got section")
3718
+            .1
3719
+            .len(),
3720
+        8
35213721
     );
3522
-    assert_strtab_within_five_percent(
3523
-        &raw_string_table(&our_bytes),
3524
-        &raw_string_table(&apple_bytes),
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)
35253731
     );
3526
-
3732
+    let status = Command::new(&our_out).status().unwrap();
35273733
     assert_eq!(
3528
-        symbol_partition_names(&our_bytes),
3529
-        symbol_partition_names(&apple_bytes)
3734
+        status.code(),
3735
+        Some(7),
3736
+        "expected local GOT executable to exit 7"
35303737
     );
35313738
 
3532
-    let _ = fs::remove_file(dylib);
35333739
     let _ = fs::remove_file(obj);
35343740
     let _ = fs::remove_file(our_out);
35353741
     let _ = fs::remove_file(apple_out);
35363742
 }
35373743
 
35383744
 #[test]
3539
-fn linker_run_strips_locals_with_x_like_ld() {
3540
-    if !have_xcrun() {
3541
-        eprintln!("skipping: xcrun unavailable");
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");
35423748
         return;
35433749
     }
3544
-
3545
-    let dylib = scratch("symtab-strip.dylib");
3546
-    let obj = scratch("symtab-strip.o");
3547
-    let our_out = scratch("symtab-strip-ours.out");
3548
-    let apple_out = scratch("symtab-strip-apple.out");
3549
-
3550
-    let dylib_src = r#"
3551
-        int ext_data = 5;
3552
-    "#;
3553
-    if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
3554
-        eprintln!("skipping: dylib compile failed: {e}");
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());
35553761
         return;
35563762
     }
35573763
 
3558
-    let asm = r#"
3559
-        .text
3560
-        .private_extern _hidden
3561
-        .globl _visible
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
+
3831
+#[test]
3832
+fn linker_run_partitions_symtab_like_ld() {
3833
+    if !have_xcrun() {
3834
+        eprintln!("skipping: xcrun unavailable");
3835
+        return;
3836
+    }
3837
+
3838
+    let dylib = scratch("symtab-partition.dylib");
3839
+    let obj = scratch("symtab-partition.o");
3840
+    let our_out = scratch("symtab-partition-ours.out");
3841
+    let apple_out = scratch("symtab-partition-apple.out");
3842
+
3843
+    let dylib_src = r#"
3844
+        int ext_data = 5;
3845
+    "#;
3846
+    if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
3847
+        eprintln!("skipping: dylib compile failed: {e}");
3848
+        return;
3849
+    }
3850
+
3851
+    let asm = r#"
3852
+        .text
3853
+        .private_extern _hidden
3854
+        .globl _visible
3855
+        .globl _main
3856
+        .p2align 2
3857
+    _local:
3858
+        ret
3859
+    _hidden:
3860
+        ret
3861
+    _visible:
3862
+        ret
3863
+    _main:
3864
+        ret
3865
+
3866
+        .data
3867
+        .quad _ext_data
3868
+        .subsections_via_symbols
3869
+    "#;
3870
+    if let Err(e) = assemble(asm, &obj) {
3871
+        eprintln!("skipping: assemble failed: {e}");
3872
+        return;
3873
+    }
3874
+
3875
+    let opts = LinkOptions {
3876
+        inputs: vec![obj.clone(), dylib.clone()],
3877
+        output: Some(our_out.clone()),
3878
+        kind: OutputKind::Executable,
3879
+        ..LinkOptions::default()
3880
+    };
3881
+    Linker::run(&opts).unwrap();
3882
+
3883
+    let apple = Command::new("xcrun")
3884
+        .args(["ld", "-arch", "arm64", "-e", "_main", "-o"])
3885
+        .arg(&apple_out)
3886
+        .arg(&obj)
3887
+        .arg(&dylib)
3888
+        .output()
3889
+        .unwrap();
3890
+    assert!(
3891
+        apple.status.success(),
3892
+        "xcrun ld failed: {}",
3893
+        String::from_utf8_lossy(&apple.stderr)
3894
+    );
3895
+
3896
+    let our_bytes = fs::read(&our_out).unwrap();
3897
+    let apple_bytes = fs::read(&apple_out).unwrap();
3898
+    let (our_symtab, our_dysymtab) = symtab_and_dysymtab(&our_bytes);
3899
+    let (apple_symtab, apple_dysymtab) = symtab_and_dysymtab(&apple_bytes);
3900
+
3901
+    assert_eq!(our_symtab.nsyms, apple_symtab.nsyms);
3902
+    assert_eq!(our_dysymtab.ilocalsym, apple_dysymtab.ilocalsym);
3903
+    assert_eq!(our_dysymtab.nlocalsym, apple_dysymtab.nlocalsym);
3904
+    assert_eq!(our_dysymtab.iextdefsym, apple_dysymtab.iextdefsym);
3905
+    assert_eq!(our_dysymtab.nextdefsym, apple_dysymtab.nextdefsym);
3906
+    assert_eq!(our_dysymtab.iundefsym, apple_dysymtab.iundefsym);
3907
+    assert_eq!(our_dysymtab.nundefsym, apple_dysymtab.nundefsym);
3908
+    assert_eq!(
3909
+        canonical_symbol_records(&our_bytes),
3910
+        canonical_symbol_records(&apple_bytes)
3911
+    );
3912
+    assert_strtab_within_five_percent(
3913
+        &raw_string_table(&our_bytes),
3914
+        &raw_string_table(&apple_bytes),
3915
+    );
3916
+
3917
+    assert_eq!(
3918
+        symbol_partition_names(&our_bytes),
3919
+        symbol_partition_names(&apple_bytes)
3920
+    );
3921
+
3922
+    let _ = fs::remove_file(dylib);
3923
+    let _ = fs::remove_file(obj);
3924
+    let _ = fs::remove_file(our_out);
3925
+    let _ = fs::remove_file(apple_out);
3926
+}
3927
+
3928
+#[test]
3929
+fn linker_run_strips_locals_with_x_like_ld() {
3930
+    if !have_xcrun() {
3931
+        eprintln!("skipping: xcrun unavailable");
3932
+        return;
3933
+    }
3934
+
3935
+    let dylib = scratch("symtab-strip.dylib");
3936
+    let obj = scratch("symtab-strip.o");
3937
+    let our_out = scratch("symtab-strip-ours.out");
3938
+    let apple_out = scratch("symtab-strip-apple.out");
3939
+
3940
+    let dylib_src = r#"
3941
+        int ext_data = 5;
3942
+    "#;
3943
+    if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
3944
+        eprintln!("skipping: dylib compile failed: {e}");
3945
+        return;
3946
+    }
3947
+
3948
+    let asm = r#"
3949
+        .text
3950
+        .private_extern _hidden
3951
+        .globl _visible
35623952
         .globl _main
35633953
         .p2align 2
35643954
     _local:
@@ -3676,8 +4066,8 @@ fn linker_run_emits_leaf_unwind_info_like_ld() {
36764066
     let our_bytes = fs::read(&our_out).unwrap();
36774067
     let apple_bytes = fs::read(&apple_out).unwrap();
36784068
     assert_eq!(
3679
-        normalized_unwind_words(&our_bytes),
3680
-        normalized_unwind_words(&apple_bytes)
4069
+        rebased_unwind_bytes(&our_bytes),
4070
+        rebased_unwind_bytes(&apple_bytes)
36814071
     );
36824072
     assert!(output_section(&our_bytes, "__LD", "__compact_unwind").is_none());
36834073
 
@@ -3730,8 +4120,8 @@ fn linker_run_emits_multi_function_unwind_info_like_ld() {
37304120
     let our_bytes = fs::read(&our_out).unwrap();
37314121
     let apple_bytes = fs::read(&apple_out).unwrap();
37324122
     assert_eq!(
3733
-        normalized_unwind_words(&our_bytes),
3734
-        normalized_unwind_words(&apple_bytes)
4123
+        rebased_unwind_bytes(&our_bytes),
4124
+        rebased_unwind_bytes(&apple_bytes)
37354125
     );
37364126
     assert!(output_section(&our_bytes, "__LD", "__compact_unwind").is_none());
37374127
 
@@ -3740,6 +4130,61 @@ fn linker_run_emits_multi_function_unwind_info_like_ld() {
37404130
     let _ = fs::remove_file(apple_out);
37414131
 }
37424132
 
4133
+#[test]
4134
+fn linker_run_handles_large_unwind_function_gaps() {
4135
+    if !have_xcrun() {
4136
+        eprintln!("skipping: xcrun unavailable");
4137
+        return;
4138
+    }
4139
+
4140
+    let obj = scratch("unwind-gap.o");
4141
+    let out = scratch("unwind-gap-ours.out");
4142
+    let asm = r#"
4143
+        .text
4144
+        .globl _main
4145
+        .p2align 2
4146
+    _main:
4147
+        .cfi_startproc
4148
+        bl _helper
4149
+        ret
4150
+        .cfi_endproc
4151
+        .space 0x1000010
4152
+        .globl _helper
4153
+        .p2align 2
4154
+    _helper:
4155
+        .cfi_startproc
4156
+        ret
4157
+        .cfi_endproc
4158
+        .subsections_via_symbols
4159
+    "#;
4160
+    if let Err(e) = assemble(asm, &obj) {
4161
+        eprintln!("skipping: assemble failed: {e}");
4162
+        return;
4163
+    }
4164
+
4165
+    let opts = LinkOptions {
4166
+        inputs: vec![obj.clone()],
4167
+        output: Some(out.clone()),
4168
+        kind: OutputKind::Executable,
4169
+        ..LinkOptions::default()
4170
+    };
4171
+    Linker::run(&opts).unwrap();
4172
+
4173
+    let bytes = fs::read(&out).unwrap();
4174
+    let (_, unwind) = output_section(&bytes, "__TEXT", "__unwind_info").unwrap();
4175
+    let decoded = decode_unwind_info(&unwind).unwrap();
4176
+    assert!(
4177
+        decoded
4178
+            .records
4179
+            .windows(2)
4180
+            .all(|pair| pair[0].function_offset < pair[1].function_offset),
4181
+        "expected strictly ascending unwind records after large-gap pagination"
4182
+    );
4183
+
4184
+    let _ = fs::remove_file(obj);
4185
+    let _ = fs::remove_file(out);
4186
+}
4187
+
37434188
 #[test]
37444189
 fn linker_run_preserves_eh_frame_like_ld() {
37454190
     if !have_xcrun() || !have_xcrun_tool("dwarfdump") {
@@ -3826,6 +4271,79 @@ fn linker_run_preserves_eh_frame_like_ld() {
38264271
     let _ = fs::remove_file(apple_out);
38274272
 }
38284273
 
4274
+#[test]
4275
+fn linker_run_emits_backtrace_metadata_like_apple_ld() {
4276
+    if !have_xcrun() {
4277
+        eprintln!("skipping: xcrun unavailable");
4278
+        return;
4279
+    }
4280
+    let Some(sdk) = sdk_path() else {
4281
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4282
+        return;
4283
+    };
4284
+    let Some(sdk_ver) = sdk_version() else {
4285
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
4286
+        return;
4287
+    };
4288
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4289
+    if !tbd.exists() {
4290
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4291
+        return;
4292
+    }
4293
+
4294
+    let obj = scratch("unwind-backtrace.o");
4295
+    let our_out = scratch("unwind-backtrace-ours.out");
4296
+    let apple_out = scratch("unwind-backtrace-apple.out");
4297
+    let src = r#"
4298
+        #include <unwind.h>
4299
+
4300
+        static _Unwind_Reason_Code cb(struct _Unwind_Context* ctx, void* arg) {
4301
+            (void)ctx;
4302
+            int* count = (int*)arg;
4303
+            (*count)++;
4304
+            return *count >= 8 ? _URC_END_OF_STACK : _URC_NO_REASON;
4305
+        }
4306
+
4307
+        __attribute__((noinline)) int helper(void) {
4308
+            int count = 0;
4309
+            _Unwind_Backtrace(cb, &count);
4310
+            return count;
4311
+        }
4312
+
4313
+        int main(void) {
4314
+            return helper() > 1 ? 0 : 1;
4315
+        }
4316
+    "#;
4317
+    if let Err(e) = compile_c(src, &obj) {
4318
+        eprintln!("skipping: clang compile failed: {e}");
4319
+        return;
4320
+    }
4321
+
4322
+    let opts = LinkOptions {
4323
+        inputs: vec![obj.clone(), tbd],
4324
+        output: Some(our_out.clone()),
4325
+        kind: OutputKind::Executable,
4326
+        ..LinkOptions::default()
4327
+    };
4328
+    Linker::run(&opts).unwrap();
4329
+    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4330
+
4331
+    let our_bytes = fs::read(&our_out).unwrap();
4332
+    let apple_bytes = fs::read(&apple_out).unwrap();
4333
+    assert_eq!(
4334
+        rebased_unwind_bytes(&our_bytes),
4335
+        rebased_unwind_bytes(&apple_bytes)
4336
+    );
4337
+    assert_eq!(
4338
+        normalize_function_start_offsets(&decode_function_starts(&our_bytes)),
4339
+        normalize_function_start_offsets(&decode_function_starts(&apple_bytes))
4340
+    );
4341
+
4342
+    let _ = fs::remove_file(obj);
4343
+    let _ = fs::remove_file(our_out);
4344
+    let _ = fs::remove_file(apple_out);
4345
+}
4346
+
38294347
 #[test]
38304348
 fn linker_run_preserves_exception_unwind_metadata_like_apple_ld() {
38314349
     if !have_xcrun() || !have_xcrun_tool("clang++") || !have_tool("codesign") {
@@ -3837,61 +4355,157 @@ fn linker_run_preserves_exception_unwind_metadata_like_apple_ld() {
38374355
         eprintln!("skipping: xcrun --show-sdk-path unavailable");
38384356
         return;
38394357
     };
3840
-    let libsystem = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
3841
-    let libcxx = PathBuf::from(format!("{sdk}/usr/lib/libc++.tbd"));
3842
-    if !libsystem.exists() {
3843
-        eprintln!("skipping: no libSystem.tbd at {}", libsystem.display());
4358
+    let libsystem = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4359
+    let libcxx = PathBuf::from(format!("{sdk}/usr/lib/libc++.tbd"));
4360
+    if !libsystem.exists() {
4361
+        eprintln!("skipping: no libSystem.tbd at {}", libsystem.display());
4362
+        return;
4363
+    }
4364
+    if !libcxx.exists() {
4365
+        eprintln!("skipping: no libc++.tbd at {}", libcxx.display());
4366
+        return;
4367
+    }
4368
+
4369
+    let obj = scratch("cxx-exc.o");
4370
+    let our_out = scratch("cxx-exc-ours.out");
4371
+    let apple_out = scratch("cxx-exc-apple.out");
4372
+    let src = r#"
4373
+        int helper() { throw 7; }
4374
+        int main() {
4375
+            try { return helper(); }
4376
+            catch (...) { return 42; }
4377
+        }
4378
+    "#;
4379
+    if let Err(e) = compile_cxx(src, &obj) {
4380
+        eprintln!("skipping: clang++ compile failed: {e}");
4381
+        return;
4382
+    }
4383
+
4384
+    let opts = LinkOptions {
4385
+        inputs: vec![obj.clone(), libcxx.clone(), libsystem.clone()],
4386
+        output: Some(our_out.clone()),
4387
+        kind: OutputKind::Executable,
4388
+        ..LinkOptions::default()
4389
+    };
4390
+    Linker::run(&opts).unwrap();
4391
+    apple_link_cxx_classic(&obj, &apple_out).unwrap();
4392
+
4393
+    let our_bytes = fs::read(&our_out).unwrap();
4394
+    let apple_bytes = fs::read(&apple_out).unwrap();
4395
+    assert_eq!(
4396
+        decode_bind_records(&our_bytes, false).unwrap(),
4397
+        decode_bind_records(&apple_bytes, false).unwrap()
4398
+    );
4399
+    assert_eq!(
4400
+        decode_bind_records(&our_bytes, true).unwrap(),
4401
+        decode_bind_records(&apple_bytes, true).unwrap()
4402
+    );
4403
+    assert_eq!(
4404
+        canonical_lazy_bind_stream(&our_bytes).unwrap(),
4405
+        canonical_lazy_bind_stream(&apple_bytes).unwrap()
4406
+    );
4407
+    let our_decoded = canonical_unwind_info(&our_bytes);
4408
+    let apple_decoded = canonical_unwind_info(&apple_bytes);
4409
+    assert_eq!(our_decoded, apple_decoded);
4410
+    assert_eq!(our_decoded.personalities.len(), 1);
4411
+    assert_eq!(our_decoded.lsdas.len(), 1);
4412
+    assert!(output_section(&our_bytes, "__TEXT", "__gcc_except_tab").is_some());
4413
+    let our_status = Command::new(&our_out).status().unwrap();
4414
+    let apple_status = Command::new(&apple_out).status().unwrap();
4415
+    assert_eq!(our_status.code(), Some(42));
4416
+    assert_eq!(apple_status.code(), Some(42));
4417
+
4418
+    let _ = fs::remove_file(obj);
4419
+    let _ = fs::remove_file(our_out);
4420
+    let _ = fs::remove_file(apple_out);
4421
+}
4422
+
4423
+#[test]
4424
+fn linker_run_resolves_backtrace_symbols_at_runtime() {
4425
+    if !have_xcrun() {
4426
+        eprintln!("skipping: xcrun unavailable");
4427
+        return;
4428
+    }
4429
+    let Some(sdk) = sdk_path() else {
4430
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4431
+        return;
4432
+    };
4433
+    let Some(sdk_ver) = sdk_version() else {
4434
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
38444435
         return;
3845
-    }
3846
-    if !libcxx.exists() {
3847
-        eprintln!("skipping: no libc++.tbd at {}", libcxx.display());
4436
+    };
4437
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4438
+    if !tbd.exists() {
4439
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
38484440
         return;
38494441
     }
38504442
 
3851
-    let obj = scratch("cxx-exc.o");
3852
-    let our_out = scratch("cxx-exc-ours.out");
3853
-    let apple_out = scratch("cxx-exc-apple.out");
4443
+    let obj = scratch("execinfo-backtrace.o");
4444
+    let our_out = scratch("execinfo-backtrace-ours.out");
4445
+    let apple_out = scratch("execinfo-backtrace-apple.out");
38544446
     let src = r#"
3855
-        int helper() { throw 7; }
3856
-        int main() {
3857
-            try { return helper(); }
3858
-            catch (...) { return 42; }
4447
+        #include <execinfo.h>
4448
+        #include <stdio.h>
4449
+        #include <stdlib.h>
4450
+        #include <string.h>
4451
+
4452
+        __attribute__((noinline)) int helper(void) {
4453
+            void *frames[8];
4454
+            int n = backtrace(frames, 8);
4455
+            char **syms = backtrace_symbols(frames, n);
4456
+            int saw_helper = 0;
4457
+            int saw_main = 0;
4458
+            if (!syms) return 2;
4459
+            for (int i = 0; i < n; i++) {
4460
+                puts(syms[i]);
4461
+                saw_helper |= strstr(syms[i], "helper") != NULL;
4462
+                saw_main |= strstr(syms[i], "main") != NULL;
4463
+            }
4464
+            free(syms);
4465
+            return (saw_helper && saw_main) ? 0 : 1;
4466
+        }
4467
+
4468
+        int main(void) {
4469
+            return helper();
38594470
         }
38604471
     "#;
3861
-    if let Err(e) = compile_cxx(src, &obj) {
3862
-        eprintln!("skipping: clang++ compile failed: {e}");
4472
+    if let Err(e) = compile_c(src, &obj) {
4473
+        eprintln!("skipping: clang compile failed: {e}");
38634474
         return;
38644475
     }
38654476
 
38664477
     let opts = LinkOptions {
3867
-        inputs: vec![obj.clone(), libcxx.clone(), libsystem.clone()],
4478
+        inputs: vec![obj.clone(), tbd],
38684479
         output: Some(our_out.clone()),
38694480
         kind: OutputKind::Executable,
38704481
         ..LinkOptions::default()
38714482
     };
38724483
     Linker::run(&opts).unwrap();
3873
-    apple_link_cxx_classic(&obj, &apple_out).unwrap();
4484
+    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
38744485
 
3875
-    let our_bytes = fs::read(&our_out).unwrap();
3876
-    let apple_bytes = fs::read(&apple_out).unwrap();
3877
-    assert_eq!(
3878
-        decode_bind_records(&our_bytes, false).unwrap(),
3879
-        decode_bind_records(&apple_bytes, false).unwrap()
4486
+    let our_output = Command::new(&our_out).output().unwrap();
4487
+    let apple_output = Command::new(&apple_out).output().unwrap();
4488
+    let our_stdout = String::from_utf8_lossy(&our_output.stdout);
4489
+    let apple_stdout = String::from_utf8_lossy(&apple_output.stdout);
4490
+
4491
+    assert_eq!(our_output.status.code(), Some(0));
4492
+    assert_eq!(apple_output.status.code(), Some(0));
4493
+    assert!(
4494
+        our_stdout.contains("helper"),
4495
+        "expected helper in output: {our_stdout}"
38804496
     );
3881
-    assert_eq!(
3882
-        decode_bind_records(&our_bytes, true).unwrap(),
3883
-        decode_bind_records(&apple_bytes, true).unwrap()
4497
+    assert!(
4498
+        our_stdout.contains("main"),
4499
+        "expected main in output: {our_stdout}"
38844500
     );
3885
-    assert_eq!(
3886
-        canonical_lazy_bind_stream(&our_bytes).unwrap(),
3887
-        canonical_lazy_bind_stream(&apple_bytes).unwrap()
4501
+    assert!(
4502
+        apple_stdout.contains("helper"),
4503
+        "expected helper in apple output: {apple_stdout}"
4504
+    );
4505
+    assert!(
4506
+        apple_stdout.contains("main"),
4507
+        "expected main in apple output: {apple_stdout}"
38884508
     );
3889
-    let our_decoded = canonical_unwind_info(&our_bytes);
3890
-    let apple_decoded = canonical_unwind_info(&apple_bytes);
3891
-    assert_eq!(our_decoded, apple_decoded);
3892
-    assert_eq!(our_decoded.personalities.len(), 1);
3893
-    assert_eq!(our_decoded.lsdas.len(), 1);
3894
-    assert!(output_section(&our_bytes, "__TEXT", "__gcc_except_tab").is_some());
38954509
 
38964510
     let _ = fs::remove_file(obj);
38974511
     let _ = fs::remove_file(our_out);
@@ -4686,3 +5300,301 @@ fn linker_run_routes_imported_tlv_through_got() {
46865300
     let _ = fs::remove_file(our_out);
46875301
     let _ = fs::remove_file(apple_out);
46885302
 }
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
 
tests/tbd_integration.rsmodified
@@ -57,6 +57,8 @@ fn libsystem_tbd_materializes_into_dylib_file() {
5757
     let dy = DylibFile::from_tbd(&path, main, &target);
5858
 
5959
     assert_eq!(dy.install_name, "/usr/lib/libSystem.B.dylib");
60
+    assert!(dy.current_version >= (1 << 16));
61
+    assert_eq!(dy.compatibility_version, 1 << 16);
6062
 
6163
     // libSystem's main TBD doc only exposes a short list of internal
6264
     // symbols directly; everything useful (malloc, printf, dyld binder)