Comparing changes

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

base: runtime-hello-parity
compare: repo-cleanup
Create pull request
Able to merge. These branches can be automatically merged.
61 commits 393 files changed 2 contributors

Commits on repo-cleanup

.docs/sprints/sprint19.mdmodified
21 lines changed — click to load
@@ -144,3 +144,21 @@ afs-ld: loading /usr/lib/libSystem.tbd
144144
 - `-why_live` produces a coherent chain on fixtures with dead-strip enabled.
145145
 - Unknown-flag errors include a did-you-mean suggestion.
146146
 - CLI surface passes a snapshot test against the `--help` output.
147
+
148
+## Remaining Flag Slices
149
+- [x] `-exported_symbols_list <file>`
150
+- [x] `-unexported_symbols_list <file>`
151
+- [x] `-exported_symbol <sym>`
152
+- [x] `-unexported_symbol <sym>`
153
+- [x] `-r` / `-bundle` explicit deferred errors
154
+- [x] `-S`
155
+- [x] `-no_uuid`
156
+- [x] `-dead_strip`
157
+- [x] `-icf=safe` / `-icf=none`
158
+- [x] `-fixup_chains` / `-no_fixup_chains`
159
+- [x] `-Wl,<comma-separated>` normalization
160
+- [x] `-map <path>`
161
+- [x] `-t` / `-trace`
162
+- [x] `-v` / `--version`
163
+- [x] `-h` / `--help`
164
+- [x] `-why_live <symbol>`
.docs/sprints/sprint27.mdmodified
21 lines changed — click to load
@@ -42,15 +42,12 @@ For each scenario, compare:
4242
 
4343
 ### 3. Tolerated-diff rules
4444
 
45
-```rust
46
-pub enum ToleratedDiff {
47
-    UuidBytes,
48
-    Timestamp,
49
-    PathHashInString(&'static str),      // e.g. temp path in stabs
50
-    StringTableSuffixDedupVariance,
51
-    CodeSignatureHashes,
52
-}
53
-```
45
+Current Sprint 27 allowlist is intentionally small and explicit:
46
+- UUID load-command bytes.
47
+- Dylib timestamp fields.
48
+- Code-signature load-command/blob bytes.
49
+- Case-specific section-byte ranges declared in `notes.md`.
50
+- String-table length drift within 5% for suffix-dedup variance.
5451
 
5552
 Each tolerance has a precise predicate — no loose "any byte in __LINKEDIT". Unknown diffs fail.
5653
 
.github/workflows/parity-matrix.ymladded
38 lines changed — click to load
@@ -0,0 +1,38 @@
1
+name: parity-matrix
2
+
3
+on:
4
+  pull_request:
5
+  push:
6
+    branches:
7
+      - trunk
8
+
9
+permissions:
10
+  contents: read
11
+
12
+jobs:
13
+  parity-matrix:
14
+    runs-on: macos-14
15
+    timeout-minutes: 30
16
+    steps:
17
+      - name: Checkout
18
+        uses: actions/checkout@v4
19
+
20
+      - name: Install Rust
21
+        uses: dtolnay/rust-toolchain@stable
22
+
23
+      - name: Run parity harness proof tests
24
+        run: cargo test --test diff_harness_tolerates_known_linkedit --test parity_harness --test parity_canary -- --nocapture
25
+
26
+      - name: Run parity matrix
27
+        env:
28
+          PARITY_MATRIX_ARTIFACT_DIR: ${{ github.workspace }}/parity-matrix-artifacts
29
+          PARITY_MATRIX_MAX_SECONDS: "120"
30
+        run: cargo test --test parity_matrix -- --nocapture
31
+
32
+      - name: Upload parity artifacts
33
+        if: always()
34
+        uses: actions/upload-artifact@v4
35
+        with:
36
+          name: parity-matrix-html
37
+          path: parity-matrix-artifacts
38
+          if-no-files-found: warn
src/args.rsmodified
773 lines changed — click to load
@@ -6,14 +6,76 @@
66
 
77
 use std::path::PathBuf;
88
 
9
-use crate::{LinkOptions, OutputKind};
9
+use crate::resolve::{levenshtein, UndefinedTreatment};
10
+use crate::{FrameworkSpec, IcfMode, LinkOptions, OutputKind, PlatformVersion, ThunkMode};
11
+
12
+const KNOWN_FLAGS: &[&str] = &[
13
+    "-o",
14
+    "-e",
15
+    "-arch",
16
+    "-l",
17
+    "-L",
18
+    "-framework",
19
+    "-weak_framework",
20
+    "-ObjC",
21
+    "-syslibroot",
22
+    "-platform_version",
23
+    "-r",
24
+    "-bundle",
25
+    "-undefined",
26
+    "-rpath",
27
+    "-install_name",
28
+    "-current_version",
29
+    "-compatibility_version",
30
+    "-exported_symbols_list",
31
+    "-unexported_symbols_list",
32
+    "-exported_symbol",
33
+    "-unexported_symbol",
34
+    "-S",
35
+    "-no_uuid",
36
+    "-no_loh",
37
+    "-thunks=none",
38
+    "-thunks=safe",
39
+    "-thunks=all",
40
+    "-dead_strip",
41
+    "-icf=safe",
42
+    "-icf=none",
43
+    "-icf=all",
44
+    "-fixup_chains",
45
+    "-no_fixup_chains",
46
+    "-map",
47
+    "-why_live",
48
+    "-t",
49
+    "-trace",
50
+    "-v",
51
+    "--version",
52
+    "-h",
53
+    "--help",
54
+    "-x",
55
+    "-dylib",
56
+    "-all_load",
57
+    "-force_load",
58
+    "--dump",
59
+    "--dump-archive",
60
+    "--dump-dylib",
61
+    "--dump-tbd",
62
+];
1063
 
1164
 #[derive(Debug)]
1265
 pub enum ArgsError {
1366
     /// A flag that takes an argument was supplied without one.
1467
     MissingValue(String),
68
+    /// A recognized flag got a value we do not accept.
69
+    InvalidValue {
70
+        flag: String,
71
+        value: String,
72
+        expected: String,
73
+    },
1574
     /// An unrecognized flag.
16
-    UnknownFlag(String),
75
+    UnknownFlag {
76
+        flag: String,
77
+        suggestion: Option<String>,
78
+    },
1779
 }
1880
 
1981
 impl std::fmt::Display for ArgsError {
@@ -22,19 +84,67 @@ impl std::fmt::Display for ArgsError {
2284
             ArgsError::MissingValue(flag) => {
2385
                 write!(f, "flag `{flag}` requires a value")
2486
             }
25
-            ArgsError::UnknownFlag(flag) => {
87
+            ArgsError::InvalidValue {
88
+                flag,
89
+                value,
90
+                expected,
91
+            } => {
2692
                 write!(
2793
                     f,
28
-                    "unknown flag `{flag}` (Sprint 19 adds the full `ld` surface)"
94
+                    "flag `{flag}` got invalid value `{value}` (expected {expected})"
2995
                 )
3096
             }
97
+            ArgsError::UnknownFlag { flag, suggestion } => {
98
+                write!(f, "unknown flag `{flag}`")?;
99
+                if let Some(suggestion) = suggestion {
100
+                    write!(f, " (did you mean `{suggestion}`?)")?;
101
+                }
102
+                write!(f, " (Sprint 19 adds the full `ld` surface)")
103
+            }
31104
         }
32105
     }
33106
 }
34107
 
108
+fn unknown_flag(flag: &str) -> ArgsError {
109
+    let suggestion = KNOWN_FLAGS
110
+        .iter()
111
+        .map(|candidate| (levenshtein(flag, candidate), *candidate))
112
+        .filter(|(distance, _)| *distance <= 3)
113
+        .min_by_key(|(distance, candidate)| (*distance, candidate.len()))
114
+        .map(|(_, candidate)| candidate.to_string());
115
+    ArgsError::UnknownFlag {
116
+        flag: flag.to_string(),
117
+        suggestion,
118
+    }
119
+}
120
+
121
+fn parse_version_component(flag: &str, value: &str) -> Result<u32, ArgsError> {
122
+    let mut parts = value.split('.');
123
+    let parse_part = |piece: Option<&str>| -> Result<u32, ArgsError> {
124
+        let raw = piece.unwrap_or("0");
125
+        raw.parse::<u32>().map_err(|_| ArgsError::InvalidValue {
126
+            flag: flag.to_string(),
127
+            value: value.to_string(),
128
+            expected: "version like <major>[.<minor>[.<patch>]]".into(),
129
+        })
130
+    };
131
+    let major = parse_part(parts.next())?;
132
+    let minor = parse_part(parts.next())?;
133
+    let patch = parse_part(parts.next())?;
134
+    if parts.next().is_some() {
135
+        return Err(ArgsError::InvalidValue {
136
+            flag: flag.to_string(),
137
+            value: value.to_string(),
138
+            expected: "version like <major>[.<minor>[.<patch>]]".into(),
139
+        });
140
+    }
141
+    Ok((major << 16) | ((minor & 0xff) << 8) | (patch & 0xff))
142
+}
143
+
35144
 pub fn parse(argv: &[String]) -> Result<LinkOptions, ArgsError> {
145
+    let normalized = normalize_wl(argv);
36146
     let mut opts = LinkOptions::default();
37
-    let mut it = argv.iter();
147
+    let mut it = normalized.iter();
38148
     while let Some(arg) = it.next() {
39149
         match arg.as_str() {
40150
             "-o" => {
@@ -57,12 +167,234 @@ pub fn parse(argv: &[String]) -> Result<LinkOptions, ArgsError> {
57167
                         .clone(),
58168
                 );
59169
             }
170
+            "-l" => {
171
+                opts.library_names.push(
172
+                    it.next()
173
+                        .ok_or_else(|| ArgsError::MissingValue("-l".into()))?
174
+                        .clone(),
175
+                );
176
+            }
177
+            s if s.starts_with("-l") && s.len() > 2 => {
178
+                opts.library_names.push(s[2..].to_string());
179
+            }
180
+            "-L" => {
181
+                opts.search_paths.push(PathBuf::from(
182
+                    it.next()
183
+                        .ok_or_else(|| ArgsError::MissingValue("-L".into()))?,
184
+                ));
185
+            }
186
+            "-framework" => {
187
+                opts.frameworks.push(FrameworkSpec {
188
+                    name: it
189
+                        .next()
190
+                        .ok_or_else(|| ArgsError::MissingValue("-framework".into()))?
191
+                        .clone(),
192
+                    weak: false,
193
+                });
194
+            }
195
+            "-weak_framework" => {
196
+                opts.frameworks.push(FrameworkSpec {
197
+                    name: it
198
+                        .next()
199
+                        .ok_or_else(|| ArgsError::MissingValue("-weak_framework".into()))?
200
+                        .clone(),
201
+                    weak: true,
202
+                });
203
+            }
204
+            "-ObjC" => {
205
+                opts.objc_force_load = true;
206
+            }
207
+            "-syslibroot" => {
208
+                opts.syslibroot =
209
+                    Some(PathBuf::from(it.next().ok_or_else(|| {
210
+                        ArgsError::MissingValue("-syslibroot".into())
211
+                    })?));
212
+            }
213
+            "-platform_version" => {
214
+                let platform = it
215
+                    .next()
216
+                    .ok_or_else(|| ArgsError::MissingValue("-platform_version".into()))?;
217
+                if platform != "macos" {
218
+                    return Err(ArgsError::InvalidValue {
219
+                        flag: "-platform_version".into(),
220
+                        value: platform.clone(),
221
+                        expected: "platform `macos`".into(),
222
+                    });
223
+                }
224
+                let minos_raw = it
225
+                    .next()
226
+                    .ok_or_else(|| ArgsError::MissingValue("-platform_version".into()))?;
227
+                let sdk_raw = it
228
+                    .next()
229
+                    .ok_or_else(|| ArgsError::MissingValue("-platform_version".into()))?;
230
+                opts.platform_version = Some(PlatformVersion {
231
+                    minos: parse_version_component("-platform_version", minos_raw)?,
232
+                    sdk: parse_version_component("-platform_version", sdk_raw)?,
233
+                });
234
+            }
235
+            "-r" => {
236
+                opts.relocatable = true;
237
+            }
238
+            "-bundle" => {
239
+                opts.bundle = true;
240
+            }
241
+            "-undefined" => {
242
+                let value = it
243
+                    .next()
244
+                    .ok_or_else(|| ArgsError::MissingValue("-undefined".into()))?;
245
+                opts.undefined_treatment = match value.as_str() {
246
+                    "error" => UndefinedTreatment::Error,
247
+                    "warning" => UndefinedTreatment::Warning,
248
+                    "suppress" => UndefinedTreatment::Suppress,
249
+                    "dynamic_lookup" => UndefinedTreatment::DynamicLookup,
250
+                    _ => {
251
+                        return Err(ArgsError::InvalidValue {
252
+                            flag: "-undefined".into(),
253
+                            value: value.clone(),
254
+                            expected: "`error`, `warning`, `suppress`, or `dynamic_lookup`".into(),
255
+                        });
256
+                    }
257
+                };
258
+            }
259
+            "-rpath" => {
260
+                opts.rpaths.push(
261
+                    it.next()
262
+                        .ok_or_else(|| ArgsError::MissingValue("-rpath".into()))?
263
+                        .clone(),
264
+                );
265
+            }
266
+            "-install_name" => {
267
+                opts.install_name = Some(
268
+                    it.next()
269
+                        .ok_or_else(|| ArgsError::MissingValue("-install_name".into()))?
270
+                        .clone(),
271
+                );
272
+            }
273
+            "-current_version" => {
274
+                let value = it
275
+                    .next()
276
+                    .ok_or_else(|| ArgsError::MissingValue("-current_version".into()))?;
277
+                opts.current_version = Some(parse_version_component("-current_version", value)?);
278
+            }
279
+            "-compatibility_version" => {
280
+                let value = it
281
+                    .next()
282
+                    .ok_or_else(|| ArgsError::MissingValue("-compatibility_version".into()))?;
283
+                opts.compatibility_version =
284
+                    Some(parse_version_component("-compatibility_version", value)?);
285
+            }
286
+            "-exported_symbols_list" => {
287
+                opts.exported_symbols_lists
288
+                    .push(PathBuf::from(it.next().ok_or_else(|| {
289
+                        ArgsError::MissingValue("-exported_symbols_list".into())
290
+                    })?));
291
+            }
292
+            "-unexported_symbols_list" => {
293
+                opts.unexported_symbols_lists.push(PathBuf::from(
294
+                    it.next().ok_or_else(|| {
295
+                        ArgsError::MissingValue("-unexported_symbols_list".into())
296
+                    })?,
297
+                ));
298
+            }
299
+            "-exported_symbol" => {
300
+                opts.exported_symbols.push(
301
+                    it.next()
302
+                        .ok_or_else(|| ArgsError::MissingValue("-exported_symbol".into()))?
303
+                        .clone(),
304
+                );
305
+            }
306
+            "-unexported_symbol" => {
307
+                opts.unexported_symbols.push(
308
+                    it.next()
309
+                        .ok_or_else(|| ArgsError::MissingValue("-unexported_symbol".into()))?
310
+                        .clone(),
311
+                );
312
+            }
313
+            "-S" => {
314
+                opts.strip_debug = true;
315
+            }
316
+            "-no_uuid" => {
317
+                opts.emit_uuid = false;
318
+            }
319
+            "-no_loh" => {
320
+                opts.no_loh = true;
321
+            }
322
+            s if s.starts_with("-thunks=") => {
323
+                opts.thunks = match s {
324
+                    "-thunks=none" => ThunkMode::None,
325
+                    "-thunks=safe" => ThunkMode::Safe,
326
+                    "-thunks=all" => ThunkMode::All,
327
+                    _ => {
328
+                        let value = s.trim_start_matches("-thunks=").to_string();
329
+                        return Err(ArgsError::InvalidValue {
330
+                            flag: "-thunks".into(),
331
+                            value,
332
+                            expected: "`none`, `safe`, or `all`".into(),
333
+                        });
334
+                    }
335
+                };
336
+            }
337
+            "-dead_strip" => {
338
+                opts.dead_strip = true;
339
+            }
340
+            s if s.starts_with("-icf=") => {
341
+                opts.icf_mode = match s {
342
+                    "-icf=none" => IcfMode::None,
343
+                    "-icf=safe" => IcfMode::Safe,
344
+                    "-icf=all" => IcfMode::All,
345
+                    _ => {
346
+                        let value = s.trim_start_matches("-icf=").to_string();
347
+                        return Err(ArgsError::InvalidValue {
348
+                            flag: "-icf".into(),
349
+                            value,
350
+                            expected: "`safe`, `none`, or `all`".into(),
351
+                        });
352
+                    }
353
+                };
354
+            }
355
+            "-fixup_chains" => {
356
+                opts.fixup_chains = true;
357
+            }
358
+            "-no_fixup_chains" => {
359
+                opts.fixup_chains = false;
360
+            }
361
+            "-map" => {
362
+                opts.map = Some(PathBuf::from(
363
+                    it.next()
364
+                        .ok_or_else(|| ArgsError::MissingValue("-map".into()))?,
365
+                ));
366
+            }
367
+            "-why_live" => {
368
+                opts.why_live.push(
369
+                    it.next()
370
+                        .ok_or_else(|| ArgsError::MissingValue("-why_live".into()))?
371
+                        .clone(),
372
+                );
373
+            }
374
+            "-t" | "-trace" => {
375
+                opts.trace_inputs = true;
376
+            }
377
+            "-v" | "--version" => {
378
+                opts.show_version = true;
379
+            }
380
+            "-h" | "--help" => {
381
+                opts.show_help = true;
382
+            }
60383
             "-x" => {
61384
                 opts.strip_locals = true;
62385
             }
63386
             "-dylib" => {
64387
                 opts.kind = OutputKind::Dylib;
65388
             }
389
+            "-all_load" => {
390
+                opts.all_load = true;
391
+            }
392
+            "-force_load" => {
393
+                opts.force_load_archives
394
+                    .push(PathBuf::from(it.next().ok_or_else(|| {
395
+                        ArgsError::MissingValue("-force_load".into())
396
+                    })?));
397
+            }
66398
             "--dump" => {
67399
                 opts.dump = Some(PathBuf::from(
68400
                     it.next()
@@ -88,7 +420,7 @@ pub fn parse(argv: &[String]) -> Result<LinkOptions, ArgsError> {
88420
                     })?));
89421
             }
90422
             s if s.starts_with('-') => {
91
-                return Err(ArgsError::UnknownFlag(s.to_string()));
423
+                return Err(unknown_flag(s));
92424
             }
93425
             _ => {
94426
                 opts.inputs.push(PathBuf::from(arg));
@@ -98,6 +430,22 @@ pub fn parse(argv: &[String]) -> Result<LinkOptions, ArgsError> {
98430
     Ok(opts)
99431
 }
100432
 
433
+fn normalize_wl(argv: &[String]) -> Vec<String> {
434
+    let mut out = Vec::with_capacity(argv.len());
435
+    for arg in argv {
436
+        if let Some(rest) = arg.strip_prefix("-Wl,") {
437
+            out.extend(
438
+                rest.split(',')
439
+                    .filter(|piece| !piece.is_empty())
440
+                    .map(ToString::to_string),
441
+            );
442
+        } else {
443
+            out.push(arg.clone());
444
+        }
445
+    }
446
+    out
447
+}
448
+
101449
 #[cfg(test)]
102450
 mod tests {
103451
     use super::*;
@@ -120,12 +468,335 @@ mod tests {
120468
         assert_eq!(opts.kind, OutputKind::Dylib);
121469
     }
122470
 
471
+    #[test]
472
+    fn deferred_output_flags_are_recorded() {
473
+        let opts = parse(&argv(&["-r", "-bundle", "foo.o"])).unwrap();
474
+        assert!(opts.relocatable);
475
+        assert!(opts.bundle);
476
+    }
477
+
123478
     #[test]
124479
     fn strip_locals_flag_is_recorded() {
125480
         let opts = parse(&argv(&["-x", "foo.o"])).unwrap();
126481
         assert!(opts.strip_locals);
127482
     }
128483
 
484
+    #[test]
485
+    fn strip_debug_and_uuid_flags_are_recorded() {
486
+        let opts = parse(&argv(&["-S", "-no_uuid", "foo.o"])).unwrap();
487
+        assert!(opts.strip_debug);
488
+        assert!(!opts.emit_uuid);
489
+    }
490
+
491
+    #[test]
492
+    fn no_loh_flag_is_recorded() {
493
+        let opts = parse(&argv(&["-no_loh", "foo.o"])).unwrap();
494
+        assert!(opts.no_loh);
495
+    }
496
+
497
+    #[test]
498
+    fn dead_strip_icf_and_fixup_chain_flags_are_recorded() {
499
+        let opts = parse(&argv(&[
500
+            "-dead_strip",
501
+            "-thunks=all",
502
+            "-icf=safe",
503
+            "-fixup_chains",
504
+            "-no_fixup_chains",
505
+            "-icf=none",
506
+            "foo.o",
507
+        ]))
508
+        .unwrap();
509
+        assert!(opts.dead_strip);
510
+        assert_eq!(opts.thunks, ThunkMode::All);
511
+        assert_eq!(opts.icf_mode, IcfMode::None);
512
+        assert!(!opts.fixup_chains);
513
+    }
514
+
515
+    #[test]
516
+    fn thunks_flag_rejects_unknown_modes() {
517
+        let err = parse(&argv(&["-thunks=clustered", "main.o"])).unwrap_err();
518
+        assert!(matches!(
519
+            err,
520
+            ArgsError::InvalidValue {
521
+                ref flag,
522
+                ref value,
523
+                ..
524
+            } if flag == "-thunks" && value == "clustered"
525
+        ));
526
+    }
527
+
528
+    #[test]
529
+    fn icf_all_flag_is_recorded() {
530
+        let opts = parse(&argv(&["-icf=all", "foo.o"])).unwrap();
531
+        assert_eq!(opts.icf_mode, IcfMode::All);
532
+    }
533
+
534
+    #[test]
535
+    fn icf_flag_rejects_unknown_modes() {
536
+        let err = parse(&argv(&["-icf=aggressive", "main.o"])).unwrap_err();
537
+        assert!(matches!(
538
+            err,
539
+            ArgsError::InvalidValue {
540
+                ref flag,
541
+                ref value,
542
+                ..
543
+            } if flag == "-icf" && value == "aggressive"
544
+        ));
545
+    }
546
+
547
+    #[test]
548
+    fn l_flag_accepts_separate_value() {
549
+        let opts = parse(&argv(&["-l", "System", "main.o"])).unwrap();
550
+        assert_eq!(opts.library_names, vec!["System".to_string()]);
551
+        assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
552
+    }
553
+
554
+    #[test]
555
+    fn l_flag_accepts_joined_value() {
556
+        let opts = parse(&argv(&["-lSystem", "main.o"])).unwrap();
557
+        assert_eq!(opts.library_names, vec!["System".to_string()]);
558
+        assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
559
+    }
560
+
561
+    #[test]
562
+    fn search_path_and_syslibroot_flags_are_recorded() {
563
+        let opts = parse(&argv(&["-L", "/tmp/lib", "-syslibroot", "/sdk", "main.o"])).unwrap();
564
+        assert_eq!(opts.search_paths, vec![PathBuf::from("/tmp/lib")]);
565
+        assert_eq!(opts.syslibroot, Some(PathBuf::from("/sdk")));
566
+    }
567
+
568
+    #[test]
569
+    fn framework_flags_are_recorded_in_order() {
570
+        let opts = parse(&argv(&[
571
+            "-framework",
572
+            "Foundation",
573
+            "-weak_framework",
574
+            "Metal",
575
+            "main.o",
576
+        ]))
577
+        .unwrap();
578
+        assert_eq!(
579
+            opts.frameworks,
580
+            vec![
581
+                FrameworkSpec {
582
+                    name: "Foundation".into(),
583
+                    weak: false,
584
+                },
585
+                FrameworkSpec {
586
+                    name: "Metal".into(),
587
+                    weak: true,
588
+                }
589
+            ]
590
+        );
591
+        assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
592
+    }
593
+
594
+    #[test]
595
+    fn objc_flag_is_recorded() {
596
+        let opts = parse(&argv(&["-ObjC", "main.o"])).unwrap();
597
+        assert!(opts.objc_force_load);
598
+    }
599
+
600
+    #[test]
601
+    fn platform_version_flag_is_recorded() {
602
+        let opts = parse(&argv(&[
603
+            "-platform_version",
604
+            "macos",
605
+            "13.2.1",
606
+            "14.5",
607
+            "main.o",
608
+        ]))
609
+        .unwrap();
610
+        let platform = opts.platform_version.expect("platform version");
611
+        assert_eq!(platform.minos, (13 << 16) | (2 << 8) | 1);
612
+        assert_eq!(platform.sdk, (14 << 16) | (5 << 8));
613
+    }
614
+
615
+    #[test]
616
+    fn platform_version_rejects_non_macos_platform() {
617
+        let err = parse(&argv(&["-platform_version", "ios", "13.0", "13.0"])).unwrap_err();
618
+        assert!(matches!(
619
+            err,
620
+            ArgsError::InvalidValue {
621
+                ref flag,
622
+                ref value,
623
+                ..
624
+            } if flag == "-platform_version" && value == "ios"
625
+        ));
626
+    }
627
+
628
+    #[test]
629
+    fn platform_version_rejects_bad_version() {
630
+        let err = parse(&argv(&["-platform_version", "macos", "13.bad", "14.0"])).unwrap_err();
631
+        assert!(matches!(
632
+            err,
633
+            ArgsError::InvalidValue {
634
+                ref flag,
635
+                ref value,
636
+                ..
637
+            } if flag == "-platform_version" && value == "13.bad"
638
+        ));
639
+    }
640
+
641
+    #[test]
642
+    fn undefined_flag_records_dynamic_lookup() {
643
+        let opts = parse(&argv(&["-undefined", "dynamic_lookup", "main.o"])).unwrap();
644
+        assert_eq!(opts.undefined_treatment, UndefinedTreatment::DynamicLookup);
645
+    }
646
+
647
+    #[test]
648
+    fn undefined_flag_records_warning_and_suppress() {
649
+        let warning = parse(&argv(&["-undefined", "warning", "main.o"])).unwrap();
650
+        assert_eq!(warning.undefined_treatment, UndefinedTreatment::Warning);
651
+
652
+        let suppress = parse(&argv(&["-undefined", "suppress", "main.o"])).unwrap();
653
+        assert_eq!(suppress.undefined_treatment, UndefinedTreatment::Suppress);
654
+    }
655
+
656
+    #[test]
657
+    fn undefined_flag_rejects_unknown_modes() {
658
+        let err = parse(&argv(&["-undefined", "bogus", "main.o"])).unwrap_err();
659
+        assert!(matches!(
660
+            err,
661
+            ArgsError::InvalidValue {
662
+                ref flag,
663
+                ref value,
664
+                ..
665
+            } if flag == "-undefined" && value == "bogus"
666
+        ));
667
+    }
668
+
669
+    #[test]
670
+    fn dylib_metadata_flags_are_recorded() {
671
+        let opts = parse(&argv(&[
672
+            "-rpath",
673
+            "@loader_path/../lib",
674
+            "-install_name",
675
+            "@rpath/libdemo.dylib",
676
+            "-current_version",
677
+            "2.3.4",
678
+            "-compatibility_version",
679
+            "1.2",
680
+            "main.o",
681
+        ]))
682
+        .unwrap();
683
+        assert_eq!(opts.rpaths, vec!["@loader_path/../lib".to_string()]);
684
+        assert_eq!(opts.install_name.as_deref(), Some("@rpath/libdemo.dylib"));
685
+        assert_eq!(opts.current_version, Some((2 << 16) | (3 << 8) | 4));
686
+        assert_eq!(opts.compatibility_version, Some((1 << 16) | (2 << 8)));
687
+    }
688
+
689
+    #[test]
690
+    fn export_visibility_flags_are_recorded() {
691
+        let opts = parse(&argv(&[
692
+            "-exported_symbols_list",
693
+            "exports.txt",
694
+            "-unexported_symbols_list",
695
+            "hidden.txt",
696
+            "-exported_symbol",
697
+            "_keep",
698
+            "-unexported_symbol",
699
+            "_drop",
700
+            "main.o",
701
+        ]))
702
+        .unwrap();
703
+        assert_eq!(
704
+            opts.exported_symbols_lists,
705
+            vec![PathBuf::from("exports.txt")]
706
+        );
707
+        assert_eq!(
708
+            opts.unexported_symbols_lists,
709
+            vec![PathBuf::from("hidden.txt")]
710
+        );
711
+        assert_eq!(opts.exported_symbols, vec!["_keep".to_string()]);
712
+        assert_eq!(opts.unexported_symbols, vec!["_drop".to_string()]);
713
+    }
714
+
715
+    #[test]
716
+    fn map_flag_is_recorded() {
717
+        let opts = parse(&argv(&["-map", "link.map", "main.o"])).unwrap();
718
+        assert_eq!(opts.map.as_deref(), Some(std::path::Path::new("link.map")));
719
+    }
720
+
721
+    #[test]
722
+    fn wl_normalizes_map_like_direct_flag() {
723
+        let opts = parse(&argv(&["-Wl,-map,link.map", "main.o"])).unwrap();
724
+        assert_eq!(opts.map.as_deref(), Some(std::path::Path::new("link.map")));
725
+        assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
726
+    }
727
+
728
+    #[test]
729
+    fn trace_flags_are_recorded() {
730
+        let opts = parse(&argv(&["-trace", "main.o"])).unwrap();
731
+        assert!(opts.trace_inputs);
732
+        let opts = parse(&argv(&["-t", "main.o"])).unwrap();
733
+        assert!(opts.trace_inputs);
734
+    }
735
+
736
+    #[test]
737
+    fn why_live_flag_accumulates_symbols() {
738
+        let opts = parse(&argv(&[
739
+            "-why_live",
740
+            "_helper",
741
+            "-why_live",
742
+            "_leaf",
743
+            "main.o",
744
+        ]))
745
+        .unwrap();
746
+        assert_eq!(
747
+            opts.why_live,
748
+            vec!["_helper".to_string(), "_leaf".to_string()]
749
+        );
750
+    }
751
+
752
+    #[test]
753
+    fn help_and_version_flags_are_recorded() {
754
+        let opts = parse(&argv(&["--help"])).unwrap();
755
+        assert!(opts.show_help);
756
+        let opts = parse(&argv(&["-h"])).unwrap();
757
+        assert!(opts.show_help);
758
+        let opts = parse(&argv(&["--version"])).unwrap();
759
+        assert!(opts.show_version);
760
+        let opts = parse(&argv(&["-v"])).unwrap();
761
+        assert!(opts.show_version);
762
+    }
763
+
764
+    #[test]
765
+    fn all_load_flag_is_recorded() {
766
+        let opts = parse(&argv(&["-all_load", "libfoo.a"])).unwrap();
767
+        assert!(opts.all_load);
768
+        assert_eq!(opts.inputs, vec![PathBuf::from("libfoo.a")]);
769
+    }
770
+
771
+    #[test]
772
+    fn force_load_flag_accumulates_archive_paths() {
773
+        let opts = parse(&argv(&[
774
+            "-force_load",
775
+            "liba.a",
776
+            "-force_load",
777
+            "libb.a",
778
+            "main.o",
779
+        ]))
780
+        .unwrap();
781
+        assert_eq!(
782
+            opts.force_load_archives,
783
+            vec![PathBuf::from("liba.a"), PathBuf::from("libb.a")]
784
+        );
785
+        assert_eq!(opts.inputs, vec![PathBuf::from("main.o")]);
786
+    }
787
+
788
+    #[test]
789
+    fn missing_force_load_value_errors() {
790
+        let err = parse(&argv(&["-force_load"])).unwrap_err();
791
+        assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "-force_load"));
792
+    }
793
+
794
+    #[test]
795
+    fn missing_l_value_errors() {
796
+        let err = parse(&argv(&["-l"])).unwrap_err();
797
+        assert!(matches!(err, ArgsError::MissingValue(ref f) if f == "-l"));
798
+    }
799
+
129800
     #[test]
130801
     fn missing_output_value_errors() {
131802
         let err = parse(&argv(&["-o"])).unwrap_err();
@@ -135,7 +806,25 @@ mod tests {
135806
     #[test]
136807
     fn unknown_flag_errors() {
137808
         let err = parse(&argv(&["-nonsense"])).unwrap_err();
138
-        assert!(matches!(err, ArgsError::UnknownFlag(ref f) if f == "-nonsense"));
809
+        assert!(matches!(
810
+            err,
811
+            ArgsError::UnknownFlag {
812
+                ref flag,
813
+                suggestion: None
814
+            } if flag == "-nonsense"
815
+        ));
816
+    }
817
+
818
+    #[test]
819
+    fn unknown_flag_suggests_nearby_match() {
820
+        let err = parse(&argv(&["-all_lod"])).unwrap_err();
821
+        assert!(matches!(
822
+            err,
823
+            ArgsError::UnknownFlag {
824
+                ref flag,
825
+                suggestion: Some(ref suggestion)
826
+            } if flag == "-all_lod" && suggestion == "-all_load"
827
+        ));
139828
     }
140829
 
141830
     #[test]
src/atom.rsmodified
177 lines changed — click to load
@@ -297,9 +297,10 @@ pub fn atomize_object(
297297
         );
298298
     }
299299
 
300
-    // Post-pass: wire `parent_of` for every `__compact_unwind` atom to the
301
-    // function atom that its `function_start` reloc references.
300
+    // Post-pass: wire metadata atoms to the function atoms whose lifetime
301
+    // they track, so dead-strip can prune unwind surfaces precisely.
302302
     link_unwind_parents(input_id, obj, table, &out);
303
+    link_eh_frame_parents(input_id, obj, table, &out);
303304
 
304305
     out
305306
 }
@@ -486,6 +487,11 @@ fn atomize_regular_section(
486487
         return;
487488
     }
488489
 
490
+    if atom_section == AtomSection::EhFrame {
491
+        atomize_eh_frame(input_id, section_idx, sect, atom_section, table, out);
492
+        return;
493
+    }
494
+
489495
     // With subsections_via_symbols and at least one split point, walk the
490496
     // sorted symbols and emit one atom per non-alt_entry boundary.
491497
     if syms.is_empty() {
@@ -768,6 +774,154 @@ fn atomize_compact_unwind(
768774
     }
769775
 }
770776
 
777
+/// Split `__eh_frame` into DWARF CFI records so dead-strip can retain only
778
+/// the live FDEs and their shared CIEs.
779
+fn atomize_eh_frame(
780
+    input_id: InputId,
781
+    section_idx: u8,
782
+    sect: &InputSection,
783
+    atom_section: AtomSection,
784
+    table: &mut AtomTable,
785
+    out: &mut ObjectAtomization,
786
+) {
787
+    let mut offset = 0usize;
788
+    while offset < sect.data.len() {
789
+        let Some(size) = eh_frame_record_size(&sect.data, offset) else {
790
+            let atom = build_section_atom(input_id, section_idx, sect, atom_section);
791
+            let id = table.push(atom);
792
+            out.atoms.push(id);
793
+            return;
794
+        };
795
+
796
+        let end = (offset + size).min(sect.data.len());
797
+        let atom = Atom {
798
+            id: AtomId(0),
799
+            origin: input_id,
800
+            input_section: section_idx,
801
+            section: atom_section,
802
+            input_offset: offset as u32,
803
+            size: (end - offset) as u32,
804
+            align_pow2: (sect.align_pow2 as u8).min(2),
805
+            owner: None,
806
+            alt_entries: Vec::new(),
807
+            data: sect.data[offset..end].to_vec(),
808
+            flags: AtomFlags::default(),
809
+            parent_of: None,
810
+        };
811
+        let id = table.push(atom);
812
+        out.atoms.push(id);
813
+        offset = end;
814
+    }
815
+}
816
+
817
+fn eh_frame_record_size(data: &[u8], offset: usize) -> Option<usize> {
818
+    let length_end = offset.checked_add(4)?;
819
+    let length_bytes: [u8; 4] = data.get(offset..length_end)?.try_into().ok()?;
820
+    let length = u32::from_le_bytes(length_bytes);
821
+    if length == 0 {
822
+        return Some(4);
823
+    }
824
+    if length == u32::MAX {
825
+        return None;
826
+    }
827
+    let size = 4usize.checked_add(length as usize)?;
828
+    (offset + size <= data.len()).then_some(size)
829
+}
830
+
831
+fn eh_frame_cie_pointer(atom: &Atom) -> Option<u32> {
832
+    (atom.section == AtomSection::EhFrame && atom.data.len() >= 8).then(|| {
833
+        let mut buf = [0u8; 4];
834
+        buf.copy_from_slice(&atom.data[4..8]);
835
+        u32::from_le_bytes(buf)
836
+    })
837
+}
838
+
839
+fn resolve_function_parent(
840
+    obj: &ObjectFile,
841
+    atom: &Atom,
842
+    reloc: crate::reloc::Reloc,
843
+    atom_index: &HashMap<(u8, u32), AtomId>,
844
+    field_offset: usize,
845
+) -> Option<AtomId> {
846
+    match reloc.referent {
847
+        Referent::Section(sect_idx) => {
848
+            let end = field_offset.checked_add(8)?;
849
+            let mut buf = [0u8; 8];
850
+            buf.copy_from_slice(atom.data.get(field_offset..end)?);
851
+            let target_offset = u64::from_le_bytes(buf) as u32;
852
+            atom_index.get(&(sect_idx, target_offset)).copied()
853
+        }
854
+        Referent::Symbol(sym_idx) => {
855
+            let input_sym = obj.symbols.get(sym_idx as usize)?;
856
+            (input_sym.kind() == SymKind::Sect)
857
+                .then(|| {
858
+                    let target_offset = input_sym.value().saturating_sub(
859
+                        obj.sections
860
+                            .get(input_sym.sect_idx().saturating_sub(1) as usize)
861
+                            .map(|section| section.addr)
862
+                            .unwrap_or(0),
863
+                    ) as u32;
864
+                    atom_index
865
+                        .get(&(input_sym.sect_idx(), target_offset))
866
+                        .copied()
867
+                })
868
+                .flatten()
869
+        }
870
+    }
871
+}
872
+
873
+fn link_eh_frame_parents(
874
+    input_id: InputId,
875
+    obj: &ObjectFile,
876
+    table: &mut AtomTable,
877
+    out: &ObjectAtomization,
878
+) {
879
+    let Some((eh_idx_zero, eh_sect)) = obj
880
+        .sections
881
+        .iter()
882
+        .enumerate()
883
+        .find(|(_, s)| s.kind == SectionKind::EhFrame)
884
+    else {
885
+        return;
886
+    };
887
+    let eh_idx_one = (eh_idx_zero + 1) as u8;
888
+
889
+    let raws = match parse_raw_relocs(&eh_sect.raw_relocs, 0, eh_sect.nreloc) {
890
+        Ok(r) => r,
891
+        Err(_) => return,
892
+    };
893
+    let fused = match parse_relocs(&raws) {
894
+        Ok(f) => f,
895
+        Err(_) => return,
896
+    };
897
+
898
+    let mut atom_index: HashMap<(u8, u32), AtomId> = HashMap::new();
899
+    for id in &out.atoms {
900
+        let a = table.get(*id);
901
+        atom_index.insert((a.input_section, a.input_offset), *id);
902
+    }
903
+
904
+    for id in &out.atoms {
905
+        let atom = table.get(*id);
906
+        if atom.input_section != eh_idx_one {
907
+            continue;
908
+        }
909
+        let Some(cie_pointer) = eh_frame_cie_pointer(atom) else {
910
+            continue;
911
+        };
912
+        if cie_pointer == 0 {
913
+            continue;
914
+        }
915
+        let Some(reloc) = fused.iter().find(|r| r.offset == atom.input_offset + 8) else {
916
+            continue;
917
+        };
918
+        if let Some(parent_id) = resolve_function_parent(obj, atom, *reloc, &atom_index, 8) {
919
+            table.get_mut(*id).parent_of = Some(parent_id);
920
+        }
921
+    }
922
+    let _ = input_id;
923
+}
924
+
771925
 fn atomize_zerofill(
772926
     input_id: InputId,
773927
     section_idx: u8,
src/diag.rsmodified
15 lines changed — click to load
@@ -16,3 +16,15 @@ pub fn error_verbatim(msg: &str) {
1616
     let mut h = stderr.lock();
1717
     let _ = writeln!(h, "{msg}");
1818
 }
19
+
20
+pub fn warning(msg: &str) {
21
+    let stderr = std::io::stderr();
22
+    let mut h = stderr.lock();
23
+    let _ = writeln!(h, "afs-ld: warning: {msg}");
24
+}
25
+
26
+pub fn warning_verbatim(msg: &str) {
27
+    let stderr = std::io::stderr();
28
+    let mut h = stderr.lock();
29
+    let _ = writeln!(h, "{msg}");
30
+}
src/icf.rsadded
506 lines changed — click to load
@@ -0,0 +1,506 @@
1
+use std::collections::{HashMap, HashSet};
2
+use std::fmt;
3
+
4
+use crate::atom::{Atom, AtomFlags, AtomSection, AtomTable};
5
+use crate::layout::LayoutInput;
6
+use crate::reloc::{parse_raw_relocs, parse_relocs, Referent, Reloc, RelocKind, RelocLength};
7
+use crate::resolve::{AtomId, InputId, Symbol, SymbolId, SymbolTable};
8
+
9
+#[derive(Debug, Clone, Default)]
10
+pub struct IcfPlan {
11
+    kept_atoms: HashSet<AtomId>,
12
+    redirects: HashMap<AtomId, AtomId>,
13
+}
14
+
15
+#[derive(Debug, Clone, PartialEq, Eq)]
16
+pub struct FoldedSymbol {
17
+    pub name: String,
18
+    pub winner: String,
19
+    pub file_index: usize,
20
+}
21
+
22
+impl IcfPlan {
23
+    pub fn kept_atoms(&self) -> &HashSet<AtomId> {
24
+        &self.kept_atoms
25
+    }
26
+
27
+    pub fn redirects(&self) -> &HashMap<AtomId, AtomId> {
28
+        &self.redirects
29
+    }
30
+
31
+    pub fn folded_symbols(
32
+        &self,
33
+        atom_table: &AtomTable,
34
+        sym_table: &SymbolTable,
35
+        layout_inputs: &[LayoutInput<'_>],
36
+    ) -> Vec<FoldedSymbol> {
37
+        let file_index_by_input: HashMap<InputId, usize> = layout_inputs
38
+            .iter()
39
+            .enumerate()
40
+            .map(|(idx, input)| (input.id, idx + 1))
41
+            .collect();
42
+        let mut out = Vec::new();
43
+        for (&loser, &winner) in &self.redirects {
44
+            let winner = canonical_atom(winner, &self.redirects);
45
+            let Some(winner_symbol) = representative_symbol(atom_table.get(winner)) else {
46
+                continue;
47
+            };
48
+            let winner_name = symbol_name(sym_table, winner_symbol);
49
+            for symbol_id in atom_symbols(atom_table.get(loser)) {
50
+                let Symbol::Defined { origin, .. } = sym_table.get(symbol_id) else {
51
+                    continue;
52
+                };
53
+                let name = symbol_name(sym_table, symbol_id);
54
+                if name == winner_name {
55
+                    continue;
56
+                }
57
+                out.push(FoldedSymbol {
58
+                    name,
59
+                    winner: winner_name.clone(),
60
+                    file_index: file_index_by_input.get(origin).copied().unwrap_or(0),
61
+                });
62
+            }
63
+        }
64
+        out.sort_by(|lhs, rhs| {
65
+            lhs.name
66
+                .cmp(&rhs.name)
67
+                .then_with(|| lhs.winner.cmp(&rhs.winner))
68
+                .then_with(|| lhs.file_index.cmp(&rhs.file_index))
69
+        });
70
+        out.dedup_by(|lhs, rhs| {
71
+            lhs.name == rhs.name && lhs.winner == rhs.winner && lhs.file_index == rhs.file_index
72
+        });
73
+        out
74
+    }
75
+}
76
+
77
+#[derive(Debug, Clone)]
78
+pub struct IcfError(String);
79
+
80
+impl fmt::Display for IcfError {
81
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82
+        write!(f, "ICF error: {}", self.0)
83
+    }
84
+}
85
+
86
+impl std::error::Error for IcfError {}
87
+
88
+pub fn fold_safe(
89
+    layout_inputs: &[LayoutInput<'_>],
90
+    atom_table: &mut AtomTable,
91
+    sym_table: &mut SymbolTable,
92
+    live_atoms: Option<&HashSet<AtomId>>,
93
+) -> Result<IcfPlan, IcfError> {
94
+    let resolved_by_name = resolved_symbol_map(sym_table);
95
+    let reloc_cache = reloc_cache(layout_inputs)?;
96
+
97
+    mark_address_taken(
98
+        layout_inputs,
99
+        atom_table,
100
+        sym_table,
101
+        &resolved_by_name,
102
+        &reloc_cache,
103
+    );
104
+
105
+    let mut kept_atoms = live_atoms.cloned().unwrap_or_else(|| {
106
+        atom_table
107
+            .iter()
108
+            .map(|(atom_id, _)| atom_id)
109
+            .collect::<HashSet<_>>()
110
+    });
111
+    let mut redirects = HashMap::new();
112
+
113
+    let order_by_input: HashMap<InputId, (usize, Option<u32>)> = layout_inputs
114
+        .iter()
115
+        .map(|input| (input.id, (input.load_order, input.archive_member_offset)))
116
+        .collect();
117
+
118
+    loop {
119
+        let mut buckets: HashMap<FoldKey, Vec<AtomId>> = HashMap::new();
120
+        for (atom_id, atom) in atom_table.iter() {
121
+            if !kept_atoms.contains(&atom_id) {
122
+                continue;
123
+            }
124
+            if !is_foldable_atom(atom, sym_table) {
125
+                continue;
126
+            }
127
+            let relocs = reloc_cache
128
+                .get(&(atom.origin, atom.input_section))
129
+                .map(Vec::as_slice)
130
+                .unwrap_or(&[]);
131
+            let Some(reloc_sig) = reloc_signature_for_atom(
132
+                atom,
133
+                relocs,
134
+                layout_inputs,
135
+                sym_table,
136
+                &resolved_by_name,
137
+                &redirects,
138
+            ) else {
139
+                continue;
140
+            };
141
+            buckets
142
+                .entry(FoldKey::from_atom(atom, reloc_sig))
143
+                .or_default()
144
+                .push(atom_id);
145
+        }
146
+
147
+        let mut changed = false;
148
+        for atom_ids in buckets.into_values() {
149
+            if atom_ids.len() < 2 {
150
+                continue;
151
+            }
152
+            let winner = *atom_ids
153
+                .iter()
154
+                .min_by_key(|atom_id| {
155
+                    fold_order_key(atom_table.get(**atom_id), &order_by_input, **atom_id)
156
+                })
157
+                .expect("bucket is non-empty");
158
+            for loser in atom_ids {
159
+                if loser == winner {
160
+                    continue;
161
+                }
162
+                redirects.insert(loser, winner);
163
+                kept_atoms.remove(&loser);
164
+                rebind_folded_symbols(sym_table, atom_table.get(loser), winner);
165
+                changed = true;
166
+            }
167
+        }
168
+
169
+        if !changed {
170
+            break;
171
+        }
172
+    }
173
+
174
+    rebind_symbols_to_canonical_winners(sym_table, &redirects);
175
+
176
+    Ok(IcfPlan {
177
+        kept_atoms,
178
+        redirects,
179
+    })
180
+}
181
+
182
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
183
+struct FoldKey {
184
+    section: AtomSection,
185
+    size: u32,
186
+    align_pow2: u8,
187
+    flags: u32,
188
+    data: Vec<u8>,
189
+    relocs: Vec<FoldReloc>,
190
+}
191
+
192
+impl FoldKey {
193
+    fn from_atom(atom: &Atom, relocs: Vec<FoldReloc>) -> Self {
194
+        Self {
195
+            section: atom.section,
196
+            size: atom.size,
197
+            align_pow2: atom.align_pow2,
198
+            flags: atom.flags.bits() & !AtomFlags::ADDRESS_TAKEN,
199
+            data: atom.data.clone(),
200
+            relocs,
201
+        }
202
+    }
203
+}
204
+
205
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
206
+struct FoldReloc {
207
+    offset: u32,
208
+    kind: RelocKind,
209
+    length: RelocLength,
210
+    pcrel: bool,
211
+    referent: FoldReferent,
212
+    addend: i64,
213
+    subtrahend: Option<FoldReferent>,
214
+}
215
+
216
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
217
+enum FoldReferent {
218
+    Atom(AtomId),
219
+    Symbol(SymbolId),
220
+    Section(u8),
221
+}
222
+
223
+fn fold_order_key(
224
+    atom: &Atom,
225
+    order_by_input: &HashMap<InputId, (usize, Option<u32>)>,
226
+    atom_id: AtomId,
227
+) -> (usize, u32, u32, u32) {
228
+    let (load_order, archive_member_offset) = order_by_input
229
+        .get(&atom.origin)
230
+        .copied()
231
+        .unwrap_or((usize::MAX, None));
232
+    (
233
+        load_order,
234
+        archive_member_offset.unwrap_or(0),
235
+        atom.input_offset,
236
+        atom_id.0,
237
+    )
238
+}
239
+
240
+fn is_foldable_atom(atom: &Atom, sym_table: &SymbolTable) -> bool {
241
+    if !matches!(
242
+        atom.section,
243
+        AtomSection::Text
244
+            | AtomSection::ConstData
245
+            | AtomSection::CStringLiterals
246
+            | AtomSection::Literal16
247
+    ) {
248
+        return false;
249
+    }
250
+    if atom.flags.has(AtomFlags::NO_DEAD_STRIP) || atom.flags.has(AtomFlags::ADDRESS_TAKEN) {
251
+        return false;
252
+    }
253
+    !matches!(
254
+        atom.owner.map(|owner| sym_table.get(owner)),
255
+        Some(Symbol::Defined {
256
+            private_extern: false,
257
+            ..
258
+        })
259
+    )
260
+}
261
+
262
+fn rebind_folded_symbols(sym_table: &mut SymbolTable, atom: &Atom, winner: AtomId) {
263
+    if let Some(owner) = atom.owner {
264
+        let value = match sym_table.get(owner) {
265
+            Symbol::Defined { value, .. } => *value,
266
+            _ => 0,
267
+        };
268
+        sym_table.bind_atom(owner, winner, value);
269
+    }
270
+    for alt in &atom.alt_entries {
271
+        let value = match sym_table.get(alt.symbol) {
272
+            Symbol::Defined { value, .. } => *value,
273
+            _ => alt.offset_within_atom as u64,
274
+        };
275
+        sym_table.bind_atom(alt.symbol, winner, value);
276
+    }
277
+}
278
+
279
+fn rebind_symbols_to_canonical_winners(
280
+    sym_table: &mut SymbolTable,
281
+    redirects: &HashMap<AtomId, AtomId>,
282
+) {
283
+    let updates: Vec<(SymbolId, AtomId, u64)> = sym_table
284
+        .iter()
285
+        .filter_map(|(symbol_id, symbol)| match symbol {
286
+            Symbol::Defined { atom, value, .. } if atom.0 != 0 => {
287
+                let canonical = canonical_atom(*atom, redirects);
288
+                (canonical != *atom).then_some((symbol_id, canonical, *value))
289
+            }
290
+            _ => None,
291
+        })
292
+        .collect();
293
+    for (symbol_id, atom, value) in updates {
294
+        sym_table.bind_atom(symbol_id, atom, value);
295
+    }
296
+}
297
+
298
+fn resolved_symbol_map(sym_table: &SymbolTable) -> HashMap<String, SymbolId> {
299
+    let mut out = HashMap::new();
300
+    for (symbol_id, symbol) in sym_table.iter() {
301
+        out.insert(
302
+            sym_table.interner.resolve(symbol.name()).to_string(),
303
+            symbol_id,
304
+        );
305
+    }
306
+    out
307
+}
308
+
309
+fn reloc_cache(
310
+    layout_inputs: &[LayoutInput<'_>],
311
+) -> Result<HashMap<(InputId, u8), Vec<Reloc>>, IcfError> {
312
+    let mut out = HashMap::new();
313
+    for input in layout_inputs {
314
+        for (section_idx_zero, section) in input.object.sections.iter().enumerate() {
315
+            if section.raw_relocs.is_empty() {
316
+                continue;
317
+            }
318
+            let raws = parse_raw_relocs(&section.raw_relocs, 0, section.nreloc)
319
+                .map_err(|err| IcfError(format!("{}: {err}", input.object.path.display())))?;
320
+            let relocs = parse_relocs(&raws)
321
+                .map_err(|err| IcfError(format!("{}: {err}", input.object.path.display())))?;
322
+            out.insert((input.id, (section_idx_zero + 1) as u8), relocs);
323
+        }
324
+    }
325
+    Ok(out)
326
+}
327
+
328
+#[allow(clippy::too_many_arguments)]
329
+fn mark_address_taken(
330
+    layout_inputs: &[LayoutInput<'_>],
331
+    atom_table: &mut AtomTable,
332
+    sym_table: &SymbolTable,
333
+    resolved_by_name: &HashMap<String, SymbolId>,
334
+    reloc_cache: &HashMap<(InputId, u8), Vec<Reloc>>,
335
+) {
336
+    for input in layout_inputs {
337
+        for (section_idx_zero, _section) in input.object.sections.iter().enumerate() {
338
+            let input_section = (section_idx_zero + 1) as u8;
339
+            let Some(relocs) = reloc_cache.get(&(input.id, input_section)) else {
340
+                continue;
341
+            };
342
+            for reloc in relocs {
343
+                if !marks_address_taken(reloc.kind) {
344
+                    continue;
345
+                }
346
+                for target_atom in target_atoms_for_reloc(
347
+                    input.object,
348
+                    reloc.referent,
349
+                    sym_table,
350
+                    resolved_by_name,
351
+                ) {
352
+                    atom_table
353
+                        .get_mut(target_atom)
354
+                        .flags
355
+                        .set(AtomFlags::ADDRESS_TAKEN);
356
+                }
357
+            }
358
+        }
359
+    }
360
+}
361
+
362
+fn marks_address_taken(kind: RelocKind) -> bool {
363
+    matches!(
364
+        kind,
365
+        RelocKind::Unsigned
366
+            | RelocKind::Page21
367
+            | RelocKind::PageOff12
368
+            | RelocKind::PointerToGot
369
+            | RelocKind::GotLoadPage21
370
+            | RelocKind::GotLoadPageOff12
371
+            | RelocKind::TlvpLoadPage21
372
+            | RelocKind::TlvpLoadPageOff12
373
+    )
374
+}
375
+
376
+fn relocs_for_atom<'a>(relocs: &'a [Reloc], atom: &Atom) -> impl Iterator<Item = Reloc> + 'a {
377
+    let start = atom.input_offset;
378
+    let end = atom.input_offset.saturating_add(atom.size);
379
+    relocs.iter().copied().filter(move |reloc| {
380
+        let reloc_end = reloc
381
+            .offset
382
+            .saturating_add(reloc.length.byte_width() as u32);
383
+        reloc.offset >= start && reloc_end <= end
384
+    })
385
+}
386
+
387
+fn reloc_signature_for_atom(
388
+    atom: &Atom,
389
+    relocs: &[Reloc],
390
+    layout_inputs: &[LayoutInput<'_>],
391
+    sym_table: &SymbolTable,
392
+    resolved_by_name: &HashMap<String, SymbolId>,
393
+    redirects: &HashMap<AtomId, AtomId>,
394
+) -> Option<Vec<FoldReloc>> {
395
+    let objects_by_input: HashMap<InputId, &crate::input::ObjectFile> = layout_inputs
396
+        .iter()
397
+        .map(|input| (input.id, input.object))
398
+        .collect();
399
+    let object = objects_by_input.get(&atom.origin)?;
400
+    relocs_for_atom(relocs, atom)
401
+        .map(|reloc| {
402
+            Some(FoldReloc {
403
+                offset: reloc.offset.saturating_sub(atom.input_offset),
404
+                kind: reloc.kind,
405
+                length: reloc.length,
406
+                pcrel: reloc.pcrel,
407
+                referent: normalize_referent(
408
+                    object,
409
+                    reloc.referent,
410
+                    sym_table,
411
+                    resolved_by_name,
412
+                    redirects,
413
+                )?,
414
+                addend: reloc.addend,
415
+                subtrahend: match reloc.subtrahend {
416
+                    Some(referent) => Some(normalize_referent(
417
+                        object,
418
+                        referent,
419
+                        sym_table,
420
+                        resolved_by_name,
421
+                        redirects,
422
+                    )?),
423
+                    None => None,
424
+                },
425
+            })
426
+        })
427
+        .collect()
428
+}
429
+
430
+fn normalize_referent(
431
+    object: &crate::input::ObjectFile,
432
+    referent: Referent,
433
+    sym_table: &SymbolTable,
434
+    resolved_by_name: &HashMap<String, SymbolId>,
435
+    redirects: &HashMap<AtomId, AtomId>,
436
+) -> Option<FoldReferent> {
437
+    match referent {
438
+        Referent::Symbol(sym_idx) => {
439
+            let input_sym = object.symbols.get(sym_idx as usize)?;
440
+            let name = object.symbol_name(input_sym).ok()?;
441
+            let &symbol_id = resolved_by_name.get(name)?;
442
+            match sym_table.get(symbol_id) {
443
+                Symbol::Defined { atom, .. } if atom.0 != 0 => {
444
+                    Some(FoldReferent::Atom(canonical_atom(*atom, redirects)))
445
+                }
446
+                _ => Some(FoldReferent::Symbol(symbol_id)),
447
+            }
448
+        }
449
+        Referent::Section(section) => Some(FoldReferent::Section(section)),
450
+    }
451
+}
452
+
453
+fn canonical_atom(atom_id: AtomId, redirects: &HashMap<AtomId, AtomId>) -> AtomId {
454
+    let mut current = atom_id;
455
+    while let Some(&next) = redirects.get(&current) {
456
+        if next == current {
457
+            break;
458
+        }
459
+        current = next;
460
+    }
461
+    current
462
+}
463
+
464
+fn representative_symbol(atom: &Atom) -> Option<SymbolId> {
465
+    atom.owner
466
+        .or_else(|| atom.alt_entries.first().map(|alt| alt.symbol))
467
+}
468
+
469
+fn atom_symbols(atom: &Atom) -> impl Iterator<Item = SymbolId> + '_ {
470
+    atom.owner
471
+        .into_iter()
472
+        .chain(atom.alt_entries.iter().map(|alt| alt.symbol))
473
+}
474
+
475
+fn symbol_name(sym_table: &SymbolTable, symbol_id: SymbolId) -> String {
476
+    sym_table
477
+        .interner
478
+        .resolve(sym_table.get(symbol_id).name())
479
+        .to_string()
480
+}
481
+
482
+fn target_atoms_for_reloc(
483
+    object: &crate::input::ObjectFile,
484
+    referent: Referent,
485
+    sym_table: &SymbolTable,
486
+    resolved_by_name: &HashMap<String, SymbolId>,
487
+) -> Vec<AtomId> {
488
+    match referent {
489
+        Referent::Symbol(sym_idx) => {
490
+            let Some(input_sym) = object.symbols.get(sym_idx as usize) else {
491
+                return Vec::new();
492
+            };
493
+            let Some(name) = object.symbol_name(input_sym).ok() else {
494
+                return Vec::new();
495
+            };
496
+            let Some(&symbol_id) = resolved_by_name.get(name) else {
497
+                return Vec::new();
498
+            };
499
+            match sym_table.get(symbol_id) {
500
+                Symbol::Defined { atom, .. } if atom.0 != 0 => vec![*atom],
501
+                _ => Vec::new(),
502
+            }
503
+        }
504
+        Referent::Section(_) => Vec::new(),
505
+    }
506
+}
src/input.rsmodified
192 lines changed — click to load
@@ -7,6 +7,7 @@
77
 
88
 use std::path::PathBuf;
99
 
10
+use crate::loh::{parse_loh_blob, LohEntry};
1011
 use crate::macho::constants::LC_DATA_IN_CODE;
1112
 use crate::macho::reader::{
1213
     parse_commands, parse_header, DysymtabCmd, LinkEditDataCmd, LoadCommand, MachHeader64,
@@ -28,6 +29,7 @@ pub struct ObjectFile {
2829
     pub strings: StringTable,
2930
     pub symtab: Option<SymtabCmd>,
3031
     pub dysymtab: Option<DysymtabCmd>,
32
+    pub loh: Vec<LohEntry>,
3133
     pub data_in_code: Vec<DataInCodeEntry>,
3234
 }
3335
 
@@ -90,6 +92,7 @@ impl ObjectFile {
9092
             ),
9193
             None => (Vec::new(), StringTable::from_bytes(Vec::new())),
9294
         };
95
+        let loh = parse_loh(&commands, file_bytes)?;
9396
         let data_in_code = parse_data_in_code(&commands, file_bytes)?;
9497
 
9598
         Ok(ObjectFile {
@@ -101,6 +104,7 @@ impl ObjectFile {
101104
             strings,
102105
             symtab,
103106
             dysymtab,
107
+            loh,
104108
             data_in_code,
105109
         })
106110
     }
@@ -132,6 +136,32 @@ impl ObjectFile {
132136
     }
133137
 }
134138
 
139
+fn parse_loh(commands: &[LoadCommand], file_bytes: &[u8]) -> Result<Vec<LohEntry>, ReadError> {
140
+    let mut out = Vec::new();
141
+    for command in commands {
142
+        let LoadCommand::LinkerOptimizationHint(linkedit) = command else {
143
+            continue;
144
+        };
145
+        let start = linkedit.dataoff as usize;
146
+        let end = start
147
+            .checked_add(linkedit.datasize as usize)
148
+            .ok_or(ReadError::Truncated {
149
+                need: usize::MAX,
150
+                have: file_bytes.len(),
151
+                context: "LC_LINKER_OPTIMIZATION_HINT payload (offset + size overflows)",
152
+            })?;
153
+        if end > file_bytes.len() {
154
+            return Err(ReadError::Truncated {
155
+                need: end,
156
+                have: file_bytes.len(),
157
+                context: "LC_LINKER_OPTIMIZATION_HINT payload",
158
+            });
159
+        }
160
+        out.extend(parse_loh_blob(&file_bytes[start..end])?);
161
+    }
162
+    Ok(out)
163
+}
164
+
135165
 fn parse_data_in_code(
136166
     commands: &[LoadCommand],
137167
     file_bytes: &[u8],
@@ -182,6 +212,7 @@ pub fn header_and_cmds_end(header: &MachHeader64) -> usize {
182212
 #[cfg(test)]
183213
 mod tests {
184214
     use super::*;
215
+    use crate::loh::{write_loh_blob, LOH_ARM64_ADRP_ADD};
185216
     use crate::macho::constants::*;
186217
     use crate::macho::reader::{
187218
         write_commands, write_header, LinkEditDataCmd, LoadCommand, Section64Header, Segment64,
@@ -389,6 +420,99 @@ mod tests {
389420
         image
390421
     }
391422
 
423
+    fn synth_image_with_loh() -> Vec<u8> {
424
+        let text_sect = Section64Header {
425
+            sectname: name16("__text"),
426
+            segname: name16("__TEXT"),
427
+            addr: 0,
428
+            size: 8,
429
+            offset: 0,
430
+            align: 2,
431
+            reloff: 0,
432
+            nreloc: 0,
433
+            flags: S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS,
434
+            reserved1: 0,
435
+            reserved2: 0,
436
+            reserved3: 0,
437
+        };
438
+        let seg = Segment64 {
439
+            segname: name16(""),
440
+            vmaddr: 0,
441
+            vmsize: 8,
442
+            fileoff: 0,
443
+            filesize: 8,
444
+            maxprot: 7,
445
+            initprot: 7,
446
+            flags: 0,
447
+            sections: vec![text_sect],
448
+        };
449
+        let strtab = b"\0_main\0";
450
+        let nsyms = 1u32;
451
+        let sym = RawNlist {
452
+            strx: 1,
453
+            n_type: N_SECT | N_EXT,
454
+            n_sect: 1,
455
+            n_desc: 0,
456
+            n_value: 0,
457
+        };
458
+        let loh_blob = write_loh_blob(&[LohEntry {
459
+            kind: LOH_ARM64_ADRP_ADD,
460
+            args: vec![0, 4],
461
+        }]);
462
+        let hdr_size = HEADER_SIZE;
463
+        let seg_size = seg.wire_size() as usize;
464
+        let loh_size = LinkEditDataCmd::WIRE_SIZE as usize;
465
+        let symtab_size = SymtabCmd::WIRE_SIZE as usize;
466
+        let sizeofcmds = (seg_size + loh_size + symtab_size) as u32;
467
+
468
+        let section_offset = (hdr_size + sizeofcmds as usize) as u32;
469
+        let loh_off = section_offset + 8;
470
+        let symoff = loh_off + loh_blob.len() as u32;
471
+        let stroff = symoff + NLIST_SIZE as u32 * nsyms;
472
+        let seg = Segment64 {
473
+            sections: vec![Section64Header {
474
+                offset: section_offset,
475
+                ..seg.sections[0]
476
+            }],
477
+            fileoff: section_offset as u64,
478
+            ..seg
479
+        };
480
+        let header = MachHeader64 {
481
+            magic: MH_MAGIC_64,
482
+            cputype: CPU_TYPE_ARM64,
483
+            cpusubtype: 0,
484
+            filetype: MH_OBJECT,
485
+            ncmds: 3,
486
+            sizeofcmds,
487
+            flags: MH_SUBSECTIONS_VIA_SYMBOLS,
488
+            reserved: 0,
489
+        };
490
+        let symtab_cmd = SymtabCmd {
491
+            symoff,
492
+            nsyms,
493
+            stroff,
494
+            strsize: strtab.len() as u32,
495
+        };
496
+        let loh_cmd = LoadCommand::LinkerOptimizationHint(LinkEditDataCmd {
497
+            dataoff: loh_off,
498
+            datasize: loh_blob.len() as u32,
499
+        });
500
+
501
+        let mut image = Vec::new();
502
+        write_header(&header, &mut image);
503
+        let cmds = vec![
504
+            LoadCommand::Segment64(seg),
505
+            loh_cmd,
506
+            LoadCommand::Symtab(symtab_cmd),
507
+        ];
508
+        write_commands(&cmds, &mut image);
509
+        image.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22]);
510
+        image.extend_from_slice(&loh_blob);
511
+        sym.write(&mut image);
512
+        image.extend_from_slice(strtab);
513
+        image
514
+    }
515
+
392516
     #[test]
393517
     fn parse_synth_object_end_to_end() {
394518
         let image = synth_image();
@@ -425,6 +549,19 @@ mod tests {
425549
         );
426550
     }
427551
 
552
+    #[test]
553
+    fn parse_preserves_loh_entries() {
554
+        let image = synth_image_with_loh();
555
+        let obj = ObjectFile::parse("/tmp/synth-loh.o", &image).unwrap();
556
+        assert_eq!(
557
+            obj.loh,
558
+            vec![LohEntry {
559
+                kind: LOH_ARM64_ADRP_ADD,
560
+                args: vec![0, 4],
561
+            }]
562
+        );
563
+    }
564
+
428565
     #[test]
429566
     fn indirect_target_name_resolves() {
430567
         // Build a minimal strtab with "\0_alias\0_target\0" and a RawNlist
@@ -448,6 +585,7 @@ mod tests {
448585
             strings: strtab,
449586
             symtab: None,
450587
             dysymtab: None,
588
+            loh: Vec::new(),
451589
             data_in_code: Vec::new(),
452590
         };
453591
         let alias = InputSymbol::from_raw(RawNlist {
src/layout.rsmodified
278 lines changed — click to load
@@ -3,12 +3,12 @@
33
 //! Groups atoms into output sections, orders them deterministically, and
44
 //! assigns segment VM/file ranges once the final Mach-O header size is known.
55
 
6
-use std::collections::HashMap;
6
+use std::collections::{HashMap, HashSet};
77
 
88
 use crate::atom::AtomTable;
99
 use crate::input::ObjectFile;
1010
 use crate::macho::constants::SG_READ_ONLY;
11
-use crate::resolve::InputId;
11
+use crate::resolve::{AtomId, InputId};
1212
 use crate::section::{
1313
     is_zerofill, InputSection, OutputAtom, OutputSection, OutputSectionId, OutputSegment, Prot,
1414
 };
@@ -48,6 +48,24 @@ struct SectionKey {
4848
     name: String,
4949
 }
5050
 
51
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52
+pub enum ExtraSectionAnchor {
53
+    AfterSection { segment: String, name: String },
54
+    AfterAtom(AtomId),
55
+}
56
+
57
+#[derive(Debug, Clone, PartialEq, Eq)]
58
+pub struct ExtraOutputSection {
59
+    pub after_section: Option<ExtraSectionAnchor>,
60
+    pub section: OutputSection,
61
+}
62
+
63
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64
+pub struct ExtraLayoutSections<'a> {
65
+    pub extra_sections: &'a [ExtraOutputSection],
66
+    pub split_after_atoms: &'a [AtomId],
67
+}
68
+
5169
 fn output_section_key(input_section: &InputSection) -> SectionKey {
5270
     match (
5371
         input_section.segname.as_str(),
@@ -75,7 +93,7 @@ impl Layout {
7593
         atoms: &AtomTable,
7694
         header_size: u64,
7795
     ) -> Self {
78
-        Self::build_with_synthetics(kind, inputs, atoms, header_size, None)
96
+        Self::build_with_synthetics_filtered(kind, inputs, atoms, header_size, None, None)
7997
     }
8098
 
8199
     pub fn build_with_synthetics(
@@ -84,6 +102,51 @@ impl Layout {
84102
         atoms: &AtomTable,
85103
         header_size: u64,
86104
         synthetic_plan: Option<&SyntheticPlan>,
105
+    ) -> Self {
106
+        Self::build_with_synthetics_and_extra_filtered(
107
+            kind,
108
+            inputs,
109
+            atoms,
110
+            header_size,
111
+            synthetic_plan,
112
+            None,
113
+            ExtraLayoutSections {
114
+                extra_sections: &[],
115
+                split_after_atoms: &[],
116
+            },
117
+        )
118
+    }
119
+
120
+    pub fn build_with_synthetics_filtered(
121
+        kind: OutputKind,
122
+        inputs: &[LayoutInput<'_>],
123
+        atoms: &AtomTable,
124
+        header_size: u64,
125
+        synthetic_plan: Option<&SyntheticPlan>,
126
+        live_atoms: Option<&HashSet<AtomId>>,
127
+    ) -> Self {
128
+        Self::build_with_synthetics_and_extra_filtered(
129
+            kind,
130
+            inputs,
131
+            atoms,
132
+            header_size,
133
+            synthetic_plan,
134
+            live_atoms,
135
+            ExtraLayoutSections {
136
+                extra_sections: &[],
137
+                split_after_atoms: &[],
138
+            },
139
+        )
140
+    }
141
+
142
+    pub fn build_with_synthetics_and_extra_filtered(
143
+        kind: OutputKind,
144
+        inputs: &[LayoutInput<'_>],
145
+        atoms: &AtomTable,
146
+        header_size: u64,
147
+        synthetic_plan: Option<&SyntheticPlan>,
148
+        live_atoms: Option<&HashSet<AtomId>>,
149
+        extra_layout: ExtraLayoutSections<'_>,
87150
     ) -> Self {
88151
         let input_map: HashMap<InputId, LayoutInput<'_>> =
89152
             inputs.iter().map(|input| (input.id, *input)).collect();
@@ -92,6 +155,9 @@ impl Layout {
92155
         let mut section_index: HashMap<SectionKey, usize> = HashMap::new();
93156
 
94157
         for (atom_id, atom) in atoms.iter() {
158
+            if live_atoms.is_some_and(|live_atoms| !live_atoms.contains(&atom_id)) {
159
+                continue;
160
+            }
95161
             let input = input_map
96162
                 .get(&atom.origin)
97163
                 .unwrap_or_else(|| panic!("missing object for input {:?}", atom.origin));
@@ -159,7 +225,6 @@ impl Layout {
159225
                 }
160226
             }
161227
         }
162
-
163228
         sections.sort_by(|a, b| {
164229
             segment_rank(kind, &a.segment)
165230
                 .cmp(&segment_rank(kind, &b.segment))
@@ -193,7 +258,12 @@ impl Layout {
193258
                     .then_with(|| lhs.input_offset.cmp(&rhs.input_offset))
194259
                     .then_with(|| a.atom.cmp(&b.atom))
195260
             });
261
+        }
196262
 
263
+        split_sections_after_atoms(&mut sections, extra_layout.split_after_atoms);
264
+        insert_extra_sections(&mut sections, extra_layout.extra_sections);
265
+
266
+        for section in &mut sections {
197267
             let mut size = 0u64;
198268
             for placed in &mut section.atoms {
199269
                 let atom = atoms.get(placed.atom);
@@ -386,6 +456,90 @@ impl Layout {
386456
     }
387457
 }
388458
 
459
+fn split_sections_after_atoms(sections: &mut Vec<OutputSection>, split_after_atoms: &[AtomId]) {
460
+    if split_after_atoms.is_empty() {
461
+        return;
462
+    }
463
+    let split_points: HashSet<AtomId> = split_after_atoms.iter().copied().collect();
464
+    let mut out = Vec::with_capacity(sections.len());
465
+    for mut section in std::mem::take(sections) {
466
+        if section.atoms.len() < 2 || !section.synthetic_data.is_empty() {
467
+            out.push(section);
468
+            continue;
469
+        }
470
+        if !section
471
+            .atoms
472
+            .iter()
473
+            .any(|placed| split_points.contains(&placed.atom))
474
+        {
475
+            out.push(section);
476
+            continue;
477
+        }
478
+        let atoms = std::mem::take(&mut section.atoms);
479
+        let last_idx = atoms.len().saturating_sub(1);
480
+        let mut current = split_section_template(&section);
481
+        for (idx, placed) in atoms.into_iter().enumerate() {
482
+            let split_here = split_points.contains(&placed.atom) && idx != last_idx;
483
+            current.atoms.push(placed);
484
+            if split_here {
485
+                out.push(current);
486
+                current = split_section_template(&section);
487
+            }
488
+        }
489
+        out.push(current);
490
+    }
491
+    *sections = out;
492
+}
493
+
494
+fn split_section_template(section: &OutputSection) -> OutputSection {
495
+    OutputSection {
496
+        segment: section.segment.clone(),
497
+        name: section.name.clone(),
498
+        kind: section.kind,
499
+        align_pow2: section.align_pow2,
500
+        flags: section.flags,
501
+        reserved1: section.reserved1,
502
+        reserved2: section.reserved2,
503
+        reserved3: section.reserved3,
504
+        atoms: Vec::new(),
505
+        synthetic_offset: 0,
506
+        synthetic_data: Vec::new(),
507
+        addr: 0,
508
+        size: 0,
509
+        file_off: 0,
510
+    }
511
+}
512
+
513
+fn insert_extra_sections(sections: &mut Vec<OutputSection>, extra_sections: &[ExtraOutputSection]) {
514
+    for extra in extra_sections {
515
+        let section = extra.section.clone();
516
+        if let Some(anchor) = &extra.after_section {
517
+            let insert_at = sections
518
+                .iter()
519
+                .rposition(|candidate| match anchor {
520
+                    ExtraSectionAnchor::AfterSection { segment, name } => {
521
+                        candidate.segment == *segment && candidate.name == *name
522
+                    }
523
+                    ExtraSectionAnchor::AfterAtom(atom_id) => candidate
524
+                        .atoms
525
+                        .last()
526
+                        .map(|placed| placed.atom == *atom_id)
527
+                        .unwrap_or(false),
528
+                })
529
+                .map(|idx| idx + 1)
530
+                .unwrap_or_else(|| {
531
+                    panic!(
532
+                        "missing anchor {:?} for synthetic section {},{}",
533
+                        anchor, section.segment, section.name
534
+                    )
535
+                });
536
+            sections.insert(insert_at, section);
537
+        } else {
538
+            sections.push(section);
539
+        }
540
+    }
541
+}
542
+
389543
 fn merge_synthetic_section(existing: &mut OutputSection, synthetic: OutputSection) {
390544
     debug_assert_eq!(existing.segment, synthetic.segment);
391545
     debug_assert_eq!(existing.name, synthetic.name);
@@ -488,6 +642,7 @@ fn section_rank(segment: &str, section: &str) -> usize {
488642
     let order: &[&str] = match segment {
489643
         "__TEXT" => &[
490644
             "__text",
645
+            "__thunks",
491646
             "__stubs",
492647
             "__stub_helper",
493648
             "__cstring",
@@ -654,6 +809,7 @@ mod tests {
654809
             strings: crate::string_table::StringTable::from_bytes(vec![0]),
655810
             symtab: None,
656811
             dysymtab: None,
812
+            loh: Vec::new(),
657813
             data_in_code: Vec::new(),
658814
         };
659815
 
@@ -741,6 +897,7 @@ mod tests {
741897
             strings: crate::string_table::StringTable::from_bytes(vec![0]),
742898
             symtab: None,
743899
             dysymtab: None,
900
+            loh: Vec::new(),
744901
             data_in_code: Vec::new(),
745902
         };
746903
 
@@ -803,6 +960,7 @@ mod tests {
803960
             strings: crate::string_table::StringTable::from_bytes(vec![0]),
804961
             symtab: None,
805962
             dysymtab: None,
963
+            loh: Vec::new(),
806964
             data_in_code: Vec::new(),
807965
         };
808966
 
@@ -861,6 +1019,7 @@ mod tests {
8611019
             strings: crate::string_table::StringTable::from_bytes(vec![0]),
8621020
             symtab: None,
8631021
             dysymtab: None,
1022
+            loh: Vec::new(),
8641023
             data_in_code: Vec::new(),
8651024
         };
8661025
 
@@ -933,6 +1092,7 @@ mod tests {
9331092
             strings: crate::string_table::StringTable::from_bytes(vec![0]),
9341093
             symtab: None,
9351094
             dysymtab: None,
1095
+            loh: Vec::new(),
9361096
             data_in_code: Vec::new(),
9371097
         };
9381098
 
@@ -994,6 +1154,7 @@ mod tests {
9941154
             strings: crate::string_table::StringTable::from_bytes(vec![0]),
9951155
             symtab: None,
9961156
             dysymtab: None,
1157
+            loh: Vec::new(),
9971158
             data_in_code: Vec::new(),
9981159
         };
9991160
 
@@ -1129,6 +1290,7 @@ mod tests {
11291290
             strings: crate::string_table::StringTable::from_bytes(vec![0]),
11301291
             symtab: None,
11311292
             dysymtab: None,
1293
+            loh: Vec::new(),
11321294
             data_in_code: Vec::new(),
11331295
         };
11341296
 
@@ -1216,6 +1378,7 @@ mod tests {
12161378
             strings: crate::string_table::StringTable::from_bytes(vec![0]),
12171379
             symtab: None,
12181380
             dysymtab: None,
1381
+            loh: Vec::new(),
12191382
             data_in_code: Vec::new(),
12201383
         };
12211384
 
src/lib.rsmodified
766 lines changed — click to load
@@ -9,9 +9,12 @@ pub mod args;
99
 pub mod atom;
1010
 pub mod diag;
1111
 pub mod dump;
12
+pub mod icf;
1213
 pub mod input;
1314
 pub mod layout;
1415
 pub mod leb;
16
+pub mod link_map;
17
+pub mod loh;
1518
 pub mod macho;
1619
 pub mod reloc;
1720
 pub mod resolve;
@@ -19,23 +22,29 @@ pub mod section;
1922
 pub mod string_table;
2023
 pub mod symbol;
2124
 pub mod synth;
25
+pub mod why_live;
2226
 
2327
 use std::os::unix::fs::PermissionsExt;
2428
 use std::path::PathBuf;
29
+use std::time::{Duration, Instant};
2530
 use std::{fs, io};
2631
 
2732
 use atom::{atomize_object, backpatch_symbol_atoms, AtomTable};
28
-use layout::{Layout, LayoutInput};
33
+use icf::IcfError;
34
+use layout::{ExtraLayoutSections, Layout, LayoutInput};
2935
 use macho::dylib::{DylibDependency, DylibFile, DylibLoadKind};
3036
 use macho::reader::ReadError;
3137
 use macho::tbd::{parse_tbd, parse_version, Arch, Platform, Target};
3238
 use reloc::arm64::RelocError;
3339
 use resolve::{
34
-    classify_unresolved, drain_fetches, format_duplicate_diagnostic, format_undefined_diagnostic,
35
-    seed_all, DylibLoadMeta, InputAddError, Inputs, Symbol, SymbolTable, UndefinedTreatment,
40
+    classify_unresolved, drain_fetches, find_archive_by_path, force_load_all, force_load_archive,
41
+    format_duplicate_diagnostic, format_undefined_diagnostic, format_undefined_warning_diagnostic,
42
+    seed_all, DrainReport, DylibLoadMeta, InputAddError, Inputs, Symbol, SymbolTable,
43
+    UndefinedTreatment,
3644
 };
3745
 
3846
 const DEFAULT_TBD_VERSION: u32 = 1 << 16;
47
+const THUNK_PLAN_MAX_ITERATIONS: usize = 16;
3948
 
4049
 /// What kind of Mach-O file the linker is producing.
4150
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -44,14 +53,71 @@ pub enum OutputKind {
4453
     Dylib,
4554
 }
4655
 
56
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57
+pub enum IcfMode {
58
+    None,
59
+    Safe,
60
+    All,
61
+}
62
+
63
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64
+pub enum ThunkMode {
65
+    None,
66
+    Safe,
67
+    All,
68
+}
69
+
70
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71
+pub struct PlatformVersion {
72
+    pub minos: u32,
73
+    pub sdk: u32,
74
+}
75
+
76
+#[derive(Debug, Clone, PartialEq, Eq)]
77
+pub struct FrameworkSpec {
78
+    pub name: String,
79
+    pub weak: bool,
80
+}
81
+
4782
 /// User-facing linker configuration, populated by the CLI parser.
4883
 #[derive(Debug, Clone)]
4984
 pub struct LinkOptions {
5085
     pub inputs: Vec<PathBuf>,
86
+    pub library_names: Vec<String>,
87
+    pub frameworks: Vec<FrameworkSpec>,
88
+    pub search_paths: Vec<PathBuf>,
89
+    pub syslibroot: Option<PathBuf>,
90
+    pub platform_version: Option<PlatformVersion>,
91
+    pub undefined_treatment: UndefinedTreatment,
92
+    pub rpaths: Vec<String>,
93
+    pub install_name: Option<String>,
94
+    pub current_version: Option<u32>,
95
+    pub compatibility_version: Option<u32>,
96
+    pub exported_symbols_lists: Vec<PathBuf>,
97
+    pub unexported_symbols_lists: Vec<PathBuf>,
98
+    pub exported_symbols: Vec<String>,
99
+    pub unexported_symbols: Vec<String>,
100
+    pub map: Option<PathBuf>,
101
+    pub why_live: Vec<String>,
102
+    pub trace_inputs: bool,
103
+    pub show_version: bool,
104
+    pub show_help: bool,
51105
     pub output: Option<PathBuf>,
52106
     pub entry: Option<String>,
53107
     pub arch: Option<String>,
108
+    pub relocatable: bool,
109
+    pub bundle: bool,
110
+    pub objc_force_load: bool,
54111
     pub strip_locals: bool,
112
+    pub strip_debug: bool,
113
+    pub emit_uuid: bool,
114
+    pub dead_strip: bool,
115
+    pub no_loh: bool,
116
+    pub icf_mode: IcfMode,
117
+    pub thunks: ThunkMode,
118
+    pub fixup_chains: bool,
119
+    pub all_load: bool,
120
+    pub force_load_archives: Vec<PathBuf>,
55121
     pub kind: OutputKind,
56122
     /// When set, afs-ld operates in dump mode and prints the given file's
57123
     /// header + load commands instead of linking.
@@ -68,10 +134,41 @@ impl Default for LinkOptions {
68134
     fn default() -> Self {
69135
         Self {
70136
             inputs: Vec::new(),
137
+            library_names: Vec::new(),
138
+            frameworks: Vec::new(),
139
+            search_paths: Vec::new(),
140
+            syslibroot: None,
141
+            platform_version: None,
142
+            undefined_treatment: UndefinedTreatment::Error,
143
+            rpaths: Vec::new(),
144
+            install_name: None,
145
+            current_version: None,
146
+            compatibility_version: None,
147
+            exported_symbols_lists: Vec::new(),
148
+            unexported_symbols_lists: Vec::new(),
149
+            exported_symbols: Vec::new(),
150
+            unexported_symbols: Vec::new(),
151
+            map: None,
152
+            why_live: Vec::new(),
153
+            trace_inputs: false,
154
+            show_version: false,
155
+            show_help: false,
71156
             output: None,
72157
             entry: None,
73158
             arch: None,
159
+            relocatable: false,
160
+            bundle: false,
161
+            objc_force_load: false,
74162
             strip_locals: false,
163
+            strip_debug: false,
164
+            emit_uuid: true,
165
+            dead_strip: false,
166
+            no_loh: false,
167
+            icf_mode: IcfMode::None,
168
+            thunks: ThunkMode::Safe,
169
+            fixup_chains: false,
170
+            all_load: false,
171
+            force_load_archives: Vec::new(),
75172
             kind: OutputKind::Executable,
76173
             dump: None,
77174
             dump_archive: None,
@@ -94,11 +191,58 @@ pub enum LinkError {
94191
     Reloc(RelocError),
95192
     Synth(synth::SynthError),
96193
     Unwind(synth::unwind::UnwindError),
194
+    Icf(IcfError),
195
+    Loh(loh::LohError),
97196
     DuplicateSymbols(String),
98197
     UndefinedSymbols(String),
99198
     UnsupportedArch(String),
100199
     NoTbdDocument(PathBuf),
101200
     EntrySymbolNotFound(String),
201
+    ForceLoadNotArchive(PathBuf),
202
+    LibraryNotFound(String),
203
+    FrameworkNotFound(String),
204
+    ThunkPlanningDidNotConverge,
205
+    WhyLive(String),
206
+    UnsupportedOption(String),
207
+}
208
+
209
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
210
+pub struct LinkPhaseTimings {
211
+    pub input_parsing: Duration,
212
+    pub symbol_resolution: Duration,
213
+    pub atomization: Duration,
214
+    pub layout: Duration,
215
+    pub synth_sections: Duration,
216
+    pub synth_linkedit_finalize: Duration,
217
+    pub synth_linkedit_symbol_plan: Duration,
218
+    pub synth_linkedit_symbol_plan_locals: Duration,
219
+    pub synth_linkedit_symbol_plan_globals: Duration,
220
+    pub synth_linkedit_symbol_plan_strtab: Duration,
221
+    pub synth_linkedit_dyld_info: Duration,
222
+    pub synth_linkedit_metadata_tables: Duration,
223
+    pub synth_linkedit_code_signature: Duration,
224
+    pub synth_unwind: Duration,
225
+    pub reloc_apply: Duration,
226
+    pub write_output: Duration,
227
+}
228
+
229
+impl LinkPhaseTimings {
230
+    pub fn accounted_total(&self) -> Duration {
231
+        self.input_parsing
232
+            + self.symbol_resolution
233
+            + self.atomization
234
+            + self.layout
235
+            + self.synth_sections
236
+            + self.reloc_apply
237
+            + self.write_output
238
+    }
239
+}
240
+
241
+#[derive(Debug, Clone, PartialEq, Eq)]
242
+pub struct LinkProfile {
243
+    pub output: PathBuf,
244
+    pub phases: LinkPhaseTimings,
245
+    pub total_wall: Duration,
102246
 }
103247
 
104248
 impl std::fmt::Display for LinkError {
@@ -114,6 +258,8 @@ impl std::fmt::Display for LinkError {
114258
             LinkError::Reloc(e) => write!(f, "{e}"),
115259
             LinkError::Synth(e) => write!(f, "{e}"),
116260
             LinkError::Unwind(e) => write!(f, "{e}"),
261
+            LinkError::Icf(e) => write!(f, "{e}"),
262
+            LinkError::Loh(e) => write!(f, "{e}"),
117263
             LinkError::DuplicateSymbols(msg) | LinkError::UndefinedSymbols(msg) => {
118264
                 write!(f, "{msg}")
119265
             }
@@ -126,6 +272,24 @@ impl std::fmt::Display for LinkError {
126272
             LinkError::EntrySymbolNotFound(name) => {
127273
                 write!(f, "entry symbol `{name}` was not found in linked objects")
128274
             }
275
+            LinkError::ForceLoadNotArchive(path) => {
276
+                write!(
277
+                    f,
278
+                    "{}: -force_load requires a path that is also present as an archive input",
279
+                    path.display()
280
+                )
281
+            }
282
+            LinkError::LibraryNotFound(name) => {
283
+                write!(f, "unable to find library `{name}`")
284
+            }
285
+            LinkError::FrameworkNotFound(name) => {
286
+                write!(f, "unable to find framework `{name}`")
287
+            }
288
+            LinkError::ThunkPlanningDidNotConverge => {
289
+                write!(f, "thunk planning did not converge")
290
+            }
291
+            LinkError::WhyLive(msg) => write!(f, "{msg}"),
292
+            LinkError::UnsupportedOption(msg) => write!(f, "{msg}"),
129293
         }
130294
     }
131295
 }
@@ -192,13 +356,51 @@ impl From<synth::unwind::UnwindError> for LinkError {
192356
     }
193357
 }
194358
 
359
+impl From<IcfError> for LinkError {
360
+    fn from(value: IcfError) -> Self {
361
+        LinkError::Icf(value)
362
+    }
363
+}
364
+
365
+impl From<loh::LohError> for LinkError {
366
+    fn from(value: loh::LohError) -> Self {
367
+        LinkError::Loh(value)
368
+    }
369
+}
370
+
195371
 /// The linker itself. Sprint 0 only validates that inputs exist; later sprints
196372
 /// grow this into the full pipeline described in `.docs/overview.md`.
197373
 pub struct Linker;
198374
 
199375
 impl Linker {
200376
     pub fn run(opts: &LinkOptions) -> Result<(), LinkError> {
201
-        if opts.inputs.is_empty() {
377
+        Self::run_profiled(opts).map(|_| ())
378
+    }
379
+
380
+    pub fn run_profiled(opts: &LinkOptions) -> Result<LinkProfile, LinkError> {
381
+        let overall_started = Instant::now();
382
+        let mut phases = LinkPhaseTimings::default();
383
+        if opts.relocatable {
384
+            return Err(LinkError::UnsupportedOption(
385
+                "`-r` relocatable output is not yet supported".into(),
386
+            ));
387
+        }
388
+        if opts.bundle {
389
+            return Err(LinkError::UnsupportedOption(
390
+                "`-bundle` output is not yet supported".into(),
391
+            ));
392
+        }
393
+        if opts.fixup_chains {
394
+            return Err(LinkError::UnsupportedOption(
395
+                "`-fixup_chains` is not yet supported".into(),
396
+            ));
397
+        }
398
+        if opts.icf_mode == IcfMode::All {
399
+            return Err(LinkError::UnsupportedOption(
400
+                "`-icf=all` is not yet supported; use `-icf=safe` or `-icf=none`".into(),
401
+            ));
402
+        }
403
+        if opts.inputs.is_empty() && opts.library_names.is_empty() && opts.frameworks.is_empty() {
202404
             return Err(LinkError::NoInputs);
203405
         }
204406
 
@@ -208,12 +410,54 @@ impl Linker {
208410
             }
209411
         }
210412
 
413
+        if opts.strip_debug {
414
+            crate::diag::warning(
415
+                "`-S` requested, but afs-ld does not currently emit debug symbols",
416
+            );
417
+        }
418
+        if opts.objc_force_load {
419
+            crate::diag::warning(
420
+                "`-ObjC` requested, but afs-ld does not yet scan Objective-C archive metadata; the flag currently has no effect",
421
+            );
422
+        }
423
+        if opts.no_loh {
424
+            crate::diag::warning(
425
+                "`-no_loh` requested, but afs-ld currently matches Apple ld by omitting final-output LOH; the flag has no effect",
426
+            );
427
+        }
428
+
429
+        let mut load_paths = opts.inputs.clone();
430
+        let mut dylib_load_kinds = std::collections::HashMap::new();
431
+        for name in &opts.library_names {
432
+            let path = resolve_library_input(opts, name)?;
433
+            dylib_load_kinds.insert(path.clone(), DylibLoadKind::Normal);
434
+            load_paths.push(path);
435
+        }
436
+        for framework in &opts.frameworks {
437
+            let path = resolve_framework_input(opts, &framework.name)?;
438
+            dylib_load_kinds.insert(
439
+                path.clone(),
440
+                if framework.weak {
441
+                    DylibLoadKind::Weak
442
+                } else {
443
+                    DylibLoadKind::Normal
444
+                },
445
+            );
446
+            load_paths.push(path);
447
+        }
448
+
211449
         let mut inputs = Inputs::new();
212
-        for (load_order, path) in opts.inputs.iter().enumerate() {
450
+        let phase_started = Instant::now();
451
+        for (load_order, path) in load_paths.iter().enumerate() {
452
+            if opts.trace_inputs {
453
+                eprintln!("afs-ld: loading {}", path.display());
454
+            }
213455
             register_input(&mut inputs, path, load_order)?;
214456
         }
457
+        phases.input_parsing = phase_started.elapsed();
215458
 
216459
         let mut sym_table = SymbolTable::new();
460
+        let phase_started = Instant::now();
217461
         let seed_report = seed_all(&inputs, &mut sym_table)?;
218462
         if seed_report.has_errors() {
219463
             let mut msg = String::new();
@@ -223,7 +467,35 @@ impl Linker {
223467
             return Err(LinkError::DuplicateSymbols(msg));
224468
         }
225469
 
470
+        let mut force_report = DrainReport::default();
471
+        if opts.all_load {
472
+            force_load_all(&mut inputs, &mut sym_table, &mut force_report)?;
473
+        }
474
+        for archive_path in &opts.force_load_archives {
475
+            let Some(archive_id) = find_archive_by_path(&inputs, archive_path) else {
476
+                return Err(LinkError::ForceLoadNotArchive(archive_path.clone()));
477
+            };
478
+            force_load_archive(&mut inputs, &mut sym_table, archive_id, &mut force_report)?;
479
+        }
480
+        if opts.trace_inputs {
481
+            for path in &force_report.loaded_paths {
482
+                eprintln!("afs-ld: loading {}", path.display());
483
+            }
484
+        }
485
+        if !force_report.duplicates.is_empty() {
486
+            let mut msg = String::new();
487
+            for err in &force_report.duplicates {
488
+                msg.push_str(&format_duplicate_diagnostic(&sym_table, &inputs, err));
489
+            }
490
+            return Err(LinkError::DuplicateSymbols(msg));
491
+        }
492
+
226493
         let drain_report = drain_fetches(&mut inputs, &mut sym_table, seed_report.pending_fetches)?;
494
+        if opts.trace_inputs {
495
+            for path in &drain_report.loaded_paths {
496
+                eprintln!("afs-ld: loading {}", path.display());
497
+            }
498
+        }
227499
         if !drain_report.duplicates.is_empty() {
228500
             let mut msg = String::new();
229501
             for err in &drain_report.duplicates {
@@ -232,8 +504,9 @@ impl Linker {
232504
             return Err(LinkError::DuplicateSymbols(msg));
233505
         }
234506
         let mut referrers = seed_report.referrers.clone();
507
+        referrers.extend_from(&force_report.referrers);
235508
         referrers.extend_from(&drain_report.referrers);
236
-        let unresolved = classify_unresolved(&mut sym_table, UndefinedTreatment::Error);
509
+        let unresolved = classify_unresolved(&mut sym_table, opts.undefined_treatment);
237510
         if !unresolved.errors.is_empty() {
238511
             return Err(LinkError::UndefinedSymbols(format_undefined_diagnostic(
239512
                 &sym_table,
@@ -242,9 +515,19 @@ impl Linker {
242515
                 &unresolved.errors,
243516
             )));
244517
         }
518
+        if !unresolved.warnings.is_empty() {
519
+            crate::diag::warning_verbatim(&format_undefined_warning_diagnostic(
520
+                &sym_table,
521
+                &inputs,
522
+                &referrers,
523
+                &unresolved.warnings,
524
+            ));
525
+        }
526
+        phases.symbol_resolution = phase_started.elapsed();
245527
 
246528
         let mut atom_table = AtomTable::new();
247529
         let mut objects = Vec::new();
530
+        let phase_started = Instant::now();
248531
         for idx in 0..inputs.objects.len() {
249532
             let input_id = resolve::InputId(idx as u32);
250533
             let obj = inputs.object_file(input_id)?;
@@ -258,6 +541,7 @@ impl Linker {
258541
             );
259542
             objects.push((input_id, obj));
260543
         }
544
+        phases.atomization = phase_started.elapsed();
261545
 
262546
         let layout_inputs: Vec<LayoutInput<'_>> = objects
263547
             .iter()
@@ -278,43 +562,141 @@ impl Linker {
278562
                 continue;
279563
             }
280564
             dylib_loads.push(DylibDependency {
281
-                kind: DylibLoadKind::Normal,
565
+                kind: dylib_load_kinds
566
+                    .get(&dylib.path)
567
+                    .copied()
568
+                    .unwrap_or(DylibLoadKind::Normal),
282569
                 install_name: dylib.load_install_name.clone(),
283570
                 current_version: dylib.load_current_version,
284571
                 compatibility_version: dylib.load_compatibility_version,
285572
                 ordinal: dylib.ordinal,
286573
             });
287574
         }
288
-        let synthetic_plan = synth::SyntheticPlan::build(
575
+        let phase_started = Instant::now();
576
+        let parsed_relocs = macho::writer::build_parsed_reloc_cache(&layout_inputs)?;
577
+        phases.input_parsing += phase_started.elapsed();
578
+        let phase_started = Instant::now();
579
+        let entry_symbol = find_entry_symbol_id(opts, &sym_table)?;
580
+        let dead_strip = opts.dead_strip.then(|| {
581
+            why_live::DeadStripAnalysis::build(
582
+                opts,
583
+                &layout_inputs,
584
+                &atom_table,
585
+                &sym_table,
586
+                entry_symbol,
587
+            )
588
+        });
589
+        let icf = (opts.icf_mode == IcfMode::Safe)
590
+            .then(|| {
591
+                icf::fold_safe(
592
+                    &layout_inputs,
593
+                    &mut atom_table,
594
+                    &mut sym_table,
595
+                    dead_strip.as_ref().map(|analysis| analysis.live_atoms()),
596
+                )
597
+            })
598
+            .transpose()?;
599
+        let kept_atoms = if let Some(icf) = &icf {
600
+            Some(icf.kept_atoms())
601
+        } else {
602
+            dead_strip.as_ref().map(|analysis| analysis.live_atoms())
603
+        };
604
+        let synthetic_plan = synth::SyntheticPlan::build_filtered(
289605
             &layout_inputs,
290606
             &atom_table,
291607
             &mut sym_table,
292608
             &inputs.dylibs,
609
+            kept_atoms,
293610
         )?;
294
-        let mut layout = Layout::build_with_synthetics(
611
+        let icf_redirects = icf.as_ref().map(|plan| plan.redirects());
612
+        let mut layout = Layout::build_with_synthetics_filtered(
295613
             opts.kind,
296614
             &layout_inputs,
297615
             &atom_table,
298616
             0,
299617
             Some(&synthetic_plan),
618
+            kept_atoms,
300619
         );
620
+        let mut thunk_plan = None;
621
+        let mut thunk_converged = false;
622
+        for _ in 0..THUNK_PLAN_MAX_ITERATIONS {
623
+            let next_plan = reloc::arm64::plan_thunks(
624
+                opts,
625
+                &layout,
626
+                &layout_inputs,
627
+                &atom_table,
628
+                &sym_table,
629
+                Some(&synthetic_plan),
630
+                icf_redirects,
631
+            )?;
632
+            if next_plan == thunk_plan {
633
+                thunk_converged = true;
634
+                break;
635
+            }
636
+            let extra_sections = next_plan
637
+                .as_ref()
638
+                .map_or_else(Vec::new, |plan| plan.output_sections());
639
+            let split_after_atoms = next_plan
640
+                .as_ref()
641
+                .map_or_else(Vec::new, |plan| plan.split_after_atoms());
642
+            layout = Layout::build_with_synthetics_and_extra_filtered(
643
+                opts.kind,
644
+                &layout_inputs,
645
+                &atom_table,
646
+                0,
647
+                Some(&synthetic_plan),
648
+                kept_atoms,
649
+                ExtraLayoutSections {
650
+                    extra_sections: &extra_sections,
651
+                    split_after_atoms: &split_after_atoms,
652
+                },
653
+            );
654
+            thunk_plan = next_plan;
655
+        }
656
+        if !thunk_converged {
657
+            return Err(LinkError::ThunkPlanningDidNotConverge);
658
+        }
659
+        phases.layout = phase_started.elapsed();
301660
         let linkedit_context = macho::writer::LinkEditContext {
302661
             layout_inputs: &layout_inputs,
303662
             atom_table: &atom_table,
304663
             sym_table: &sym_table,
305664
             synthetic_plan: &synthetic_plan,
665
+            icf_redirects,
666
+            parsed_relocs: &parsed_relocs,
306667
         };
668
+        let phase_started = Instant::now();
307669
         let mut linkedit = None;
670
+        let mut synth_linkedit_finalize = Duration::ZERO;
671
+        let mut synth_linkedit_symbol_plan = Duration::ZERO;
672
+        let mut synth_linkedit_symbol_plan_locals = Duration::ZERO;
673
+        let mut synth_linkedit_symbol_plan_globals = Duration::ZERO;
674
+        let mut synth_linkedit_symbol_plan_strtab = Duration::ZERO;
675
+        let mut synth_linkedit_dyld_info = Duration::ZERO;
676
+        let mut synth_linkedit_metadata_tables = Duration::ZERO;
677
+        let mut synth_linkedit_code_signature = Duration::ZERO;
678
+        let mut synth_unwind = Duration::ZERO;
308679
         for _ in 0..4 {
309
-            let (next_layout, next_linkedit) = macho::writer::finalize_layout_with_linkedit(
310
-                &layout,
311
-                opts.kind,
312
-                opts,
313
-                &dylib_loads,
314
-                linkedit_context,
315
-            )?;
680
+            let phase_started = Instant::now();
681
+            let (next_layout, next_linkedit, linkedit_timings) =
682
+                macho::writer::finalize_layout_with_linkedit(
683
+                    &layout,
684
+                    opts.kind,
685
+                    opts,
686
+                    &dylib_loads,
687
+                    linkedit_context,
688
+                )?;
689
+            synth_linkedit_finalize += phase_started.elapsed();
690
+            synth_linkedit_symbol_plan += linkedit_timings.symbol_plan;
691
+            synth_linkedit_symbol_plan_locals += linkedit_timings.symbol_plan_locals;
692
+            synth_linkedit_symbol_plan_globals += linkedit_timings.symbol_plan_globals;
693
+            synth_linkedit_symbol_plan_strtab += linkedit_timings.symbol_plan_strtab;
694
+            synth_linkedit_dyld_info += linkedit_timings.dyld_info;
695
+            synth_linkedit_metadata_tables += linkedit_timings.metadata_tables;
696
+            synth_linkedit_code_signature += linkedit_timings.code_signature;
316697
             layout = next_layout;
317698
             linkedit = Some(next_linkedit);
699
+            let phase_started = Instant::now();
318700
             let changed = synth::unwind::synthesize(
319701
                 &mut layout,
320702
                 &layout_inputs,
@@ -322,20 +704,56 @@ impl Linker {
322704
                 &sym_table,
323705
                 &synthetic_plan,
324706
             )?;
707
+            synth_unwind += phase_started.elapsed();
325708
             if !changed {
326709
                 break;
327710
             }
328711
         }
329712
         let linkedit = linkedit.expect("finalize loop always runs at least once");
713
+        phases.synth_linkedit_finalize = synth_linkedit_finalize;
714
+        phases.synth_linkedit_symbol_plan = synth_linkedit_symbol_plan;
715
+        phases.synth_linkedit_symbol_plan_locals = synth_linkedit_symbol_plan_locals;
716
+        phases.synth_linkedit_symbol_plan_globals = synth_linkedit_symbol_plan_globals;
717
+        phases.synth_linkedit_symbol_plan_strtab = synth_linkedit_symbol_plan_strtab;
718
+        phases.synth_linkedit_dyld_info = synth_linkedit_dyld_info;
719
+        phases.synth_linkedit_metadata_tables = synth_linkedit_metadata_tables;
720
+        phases.synth_linkedit_code_signature = synth_linkedit_code_signature;
721
+        phases.synth_unwind = synth_unwind;
722
+        phases.synth_sections = phase_started.elapsed();
723
+        let phase_started = Instant::now();
330724
         reloc::arm64::apply_layout(
331725
             &mut layout,
332726
             &layout_inputs,
333727
             &atom_table,
334728
             &sym_table,
335
-            Some(&synthetic_plan),
336
-            &linkedit,
729
+            reloc::arm64::ApplyLayoutPlan {
730
+                synthetic_plan: Some(&synthetic_plan),
731
+                thunk_plan: thunk_plan.as_ref(),
732
+                linkedit: &linkedit,
733
+                icf_redirects,
734
+            },
337735
         )?;
736
+        phases.reloc_apply = phase_started.elapsed();
737
+        let folded_symbols = icf
738
+            .as_ref()
739
+            .map(|plan| plan.folded_symbols(&atom_table, &sym_table, &layout_inputs))
740
+            .unwrap_or_default();
338741
 
742
+        if let Some(report) = why_live::format_explanations(
743
+            opts,
744
+            &layout_inputs,
745
+            &atom_table,
746
+            &sym_table,
747
+            entry_symbol,
748
+            dead_strip.as_ref(),
749
+            &folded_symbols,
750
+        )
751
+        .map_err(LinkError::WhyLive)?
752
+        {
753
+            print!("{report}");
754
+        }
755
+
756
+        let phase_started = Instant::now();
339757
         let mut image = Vec::new();
340758
         let entry_point = resolve_entry_point(opts, &sym_table)?;
341759
         macho::writer::write_finalized_with_linkedit(
@@ -349,16 +767,95 @@ impl Linker {
349767
         )?;
350768
         let output = default_output_path(opts);
351769
         fs::write(&output, image)?;
770
+        if let Some(map_path) = &opts.map {
771
+            let dead_stripped = dead_strip
772
+                .as_ref()
773
+                .map(|analysis| {
774
+                    analysis.dead_stripped_symbols(&atom_table, &sym_table, &layout_inputs)
775
+                })
776
+                .unwrap_or_default();
777
+            link_map::write_link_map(
778
+                map_path,
779
+                opts,
780
+                &layout,
781
+                &layout_inputs,
782
+                &linkedit,
783
+                &folded_symbols,
784
+                &dead_stripped,
785
+            )?;
786
+        }
352787
         if opts.kind == OutputKind::Executable {
353788
             let mut perms = fs::metadata(&output)?.permissions();
354789
             let mode = perms.mode();
355790
             perms.set_mode(mode | ((mode & 0o444) >> 2));
356791
             fs::set_permissions(&output, perms)?;
357792
         }
358
-        Ok(())
793
+        phases.write_output = phase_started.elapsed();
794
+        Ok(LinkProfile {
795
+            output,
796
+            phases,
797
+            total_wall: overall_started.elapsed(),
798
+        })
359799
     }
360800
 }
361801
 
802
+fn resolve_library_input(opts: &LinkOptions, name: &str) -> Result<PathBuf, LinkError> {
803
+    let mut search_dirs = Vec::new();
804
+    for dir in &opts.search_paths {
805
+        search_dirs.push(dir.clone());
806
+        if let Some(root) = &opts.syslibroot {
807
+            if let Ok(stripped) = dir.strip_prefix("/") {
808
+                search_dirs.push(root.join(stripped));
809
+            }
810
+        }
811
+    }
812
+    if let Some(root) = &opts.syslibroot {
813
+        search_dirs.push(root.join("usr/lib"));
814
+    } else {
815
+        search_dirs.push(PathBuf::from("/usr/lib"));
816
+    }
817
+
818
+    let candidates = [
819
+        format!("lib{name}.tbd"),
820
+        format!("lib{name}.dylib"),
821
+        format!("lib{name}.a"),
822
+    ];
823
+    for dir in search_dirs {
824
+        for candidate in &candidates {
825
+            let path = dir.join(candidate);
826
+            if path.is_file() {
827
+                return Ok(path);
828
+            }
829
+        }
830
+    }
831
+    Err(LinkError::LibraryNotFound(name.to_string()))
832
+}
833
+
834
+fn resolve_framework_input(opts: &LinkOptions, name: &str) -> Result<PathBuf, LinkError> {
835
+    let mut roots = Vec::new();
836
+    if let Some(root) = &opts.syslibroot {
837
+        roots.push(root.join("System/Library/Frameworks"));
838
+        roots.push(root.join("Library/Frameworks"));
839
+    } else {
840
+        roots.push(PathBuf::from("/System/Library/Frameworks"));
841
+        roots.push(PathBuf::from("/Library/Frameworks"));
842
+    }
843
+
844
+    for root in roots {
845
+        let framework_dir = root.join(format!("{name}.framework"));
846
+        for candidate in [
847
+            framework_dir.join(format!("{name}.tbd")),
848
+            framework_dir.join(name),
849
+        ] {
850
+            if candidate.is_file() {
851
+                return Ok(candidate);
852
+            }
853
+        }
854
+    }
855
+
856
+    Err(LinkError::FrameworkNotFound(name.to_string()))
857
+}
858
+
362859
 fn default_output_path(opts: &LinkOptions) -> PathBuf {
363860
     opts.output
364861
         .clone()
@@ -432,6 +929,23 @@ fn resolve_entry_point(
432929
     opts: &LinkOptions,
433930
     sym_table: &SymbolTable,
434931
 ) -> Result<Option<macho::writer::EntryPoint>, LinkError> {
932
+    let Some(symbol_id) = find_entry_symbol_id(opts, sym_table)? else {
933
+        return Ok(None);
934
+    };
935
+    let Symbol::Defined { atom, value, .. } = sym_table.get(symbol_id) else {
936
+        let name = sym_table.interner.resolve(sym_table.get(symbol_id).name());
937
+        return Err(LinkError::EntrySymbolNotFound(name.to_string()));
938
+    };
939
+    Ok(Some(macho::writer::EntryPoint {
940
+        atom: *atom,
941
+        atom_value: *value,
942
+    }))
943
+}
944
+
945
+fn find_entry_symbol_id(
946
+    opts: &LinkOptions,
947
+    sym_table: &SymbolTable,
948
+) -> Result<Option<resolve::SymbolId>, LinkError> {
435949
     let name = if let Some(name) = &opts.entry {
436950
         name.as_str()
437951
     } else if opts.kind == OutputKind::Executable {
@@ -451,13 +965,7 @@ fn resolve_entry_point(
451965
     else {
452966
         return Err(LinkError::EntrySymbolNotFound(name.to_string()));
453967
     };
454
-    let Symbol::Defined { atom, value, .. } = sym_table.get(symbol_id) else {
455
-        return Err(LinkError::EntrySymbolNotFound(name.to_string()));
456
-    };
457
-    Ok(Some(macho::writer::EntryPoint {
458
-        atom: *atom,
459
-        atom_value: *value,
460
-    }))
968
+    Ok(Some(symbol_id))
461969
 }
462970
 
463971
 fn symbol_defined(sym_table: &SymbolTable, name: &str) -> bool {
src/loh.rsadded
552 lines changed — click to load
@@ -0,0 +1,552 @@
1
+//! ARM64 Linker Optimization Hints (LOH).
2
+//!
3
+//! `LC_LINKER_OPTIMIZATION_HINT` stores a ULEB128 stream of `(kind, argc,
4
+//! args...)` records. The args are file offsets of the participating
5
+//! instructions.
6
+
7
+use std::collections::HashSet;
8
+use std::fmt;
9
+
10
+use crate::layout::Layout;
11
+use crate::leb::{read_uleb, write_uleb};
12
+use crate::macho::reader::ReadError;
13
+use crate::macho::writer::LinkEditPlan;
14
+
15
+pub const LOH_ARM64_ADRP_LDR: u32 = 2;
16
+pub const LOH_ARM64_ADRP_LDR_GOT_LDR: u32 = 4;
17
+pub const LOH_ARM64_ADRP_ADD: u32 = 7;
18
+pub const LOH_ARM64_ADRP_LDR_GOT: u32 = 8;
19
+const NOP: u32 = 0xd503_201f;
20
+
21
+#[derive(Debug, Clone, PartialEq, Eq)]
22
+pub struct LohEntry {
23
+    pub kind: u32,
24
+    pub args: Vec<u32>,
25
+}
26
+
27
+#[derive(Debug, Clone, PartialEq, Eq)]
28
+pub struct LohError(String);
29
+
30
+impl fmt::Display for LohError {
31
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32
+        write!(f, "LOH relaxation error: {}", self.0)
33
+    }
34
+}
35
+
36
+impl std::error::Error for LohError {}
37
+
38
+impl From<ReadError> for LohError {
39
+    fn from(value: ReadError) -> Self {
40
+        Self(value.to_string())
41
+    }
42
+}
43
+
44
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45
+struct LocatedWord {
46
+    section_idx: usize,
47
+    atom_idx: usize,
48
+    word_off: usize,
49
+    addr: u64,
50
+    insn: u32,
51
+}
52
+
53
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54
+enum LiteralLoadKind {
55
+    W,
56
+    X,
57
+    S,
58
+    D,
59
+    Q,
60
+}
61
+
62
+pub fn parse_loh_blob(bytes: &[u8]) -> Result<Vec<LohEntry>, ReadError> {
63
+    let mut out = Vec::new();
64
+    let mut cursor = 0usize;
65
+    while cursor < bytes.len() {
66
+        if bytes[cursor..].iter().all(|&byte| byte == 0) {
67
+            break;
68
+        }
69
+        let at_offset = cursor as u32;
70
+        let (kind, used) = read_uleb(&bytes[cursor..])?;
71
+        cursor += used;
72
+        let (argc, used) = read_uleb(&bytes[cursor..])?;
73
+        cursor += used;
74
+        let kind = u32::try_from(kind).map_err(|_| ReadError::BadRelocation {
75
+            at_offset,
76
+            reason: "LOH kind overflows u32",
77
+        })?;
78
+        let argc = usize::try_from(argc).map_err(|_| ReadError::BadRelocation {
79
+            at_offset,
80
+            reason: "LOH argcount overflows usize",
81
+        })?;
82
+        let mut args = Vec::with_capacity(argc);
83
+        for _ in 0..argc {
84
+            let (arg, used) = read_uleb(&bytes[cursor..])?;
85
+            cursor += used;
86
+            args.push(u32::try_from(arg).map_err(|_| ReadError::BadRelocation {
87
+                at_offset,
88
+                reason: "LOH arg overflows u32",
89
+            })?);
90
+        }
91
+        out.push(LohEntry { kind, args });
92
+    }
93
+    Ok(out)
94
+}
95
+
96
+pub fn write_loh_blob(entries: &[LohEntry]) -> Vec<u8> {
97
+    let mut out = Vec::new();
98
+    for entry in entries {
99
+        write_uleb(entry.kind as u64, &mut out);
100
+        write_uleb(entry.args.len() as u64, &mut out);
101
+        for &arg in &entry.args {
102
+            write_uleb(arg as u64, &mut out);
103
+        }
104
+    }
105
+    out
106
+}
107
+
108
+pub fn relax_layout(
109
+    layout: &mut Layout,
110
+    linkedit: &LinkEditPlan,
111
+    enabled: bool,
112
+) -> Result<(), LohError> {
113
+    if !enabled || linkedit.loh.is_none() || linkedit.loh_bytes().is_empty() {
114
+        return Ok(());
115
+    }
116
+
117
+    let mut entries = parse_loh_blob(linkedit.loh_bytes())?;
118
+    entries.sort_by(|lhs, rhs| {
119
+        rhs.args
120
+            .len()
121
+            .cmp(&lhs.args.len())
122
+            .then_with(|| lhs.args.first().cmp(&rhs.args.first()))
123
+            .then_with(|| lhs.kind.cmp(&rhs.kind))
124
+    });
125
+    let mut rewritten = HashSet::new();
126
+    for entry in entries {
127
+        match entry.kind {
128
+            LOH_ARM64_ADRP_LDR => relax_adrp_ldr(layout, &entry, &mut rewritten)?,
129
+            LOH_ARM64_ADRP_LDR_GOT_LDR => relax_adrp_ldr_got_ldr(layout, &entry, &mut rewritten)?,
130
+            LOH_ARM64_ADRP_ADD => relax_adrp_add(layout, &entry, &mut rewritten)?,
131
+            LOH_ARM64_ADRP_LDR_GOT => relax_adrp_ldr_got(layout, &entry, &mut rewritten)?,
132
+            _ => {}
133
+        }
134
+    }
135
+    Ok(())
136
+}
137
+
138
+fn relax_adrp_add(
139
+    layout: &mut Layout,
140
+    entry: &LohEntry,
141
+    rewritten: &mut HashSet<u64>,
142
+) -> Result<(), LohError> {
143
+    if entry.args.len() != 2 {
144
+        return Ok(());
145
+    }
146
+    let adrp_off = entry.args[0] as u64;
147
+    let add_off = entry.args[1] as u64;
148
+    if !claim_offsets(rewritten, &[adrp_off, add_off]) {
149
+        return Ok(());
150
+    }
151
+    let adrp = locate_word(layout, adrp_off)?;
152
+    let add = locate_word(layout, add_off)?;
153
+    let Some(target) = decode_adrp_add_target(adrp.insn, add.insn, adrp.addr) else {
154
+        return Ok(());
155
+    };
156
+    let dest = (add.insn & 0x1f) as u8;
157
+    let Some(adr) = encode_adr(target, adrp.addr, dest) else {
158
+        return Ok(());
159
+    };
160
+    write_word(layout, adrp, adr)?;
161
+    write_word(layout, add, NOP)?;
162
+    Ok(())
163
+}
164
+
165
+fn relax_adrp_ldr(
166
+    layout: &mut Layout,
167
+    entry: &LohEntry,
168
+    rewritten: &mut HashSet<u64>,
169
+) -> Result<(), LohError> {
170
+    if entry.args.len() != 2 {
171
+        return Ok(());
172
+    }
173
+    let adrp_off = entry.args[0] as u64;
174
+    let ldr_off = entry.args[1] as u64;
175
+    if !claim_offsets(rewritten, &[adrp_off, ldr_off]) {
176
+        return Ok(());
177
+    }
178
+    let adrp = locate_word(layout, adrp_off)?;
179
+    let ldr = locate_word(layout, ldr_off)?;
180
+    let Some(target) = decode_adrp_ldr_target(adrp.insn, ldr.insn, adrp.addr) else {
181
+        return Ok(());
182
+    };
183
+    let Some(literal) = encode_ldr_literal(ldr.insn, target, ldr.addr) else {
184
+        return Ok(());
185
+    };
186
+    write_word(layout, adrp, NOP)?;
187
+    write_word(layout, ldr, literal)?;
188
+    Ok(())
189
+}
190
+
191
+fn relax_adrp_ldr_got(
192
+    layout: &mut Layout,
193
+    entry: &LohEntry,
194
+    rewritten: &mut HashSet<u64>,
195
+) -> Result<(), LohError> {
196
+    if entry.args.len() != 2 {
197
+        return Ok(());
198
+    }
199
+    let adrp_off = entry.args[0] as u64;
200
+    let ldr_off = entry.args[1] as u64;
201
+    if !claim_offsets(rewritten, &[adrp_off, ldr_off]) {
202
+        return Ok(());
203
+    }
204
+    let adrp = locate_word(layout, adrp_off)?;
205
+    let ldr = locate_word(layout, ldr_off)?;
206
+    let Some(got_slot_addr) = decode_adrp_ldr_target(adrp.insn, ldr.insn, adrp.addr) else {
207
+        return Ok(());
208
+    };
209
+    if pageoff_load_kind(ldr.insn) != Some(LiteralLoadKind::X) {
210
+        return Ok(());
211
+    }
212
+    let Some(local_target) = read_u64_at_addr(layout, got_slot_addr) else {
213
+        return Ok(());
214
+    };
215
+    if !points_into_output(layout, local_target) {
216
+        return Ok(());
217
+    }
218
+    let dest = (ldr.insn & 0x1f) as u8;
219
+    let Some(adr) = encode_adr(local_target, adrp.addr, dest) else {
220
+        return Ok(());
221
+    };
222
+    write_word(layout, adrp, adr)?;
223
+    write_word(layout, ldr, NOP)?;
224
+    Ok(())
225
+}
226
+
227
+fn relax_adrp_ldr_got_ldr(
228
+    layout: &mut Layout,
229
+    entry: &LohEntry,
230
+    rewritten: &mut HashSet<u64>,
231
+) -> Result<(), LohError> {
232
+    if entry.args.len() != 3 {
233
+        return Ok(());
234
+    }
235
+    let adrp_off = entry.args[0] as u64;
236
+    let got_ldr_off = entry.args[1] as u64;
237
+    let final_ldr_off = entry.args[2] as u64;
238
+    if !claim_offsets(rewritten, &[adrp_off, got_ldr_off, final_ldr_off]) {
239
+        return Ok(());
240
+    }
241
+    let adrp = locate_word(layout, adrp_off)?;
242
+    let got_ldr = locate_word(layout, got_ldr_off)?;
243
+    let final_ldr = locate_word(layout, final_ldr_off)?;
244
+    let Some(got_slot_addr) = decode_adrp_ldr_target(adrp.insn, got_ldr.insn, adrp.addr) else {
245
+        return Ok(());
246
+    };
247
+    if pageoff_load_kind(got_ldr.insn) != Some(LiteralLoadKind::X) {
248
+        return Ok(());
249
+    }
250
+    let got_dest = (got_ldr.insn & 0x1f) as u8;
251
+    if load_base_reg(final_ldr.insn) != Some(got_dest) {
252
+        return Ok(());
253
+    }
254
+    let Some(local_target) = read_u64_at_addr(layout, got_slot_addr) else {
255
+        return Ok(());
256
+    };
257
+    if !points_into_output(layout, local_target) {
258
+        return Ok(());
259
+    }
260
+    let Some(adr) = encode_adr(local_target, adrp.addr, got_dest) else {
261
+        return Ok(());
262
+    };
263
+    write_word(layout, adrp, adr)?;
264
+    write_word(layout, got_ldr, NOP)?;
265
+    Ok(())
266
+}
267
+
268
+fn claim_offsets(rewritten: &mut HashSet<u64>, offsets: &[u64]) -> bool {
269
+    if offsets.iter().any(|offset| rewritten.contains(offset)) {
270
+        return false;
271
+    }
272
+    rewritten.extend(offsets.iter().copied());
273
+    true
274
+}
275
+
276
+fn locate_word(layout: &Layout, file_offset: u64) -> Result<LocatedWord, LohError> {
277
+    for (section_idx, section) in layout.sections.iter().enumerate() {
278
+        for (atom_idx, atom) in section.atoms.iter().enumerate() {
279
+            let start = section.file_off + atom.offset;
280
+            let end = start + atom.data.len() as u64;
281
+            if !(start <= file_offset && file_offset + 4 <= end) {
282
+                continue;
283
+            }
284
+            let word_off = (file_offset - start) as usize;
285
+            let bytes = atom.data.get(word_off..word_off + 4).ok_or_else(|| {
286
+                LohError(format!(
287
+                    "instruction read OOB at file offset 0x{file_offset:x}"
288
+                ))
289
+            })?;
290
+            return Ok(LocatedWord {
291
+                section_idx,
292
+                atom_idx,
293
+                word_off,
294
+                addr: section.addr + atom.offset + word_off as u64,
295
+                insn: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
296
+            });
297
+        }
298
+    }
299
+    Err(LohError(format!(
300
+        "LOH instruction offset 0x{file_offset:x} did not resolve to an output atom"
301
+    )))
302
+}
303
+
304
+fn write_word(layout: &mut Layout, word: LocatedWord, insn: u32) -> Result<(), LohError> {
305
+    let atom = &mut layout.sections[word.section_idx].atoms[word.atom_idx];
306
+    let bytes = atom
307
+        .data
308
+        .get_mut(word.word_off..word.word_off + 4)
309
+        .ok_or_else(|| {
310
+            LohError(format!(
311
+                "instruction write OOB at section {} atom {} word {}",
312
+                word.section_idx, word.atom_idx, word.word_off
313
+            ))
314
+        })?;
315
+    bytes.copy_from_slice(&insn.to_le_bytes());
316
+    Ok(())
317
+}
318
+
319
+fn decode_adrp_add_target(adrp: u32, add: u32, place: u64) -> Option<u64> {
320
+    if !is_adrp(adrp) || !is_add_imm_64(add) {
321
+        return None;
322
+    }
323
+    let rd = (adrp & 0x1f) as u8;
324
+    let add_rd = (add & 0x1f) as u8;
325
+    let add_rn = ((add >> 5) & 0x1f) as u8;
326
+    if rd == 31 || add_rd != rd || add_rn != rd {
327
+        return None;
328
+    }
329
+    let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
330
+    let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
331
+    let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
332
+    let adrp_base = ((place as i64) & !0xfff) + (adrp_pages << 12);
333
+    let low = ((add >> 10) & 0xfff) as u64;
334
+    Some((adrp_base as u64) + low)
335
+}
336
+
337
+fn decode_adrp_ldr_target(adrp: u32, ldr: u32, place: u64) -> Option<u64> {
338
+    let _kind = pageoff_load_kind(ldr)?;
339
+    let base = ((ldr >> 5) & 0x1f) as u8;
340
+    let adrp_reg = (adrp & 0x1f) as u8;
341
+    if adrp_reg == 31 || base != adrp_reg {
342
+        return None;
343
+    }
344
+    let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
345
+    let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
346
+    let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
347
+    let adrp_base = ((place as i64) & !0xfff) + (adrp_pages << 12);
348
+    let shift = pageoff_shift(ldr);
349
+    let low = (((ldr >> 10) & 0xfff) as u64) << shift;
350
+    Some((adrp_base as u64) + low)
351
+}
352
+
353
+fn encode_adr(target: u64, place: u64, reg: u8) -> Option<u32> {
354
+    if reg == 31 {
355
+        return None;
356
+    }
357
+    let delta = (target as i64).wrapping_sub(place as i64);
358
+    if !fits_signed(delta, 21) {
359
+        return None;
360
+    }
361
+    let encoded = (delta as u32) & 0x1f_ffff;
362
+    let immlo = encoded & 0x3;
363
+    let immhi = (encoded >> 2) & 0x7ffff;
364
+    Some(0x1000_0000 | (immlo << 29) | (immhi << 5) | reg as u32)
365
+}
366
+
367
+fn encode_ldr_literal(insn: u32, target: u64, place: u64) -> Option<u32> {
368
+    let kind = pageoff_load_kind(insn)?;
369
+    let delta = (target as i64).wrapping_sub(place as i64);
370
+    if delta & 0b11 != 0 {
371
+        return None;
372
+    }
373
+    let imm = delta >> 2;
374
+    if !fits_signed(imm, 19) {
375
+        return None;
376
+    }
377
+    let encoded = (imm as u32) & 0x7ffff;
378
+    let rt = insn & 0x1f;
379
+    let base = match kind {
380
+        LiteralLoadKind::W => 0x1800_0000,
381
+        LiteralLoadKind::X => 0x5800_0000,
382
+        LiteralLoadKind::S => 0x1c00_0000,
383
+        LiteralLoadKind::D => 0x5c00_0000,
384
+        LiteralLoadKind::Q => 0x9c00_0000,
385
+    };
386
+    Some(base | (encoded << 5) | rt)
387
+}
388
+
389
+fn is_adrp(insn: u32) -> bool {
390
+    (insn & 0x9f00_0000) == 0x9000_0000
391
+}
392
+
393
+fn is_add_imm_64(insn: u32) -> bool {
394
+    (insn & 0xffc0_0000) == 0x9100_0000
395
+}
396
+
397
+fn pageoff_load_kind(insn: u32) -> Option<LiteralLoadKind> {
398
+    match insn & 0xffc0_0000 {
399
+        0xb940_0000 => Some(LiteralLoadKind::W),
400
+        0xf940_0000 => Some(LiteralLoadKind::X),
401
+        0xbd40_0000 => Some(LiteralLoadKind::S),
402
+        0xfd40_0000 => Some(LiteralLoadKind::D),
403
+        0x3dc0_0000 => Some(LiteralLoadKind::Q),
404
+        _ => None,
405
+    }
406
+}
407
+
408
+fn load_base_reg(insn: u32) -> Option<u8> {
409
+    match insn & 0xffc0_0000 {
410
+        0xb940_0000 | 0xf940_0000 | 0xbd40_0000 | 0xfd40_0000 | 0x3dc0_0000 | 0x7940_0000
411
+        | 0x3940_0000 => Some(((insn >> 5) & 0x1f) as u8),
412
+        _ => None,
413
+    }
414
+}
415
+
416
+fn pageoff_shift(insn: u32) -> u64 {
417
+    if is_simd_fp_pageoff(insn) {
418
+        let size = ((insn >> 30) & 0b11) as u64;
419
+        let opc = ((insn >> 22) & 0b11) as u64;
420
+        if size == 0 && (opc & 0b10) != 0 {
421
+            4
422
+        } else {
423
+            size
424
+        }
425
+    } else {
426
+        ((insn >> 30) & 0b11) as u64
427
+    }
428
+}
429
+
430
+fn is_simd_fp_pageoff(insn: u32) -> bool {
431
+    ((insn >> 24) & 0b111) == 0b101
432
+}
433
+
434
+fn points_into_output(layout: &Layout, addr: u64) -> bool {
435
+    layout
436
+        .sections
437
+        .iter()
438
+        .any(|section| section.addr <= addr && addr < section.addr + section.size)
439
+}
440
+
441
+fn read_u64_at_addr(layout: &Layout, addr: u64) -> Option<u64> {
442
+    let bytes = read_bytes_at_addr(layout, addr, 8)?;
443
+    Some(u64::from_le_bytes(bytes.try_into().ok()?))
444
+}
445
+
446
+fn read_bytes_at_addr(layout: &Layout, addr: u64, len: usize) -> Option<Vec<u8>> {
447
+    for section in &layout.sections {
448
+        for atom in &section.atoms {
449
+            let start = section.addr + atom.offset;
450
+            let end = start + atom.data.len() as u64;
451
+            if start <= addr && addr + len as u64 <= end {
452
+                let word_off = (addr - start) as usize;
453
+                return Some(atom.data.get(word_off..word_off + len)?.to_vec());
454
+            }
455
+        }
456
+        if !section.synthetic_data.is_empty() {
457
+            let start = section.addr + section.synthetic_offset;
458
+            let end = start + section.synthetic_data.len() as u64;
459
+            if start <= addr && addr + len as u64 <= end {
460
+                let word_off = (addr - start) as usize;
461
+                return Some(
462
+                    section
463
+                        .synthetic_data
464
+                        .get(word_off..word_off + len)?
465
+                        .to_vec(),
466
+                );
467
+            }
468
+        }
469
+    }
470
+    None
471
+}
472
+
473
+fn fits_signed(value: i64, bits: u32) -> bool {
474
+    let min = -(1i64 << (bits - 1));
475
+    let max = (1i64 << (bits - 1)) - 1;
476
+    (min..=max).contains(&value)
477
+}
478
+
479
+fn sign_extend_21(value: i64) -> i64 {
480
+    if value & (1 << 20) != 0 {
481
+        value | !0x1f_ffff
482
+    } else {
483
+        value
484
+    }
485
+}
486
+
487
+#[cfg(test)]
488
+mod tests {
489
+    use super::*;
490
+
491
+    #[test]
492
+    fn loh_blob_round_trips() {
493
+        let entries = vec![
494
+            LohEntry {
495
+                kind: LOH_ARM64_ADRP_ADD,
496
+                args: vec![0, 4],
497
+            },
498
+            LohEntry {
499
+                kind: LOH_ARM64_ADRP_LDR_GOT_LDR,
500
+                args: vec![8, 12, 16],
501
+            },
502
+        ];
503
+        let blob = write_loh_blob(&entries);
504
+        assert_eq!(parse_loh_blob(&blob).unwrap(), entries);
505
+    }
506
+
507
+    #[test]
508
+    fn loh_blob_ignores_trailing_zero_padding() {
509
+        let mut blob = write_loh_blob(&[LohEntry {
510
+            kind: LOH_ARM64_ADRP_ADD,
511
+            args: vec![0, 4],
512
+        }]);
513
+        while !blob.len().is_multiple_of(8) {
514
+            blob.push(0);
515
+        }
516
+        assert_eq!(
517
+            parse_loh_blob(&blob).unwrap(),
518
+            vec![LohEntry {
519
+                kind: LOH_ARM64_ADRP_ADD,
520
+                args: vec![0, 4],
521
+            }]
522
+        );
523
+    }
524
+
525
+    #[test]
526
+    fn encode_adr_round_trips_small_delta() {
527
+        let place = 0x1_0000_1000;
528
+        let target = place + 0x48;
529
+        let adr = encode_adr(target, place, 9).unwrap();
530
+        assert_eq!(adr & 0x1f, 9);
531
+        let immlo = ((adr >> 29) & 0x3) as i64;
532
+        let immhi = ((adr >> 5) & 0x7ffff) as i64;
533
+        let delta = sign_extend_21((immhi << 2) | immlo);
534
+        assert_eq!(place.wrapping_add_signed(delta), target);
535
+    }
536
+
537
+    #[test]
538
+    fn encode_ldr_literal_round_trips_x_load() {
539
+        let place = 0x1_0000_2004;
540
+        let target = place + 0x1fc;
541
+        let insn = 0xf940_0005u32;
542
+        let literal = encode_ldr_literal(insn, target, place).unwrap();
543
+        assert_eq!(literal & 0x1f, 5);
544
+        let imm = ((literal >> 5) & 0x7ffff) as i64;
545
+        let delta = if imm & (1 << 18) != 0 {
546
+            (imm | !0x7ffff) << 2
547
+        } else {
548
+            imm << 2
549
+        };
550
+        assert_eq!(place.wrapping_add_signed(delta), target);
551
+    }
552
+}
src/macho/dylib.rsmodified
15 lines changed — click to load
@@ -38,6 +38,15 @@ impl DylibLoadKind {
3838
             _ => None,
3939
         }
4040
     }
41
+
42
+    pub fn load_cmd(self) -> u32 {
43
+        match self {
44
+            DylibLoadKind::Normal => LC_LOAD_DYLIB,
45
+            DylibLoadKind::Weak => LC_LOAD_WEAK_DYLIB,
46
+            DylibLoadKind::Reexport => LC_REEXPORT_DYLIB,
47
+            DylibLoadKind::Upward => LC_LOAD_UPWARD_DYLIB,
48
+        }
49
+    }
4150
 }
4251
 
4352
 /// One dylib this file depends on. Ordinals match the two-level namespace
src/macho/writer.rsmodified
1434 lines changed — click to load
@@ -4,7 +4,9 @@
44
 
55
 use std::collections::HashMap;
66
 use std::fmt;
7
+use std::fs;
78
 use std::path::PathBuf;
9
+use std::time::Duration;
810
 
911
 use crate::atom::AtomTable;
1012
 use crate::input::{DataInCodeEntry, ObjectFile};
@@ -15,7 +17,8 @@ use crate::macho::dylib::DylibDependency;
1517
 use crate::macho::exports::{ExportEntry, ExportKind};
1618
 use crate::macho::reader::{
1719
     write_commands, write_header, BuildTool, BuildVersionCmd, DyldInfoCmd, DylibCmd, DysymtabCmd,
18
-    LinkEditDataCmd, LoadCommand, MachHeader64, Section64Header, Segment64, SymtabCmd, HEADER_SIZE,
20
+    LinkEditDataCmd, LoadCommand, MachHeader64, RpathCmd, Section64Header, Segment64, SymtabCmd,
21
+    HEADER_SIZE,
1922
 };
2023
 use crate::reloc::{parse_raw_relocs, parse_relocs, Referent, Reloc, RelocKind, RelocLength};
2124
 use crate::resolve::InputId;
@@ -46,6 +49,41 @@ pub struct LinkEditContext<'a> {
4649
     pub atom_table: &'a AtomTable,
4750
     pub sym_table: &'a SymbolTable,
4851
     pub synthetic_plan: &'a SyntheticPlan,
52
+    pub icf_redirects: Option<&'a HashMap<crate::resolve::AtomId, crate::resolve::AtomId>>,
53
+    pub parsed_relocs: &'a ParsedRelocCache,
54
+}
55
+
56
+pub type ParsedRelocCache = HashMap<(InputId, u8), Vec<Reloc>>;
57
+
58
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
59
+pub struct LinkEditBuildTimings {
60
+    pub symbol_plan: Duration,
61
+    pub symbol_plan_locals: Duration,
62
+    pub symbol_plan_globals: Duration,
63
+    pub symbol_plan_strtab: Duration,
64
+    pub dyld_info: Duration,
65
+    pub metadata_tables: Duration,
66
+    pub code_signature: Duration,
67
+}
68
+
69
+impl std::ops::AddAssign for LinkEditBuildTimings {
70
+    fn add_assign(&mut self, rhs: Self) {
71
+        self.symbol_plan += rhs.symbol_plan;
72
+        self.symbol_plan_locals += rhs.symbol_plan_locals;
73
+        self.symbol_plan_globals += rhs.symbol_plan_globals;
74
+        self.symbol_plan_strtab += rhs.symbol_plan_strtab;
75
+        self.dyld_info += rhs.dyld_info;
76
+        self.metadata_tables += rhs.metadata_tables;
77
+        self.code_signature += rhs.code_signature;
78
+    }
79
+}
80
+
81
+#[derive(Debug, Clone, PartialEq, Eq)]
82
+pub struct LinkMapSymbol {
83
+    pub name: String,
84
+    pub addr: u64,
85
+    pub size: u64,
86
+    pub file_index: usize,
4987
 }
5088
 
5189
 #[derive(Debug)]
@@ -60,7 +98,9 @@ pub enum WriteError {
6098
     ImportSymbolMissing(SymbolId),
6199
     ImportSymbolWrongKind(SymbolId),
62100
     MalformedRelocations(PathBuf, u8, String),
101
+    MalformedLoh(PathBuf, String),
63102
     MalformedDataInCode(PathBuf, String),
103
+    SymbolListRead(PathBuf, String),
64104
 }
65105
 
66106
 impl fmt::Display for WriteError {
@@ -113,6 +153,13 @@ impl fmt::Display for WriteError {
113153
                 path.display(),
114154
                 section
115155
             ),
156
+            WriteError::MalformedLoh(path, detail) => {
157
+                write!(
158
+                    f,
159
+                    "failed to remap LC_LINKER_OPTIMIZATION_HINT in {}: {detail}",
160
+                    path.display()
161
+                )
162
+            }
116163
             WriteError::MalformedDataInCode(path, detail) => {
117164
                 write!(
118165
                     f,
@@ -120,6 +167,13 @@ impl fmt::Display for WriteError {
120167
                     path.display()
121168
                 )
122169
             }
170
+            WriteError::SymbolListRead(path, detail) => {
171
+                write!(
172
+                    f,
173
+                    "{}: unable to read symbol list: {detail}",
174
+                    path.display()
175
+                )
176
+            }
123177
         }
124178
     }
125179
 }
@@ -162,40 +216,87 @@ pub fn finalize_layout_with_linkedit(
162216
     opts: &LinkOptions,
163217
     dylibs: &[DylibDependency],
164218
     context: LinkEditContext<'_>,
165
-) -> Result<(Layout, LinkEditPlan), WriteError> {
219
+) -> Result<(Layout, LinkEditPlan, LinkEditBuildTimings), WriteError> {
166220
     finalize_with_linkedit(layout, kind, opts, dylibs, Some(LinkEditInputs(context)))
167221
 }
168222
 
223
+pub fn build_parsed_reloc_cache(
224
+    inputs: &[LayoutInput<'_>],
225
+) -> Result<ParsedRelocCache, WriteError> {
226
+    let mut cache = HashMap::new();
227
+    for input in inputs {
228
+        for (sect_idx, section) in input.object.sections.iter().enumerate() {
229
+            if section.raw_relocs.is_empty() {
230
+                continue;
231
+            }
232
+            let section_idx = (sect_idx + 1) as u8;
233
+            let raws = parse_raw_relocs(&section.raw_relocs, 0, section.nreloc).map_err(|err| {
234
+                WriteError::MalformedRelocations(
235
+                    input.object.path.clone(),
236
+                    section_idx,
237
+                    err.to_string(),
238
+                )
239
+            })?;
240
+            let relocs = parse_relocs(&raws).map_err(|err| {
241
+                WriteError::MalformedRelocations(
242
+                    input.object.path.clone(),
243
+                    section_idx,
244
+                    err.to_string(),
245
+                )
246
+            })?;
247
+            cache.insert((input.id, section_idx), relocs);
248
+        }
249
+    }
250
+    Ok(cache)
251
+}
252
+
169253
 fn finalize_with_linkedit(
170254
     layout: &Layout,
171255
     kind: OutputKind,
172256
     opts: &LinkOptions,
173257
     dylibs: &[DylibDependency],
174258
     inputs: Option<LinkEditInputs<'_>>,
175
-) -> Result<(Layout, LinkEditPlan), WriteError> {
259
+) -> Result<(Layout, LinkEditPlan, LinkEditBuildTimings), WriteError> {
176260
     let mut layout = layout.clone();
177
-    let mut linkedit = build_linkedit_plan(&layout, kind, opts, inputs)?;
261
+    let (mut linkedit, mut timings) = build_linkedit_plan_profiled(&layout, kind, opts, inputs)?;
178262
     apply_indirect_starts(&mut layout, &linkedit);
179263
     let header_size = estimate_header_size(&layout, kind, opts, dylibs, &linkedit);
180264
     layout.relayout(header_size);
181265
 
182
-    linkedit = build_linkedit_plan(&layout, kind, opts, inputs)?;
183
-    apply_indirect_starts(&mut layout, &linkedit);
184
-    let sizeofcmds: u32 = build_commands(&layout, kind, opts, None, dylibs, &linkedit)?
185
-        .iter()
186
-        .map(LoadCommand::cmdsize)
187
-        .sum();
188
-    let header_size = HEADER_SIZE as u64 + sizeofcmds as u64;
189
-    layout.relayout(header_size);
190
-    linkedit = build_linkedit_plan(&layout, kind, opts, inputs)?;
266
+    let (next_linkedit, next_timings) = build_linkedit_plan_profiled(&layout, kind, opts, inputs)?;
267
+    linkedit = next_linkedit;
268
+    timings += next_timings;
191269
     apply_indirect_starts(&mut layout, &linkedit);
270
+    let exact_header_size =
271
+        HEADER_SIZE as u64 + exact_sizeofcmds(&layout, kind, opts, dylibs, &linkedit)? as u64;
272
+    if exact_header_size != header_size {
273
+        layout.relayout(exact_header_size);
274
+        let (next_linkedit, next_timings) =
275
+            build_linkedit_plan_profiled(&layout, kind, opts, inputs)?;
276
+        linkedit = next_linkedit;
277
+        timings += next_timings;
278
+        apply_indirect_starts(&mut layout, &linkedit);
279
+    }
192280
 
193281
     let linkedit_seg = layout
194282
         .segment_mut("__LINKEDIT")
195283
         .ok_or(WriteError::MissingSegment("__LINKEDIT"))?;
196284
     linkedit_seg.file_size = linkedit.total_size().max(1);
197285
     linkedit_seg.vm_size = align_up(linkedit.total_size().max(1), PAGE_SIZE);
198
-    Ok((layout, linkedit))
286
+    Ok((layout, linkedit, timings))
287
+}
288
+
289
+fn exact_sizeofcmds(
290
+    layout: &Layout,
291
+    kind: OutputKind,
292
+    opts: &LinkOptions,
293
+    dylibs: &[DylibDependency],
294
+    linkedit: &LinkEditPlan,
295
+) -> Result<u32, WriteError> {
296
+    Ok(build_commands(layout, kind, opts, None, dylibs, linkedit)?
297
+        .iter()
298
+        .map(LoadCommand::cmdsize)
299
+        .sum())
199300
 }
200301
 
201302
 pub fn write_finalized_with_dylibs(
@@ -270,6 +371,7 @@ pub fn write_finalized_with_linkedit(
270371
     let weak_bind_off = linkedit_plan.dyld_info.weak_bind_off as usize;
271372
     let lazy_bind_off = linkedit_plan.dyld_info.lazy_bind_off as usize;
272373
     let export_off = linkedit_plan.dyld_info.export_off as usize;
374
+    let loh_off = linkedit_plan.loh.map(|loh| loh.dataoff as usize);
273375
     let function_starts_off = linkedit_plan.function_starts.dataoff as usize;
274376
     let data_in_code_off = linkedit_plan.data_in_code.dataoff as usize;
275377
     let stroff = linkedit_plan.symtab.stroff as usize;
@@ -301,6 +403,12 @@ pub fn write_finalized_with_linkedit(
301403
         let end = export_off + linkedit_plan.export_bytes.len();
302404
         out[export_off..end].copy_from_slice(&linkedit_plan.export_bytes);
303405
     }
406
+    if let Some(loh_off) = loh_off {
407
+        if !linkedit_plan.loh_bytes.is_empty() {
408
+            let end = loh_off + linkedit_plan.loh_bytes.len();
409
+            out[loh_off..end].copy_from_slice(&linkedit_plan.loh_bytes);
410
+        }
411
+    }
304412
     if !linkedit_plan.function_starts_bytes.is_empty() {
305413
         let end = function_starts_off + linkedit_plan.function_starts_bytes.len();
306414
         out[function_starts_off..end].copy_from_slice(&linkedit_plan.function_starts_bytes);
@@ -343,43 +451,41 @@ fn build_commands(
343451
             commands.push(LoadCommand::Symtab(linkedit.symtab));
344452
             commands.push(LoadCommand::Dysymtab(linkedit.dysymtab));
345453
             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
-            }));
454
+            if opts.emit_uuid {
455
+                commands.push(raw_uuid_command(stable_uuid(layout, kind)));
456
+            }
457
+            commands.push(LoadCommand::BuildVersion(build_version_command(opts)));
356458
             commands.push(raw_source_version_command(0));
357459
             commands.push(raw_entry_point(resolve_entryoff(layout, entry_point)?, 0));
358460
         }
359461
         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)));
370462
             commands.push(LoadCommand::Dylib(DylibCmd {
371463
                 cmd: LC_ID_DYLIB,
372464
                 name: dylib_install_name(opts),
373465
                 timestamp: 2,
374
-                current_version: pack_version(1, 0, 0),
375
-                compatibility_version: pack_version(1, 0, 0),
466
+                current_version: dylib_current_version(opts),
467
+                compatibility_version: dylib_compatibility_version(opts),
376468
             }));
469
+            commands.push(LoadCommand::DyldInfoOnly(linkedit.dyld_info));
470
+            commands.push(LoadCommand::Symtab(linkedit.symtab));
471
+            commands.push(LoadCommand::Dysymtab(linkedit.dysymtab));
472
+            if opts.emit_uuid {
473
+                commands.push(raw_uuid_command(stable_uuid(layout, kind)));
474
+            }
475
+            commands.push(LoadCommand::BuildVersion(build_version_command(opts)));
476
+            commands.push(raw_source_version_command(0));
377477
         }
378478
     }
379479
 
480
+    for rpath in &opts.rpaths {
481
+        commands.push(LoadCommand::Rpath(RpathCmd {
482
+            path: rpath.clone(),
483
+        }));
484
+    }
485
+
380486
     for dylib in dylibs {
381487
         commands.push(LoadCommand::Dylib(DylibCmd {
382
-            cmd: LC_LOAD_DYLIB,
488
+            cmd: dylib.kind.load_cmd(),
383489
             name: dylib.install_name.clone(),
384490
             timestamp: 2,
385491
             current_version: dylib.current_version,
@@ -387,6 +493,13 @@ fn build_commands(
387493
         }));
388494
     }
389495
 
496
+    if let Some(loh) = linkedit.loh {
497
+        commands.push(raw_linkedit_command(
498
+            LC_LINKER_OPTIMIZATION_HINT,
499
+            loh.dataoff,
500
+            loh.datasize,
501
+        ));
502
+    }
390503
     commands.push(raw_linkedit_command(
391504
         LC_FUNCTION_STARTS,
392505
         linkedit.function_starts.dataoff,
@@ -406,12 +519,6 @@ fn build_commands(
406519
     } else {
407520
         commands.push(raw_linkedit_command(LC_CODE_SIGNATURE, 0, 0));
408521
     }
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
-    }
414
-
415522
     Ok(commands)
416523
 }
417524
 
@@ -420,41 +527,43 @@ fn estimate_header_size(
420527
     kind: OutputKind,
421528
     opts: &LinkOptions,
422529
     dylibs: &[DylibDependency],
423
-    _linkedit: &LinkEditPlan,
530
+    linkedit: &LinkEditPlan,
424531
 ) -> u64 {
425532
     let mut size = HEADER_SIZE as u64;
426533
     for segment in &layout.segments {
427534
         size += (8 + 64 + 80 * segment.sections.len()) as u64;
428535
     }
429
-    size += BuildVersionCmd {
430
-        platform: PLATFORM_MACOS,
431
-        minos: pack_version(11, 0, 0),
432
-        sdk: pack_version(11, 0, 0),
433
-        tools: vec![BuildTool {
434
-            tool: 3,
435
-            version: pack_version(0, 1, 0),
436
-        }],
536
+    size += build_version_command(opts).wire_size() as u64;
537
+    if opts.emit_uuid {
538
+        size += 24;
437539
     }
438
-    .wire_size() as u64;
439
-    size += 24;
440540
     size += match kind {
441541
         OutputKind::Executable => {
442542
             raw_dylinker_command("/usr/lib/dyld").cmdsize() as u64
443543
                 + 24
444544
                 + raw_source_version_command(0).cmdsize() as u64
445545
         }
446
-        OutputKind::Dylib => DylibCmd {
447
-            cmd: LC_ID_DYLIB,
448
-            name: dylib_install_name(opts),
449
-            timestamp: 2,
450
-            current_version: pack_version(1, 0, 0),
451
-            compatibility_version: pack_version(1, 0, 0),
546
+        OutputKind::Dylib => {
547
+            DylibCmd {
548
+                cmd: LC_ID_DYLIB,
549
+                name: dylib_install_name(opts),
550
+                timestamp: 2,
551
+                current_version: dylib_current_version(opts),
552
+                compatibility_version: dylib_compatibility_version(opts),
553
+            }
554
+            .wire_size() as u64
555
+                + raw_source_version_command(0).cmdsize() as u64
452556
         }
453
-        .wire_size() as u64,
454557
     };
558
+    for rpath in &opts.rpaths {
559
+        size += RpathCmd {
560
+            path: rpath.clone(),
561
+        }
562
+        .wire_size() as u64;
563
+    }
455564
     for dylib in dylibs {
456565
         size += DylibCmd {
457
-            cmd: LC_LOAD_DYLIB,
566
+            cmd: dylib.kind.load_cmd(),
458567
             name: dylib.install_name.clone(),
459568
             timestamp: 2,
460569
             current_version: dylib.current_version,
@@ -465,6 +574,9 @@ fn estimate_header_size(
465574
     size += SymtabCmd::WIRE_SIZE as u64;
466575
     size += DysymtabCmd::WIRE_SIZE as u64;
467576
     size += 16 * 3;
577
+    if linkedit.loh.is_some() {
578
+        size += 16;
579
+    }
468580
     size += DyldInfoCmd::WIRE_SIZE as u64;
469581
     size
470582
 }
@@ -570,6 +682,22 @@ fn raw_linkedit_command(cmd: u32, dataoff: u32, datasize: u32) -> LoadCommand {
570682
     }
571683
 }
572684
 
685
+fn build_version_command(opts: &LinkOptions) -> BuildVersionCmd {
686
+    let platform = opts.platform_version.unwrap_or(crate::PlatformVersion {
687
+        minos: pack_version(11, 0, 0),
688
+        sdk: pack_version(11, 0, 0),
689
+    });
690
+    BuildVersionCmd {
691
+        platform: PLATFORM_MACOS,
692
+        minos: platform.minos,
693
+        sdk: platform.sdk,
694
+        tools: vec![BuildTool {
695
+            tool: 3,
696
+            version: pack_version(0, 1, 0),
697
+        }],
698
+    }
699
+}
700
+
573701
 fn stable_uuid(layout: &Layout, kind: OutputKind) -> [u8; 16] {
574702
     fn mix(state: &mut u64, bytes: &[u8]) {
575703
         for byte in bytes {
@@ -627,6 +755,9 @@ fn header_flags(layout: &Layout, kind: OutputKind) -> u32 {
627755
 }
628756
 
629757
 fn dylib_install_name(opts: &LinkOptions) -> String {
758
+    if let Some(name) = &opts.install_name {
759
+        return name.clone();
760
+    }
630761
     if let Some(path) = &opts.output {
631762
         if let Some(name) = path.file_name().and_then(|name| name.to_str()) {
632763
             return format!("@rpath/{name}");
@@ -636,12 +767,23 @@ fn dylib_install_name(opts: &LinkOptions) -> String {
636767
     "@rpath/a.out.dylib".to_string()
637768
 }
638769
 
770
+fn dylib_current_version(opts: &LinkOptions) -> u32 {
771
+    opts.current_version
772
+        .unwrap_or_else(|| pack_version(1, 0, 0))
773
+}
774
+
775
+fn dylib_compatibility_version(opts: &LinkOptions) -> u32 {
776
+    opts.compatibility_version
777
+        .unwrap_or_else(|| pack_version(1, 0, 0))
778
+}
779
+
639780
 #[derive(Debug, Clone, PartialEq, Eq)]
640781
 pub struct LinkEditPlan {
641782
     base_off: u32,
642783
     pub symtab: SymtabCmd,
643784
     pub dysymtab: DysymtabCmd,
644785
     pub dyld_info: DyldInfoCmd,
786
+    pub loh: Option<LinkEditDataCmd>,
645787
     pub function_starts: LinkEditDataCmd,
646788
     pub data_in_code: LinkEditDataCmd,
647789
     pub symtab_bytes: Vec<u8>,
@@ -651,12 +793,14 @@ pub struct LinkEditPlan {
651793
     weak_bind_bytes: Vec<u8>,
652794
     lazy_bind_bytes: Vec<u8>,
653795
     export_bytes: Vec<u8>,
796
+    loh_bytes: Vec<u8>,
654797
     function_starts_bytes: Vec<u8>,
655798
     data_in_code_bytes: Vec<u8>,
656799
     pub strtab_bytes: Vec<u8>,
657800
     code_signature: Option<CodeSignaturePlan>,
658801
     indirect_starts: HashMap<(String, String), u32>,
659802
     lazy_bind_offsets: HashMap<SymbolId, u32>,
803
+    pub map_symbols: Vec<LinkMapSymbol>,
660804
 }
661805
 
662806
 impl LinkEditPlan {
@@ -678,6 +822,10 @@ impl LinkEditPlan {
678822
     pub fn lazy_bind_offset(&self, symbol: SymbolId) -> Option<u32> {
679823
         self.lazy_bind_offsets.get(&symbol).copied()
680824
     }
825
+
826
+    pub fn loh_bytes(&self) -> &[u8] {
827
+        &self.loh_bytes
828
+    }
681829
 }
682830
 
683831
 fn build_linkedit_plan(
@@ -686,6 +834,16 @@ fn build_linkedit_plan(
686834
     opts: &LinkOptions,
687835
     inputs: Option<LinkEditInputs<'_>>,
688836
 ) -> Result<LinkEditPlan, WriteError> {
837
+    build_linkedit_plan_profiled(layout, kind, opts, inputs).map(|(plan, _)| plan)
838
+}
839
+
840
+fn build_linkedit_plan_profiled(
841
+    layout: &Layout,
842
+    kind: OutputKind,
843
+    opts: &LinkOptions,
844
+    inputs: Option<LinkEditInputs<'_>>,
845
+) -> Result<(LinkEditPlan, LinkEditBuildTimings), WriteError> {
846
+    let mut timings = LinkEditBuildTimings::default();
689847
     let linkedit = layout
690848
         .segment("__LINKEDIT")
691849
         .cloned()
@@ -693,53 +851,76 @@ fn build_linkedit_plan(
693851
     let base_off = u32_fit(linkedit.file_off, "linkedit file offset")?;
694852
 
695853
     let Some(inputs) = inputs else {
696
-        return Ok(LinkEditPlan {
697
-            base_off,
698
-            symtab: SymtabCmd {
699
-                symoff: base_off,
700
-                nsyms: 0,
701
-                stroff: base_off,
702
-                strsize: 8,
703
-            },
704
-            dysymtab: DysymtabCmd::default(),
705
-            dyld_info: DyldInfoCmd::default(),
706
-            function_starts: LinkEditDataCmd {
707
-                dataoff: base_off,
708
-                datasize: 0,
709
-            },
710
-            data_in_code: LinkEditDataCmd {
711
-                dataoff: base_off,
712
-                datasize: 0,
854
+        let phase_started = std::time::Instant::now();
855
+        let code_signature = Some(build_code_signature(
856
+            layout,
857
+            kind,
858
+            opts,
859
+            base_off as u64 + 8,
860
+        )?);
861
+        timings.code_signature += phase_started.elapsed();
862
+        return Ok((
863
+            LinkEditPlan {
864
+                base_off,
865
+                symtab: SymtabCmd {
866
+                    symoff: base_off,
867
+                    nsyms: 0,
868
+                    stroff: base_off,
869
+                    strsize: 8,
870
+                },
871
+                dysymtab: DysymtabCmd::default(),
872
+                dyld_info: DyldInfoCmd::default(),
873
+                loh: None,
874
+                function_starts: LinkEditDataCmd {
875
+                    dataoff: base_off,
876
+                    datasize: 0,
877
+                },
878
+                data_in_code: LinkEditDataCmd {
879
+                    dataoff: base_off,
880
+                    datasize: 0,
881
+                },
882
+                symtab_bytes: Vec::new(),
883
+                indirect_bytes: Vec::new(),
884
+                rebase_bytes: Vec::new(),
885
+                bind_bytes: Vec::new(),
886
+                weak_bind_bytes: Vec::new(),
887
+                lazy_bind_bytes: Vec::new(),
888
+                export_bytes: Vec::new(),
889
+                loh_bytes: Vec::new(),
890
+                function_starts_bytes: Vec::new(),
891
+                data_in_code_bytes: Vec::new(),
892
+                strtab_bytes: vec![0; 8],
893
+                code_signature,
894
+                indirect_starts: HashMap::new(),
895
+                lazy_bind_offsets: HashMap::new(),
896
+                map_symbols: Vec::new(),
713897
             },
714
-            symtab_bytes: Vec::new(),
715
-            indirect_bytes: Vec::new(),
716
-            rebase_bytes: Vec::new(),
717
-            bind_bytes: Vec::new(),
718
-            weak_bind_bytes: Vec::new(),
719
-            lazy_bind_bytes: Vec::new(),
720
-            export_bytes: Vec::new(),
721
-            function_starts_bytes: Vec::new(),
722
-            data_in_code_bytes: Vec::new(),
723
-            strtab_bytes: vec![0; 8],
724
-            code_signature: Some(build_code_signature(
725
-                layout,
726
-                kind,
727
-                opts,
728
-                base_off as u64 + 8,
729
-            )?),
730
-            indirect_starts: HashMap::new(),
731
-            lazy_bind_offsets: HashMap::new(),
732
-        });
898
+            timings,
899
+        ));
733900
     };
734901
     let sym_table = inputs.0.sym_table;
735902
     let synthetic_plan = inputs.0.synthetic_plan;
736903
 
904
+    let phase_started = std::time::Instant::now();
737905
     let imports = collect_imports(sym_table, synthetic_plan)?;
738906
     let import_lookup: HashMap<SymbolId, &ImportSymbolRecord> = imports
739907
         .iter()
740908
         .map(|record| (record.symbol, record))
741909
         .collect();
742
-    let symbol_plan = build_output_symbols(layout, kind, opts.strip_locals, inputs, &imports)?;
910
+    let visibility = SymbolVisibilityPolicy::from_opts(opts)?;
911
+    let (symbol_plan, symbol_plan_timings) = build_output_symbols_profiled(
912
+        layout,
913
+        kind,
914
+        opts.dead_strip,
915
+        opts.strip_locals,
916
+        &visibility,
917
+        inputs,
918
+        &imports,
919
+    )?;
920
+    timings.symbol_plan += phase_started.elapsed();
921
+    timings.symbol_plan_locals += symbol_plan_timings.locals;
922
+    timings.symbol_plan_globals += symbol_plan_timings.globals;
923
+    timings.symbol_plan_strtab += symbol_plan_timings.strtab;
743924
     let mut symtab_bytes = Vec::new();
744925
     write_nlist_table(&symbol_plan.symbols, &mut symtab_bytes);
745926
 
@@ -775,16 +956,31 @@ fn build_linkedit_plan(
775956
         indirect_bytes.extend_from_slice(&index.to_le_bytes());
776957
     }
777958
 
959
+    let phase_started = std::time::Instant::now();
778960
     let bind_streams = build_bind_streams(layout, synthetic_plan, &import_lookup)?;
779961
     let rebase_bytes = pad_dyld_info_stream(build_rebase_stream(layout, synthetic_plan, inputs)?);
780962
     let bind_bytes = pad_dyld_info_stream(bind_streams.bind);
781963
     let weak_bind_bytes = pad_dyld_info_stream(bind_streams.weak_bind);
782964
     let lazy_bind_bytes = pad_dyld_info_stream(bind_streams.lazy_bind);
783965
     let export_bytes = pad_dyld_info_stream(build_export_trie(&symbol_plan.exports));
966
+    timings.dyld_info += phase_started.elapsed();
967
+
968
+    let phase_started = std::time::Instant::now();
969
+    let loh_bytes = build_loh(
970
+        layout,
971
+        inputs.0.layout_inputs,
972
+        inputs.0.atom_table,
973
+        inputs.0.icf_redirects,
974
+    )?;
784975
     let function_starts_bytes =
785976
         build_function_starts(layout, inputs.0.layout_inputs, inputs.0.atom_table)?;
786
-    let data_in_code_bytes =
787
-        build_data_in_code(layout, inputs.0.layout_inputs, inputs.0.atom_table)?;
977
+    let data_in_code_bytes = build_data_in_code(
978
+        layout,
979
+        inputs.0.layout_inputs,
980
+        inputs.0.atom_table,
981
+        inputs.0.icf_redirects,
982
+    )?;
983
+    timings.metadata_tables += phase_started.elapsed();
788984
 
789985
     let mut cursor = base_off as u64;
790986
     let rebase_off = place_optional_block(&mut cursor, rebase_bytes.len(), "rebase stream offset")?;
@@ -800,6 +996,7 @@ fn build_linkedit_plan(
800996
         "lazy bind stream offset",
801997
     )?;
802998
     let export_off = place_optional_block(&mut cursor, export_bytes.len(), "export trie offset")?;
999
+    let loh = place_optional_linkedit_data_block(&mut cursor, loh_bytes.len(), "LOH offset")?;
8031000
     let function_starts = place_linkedit_data_block(
8041001
         &mut cursor,
8051002
         function_starts_bytes.len(),
@@ -819,47 +1016,56 @@ fn build_linkedit_plan(
8191016
         "string table offset",
8201017
     )?;
8211018
     let regular_end = stroff as u64 + symbol_plan.strtab_bytes.len() as u64;
822
-    Ok(LinkEditPlan {
823
-        base_off,
824
-        symtab: SymtabCmd {
825
-            symoff,
826
-            nsyms: symbol_plan.symbols.len() as u32,
827
-            stroff,
828
-            strsize: symbol_plan.strtab_bytes.len() as u32,
829
-        },
830
-        dysymtab: DysymtabCmd {
831
-            indirectsymoff,
832
-            nindirectsyms: indirect_symbols.len() as u32,
833
-            ..symbol_plan.dysymtab
834
-        },
835
-        dyld_info: DyldInfoCmd {
836
-            rebase_off,
837
-            rebase_size: rebase_bytes.len() as u32,
838
-            bind_off: bindoff,
839
-            bind_size: bind_bytes.len() as u32,
840
-            weak_bind_off,
841
-            weak_bind_size: weak_bind_bytes.len() as u32,
842
-            lazy_bind_off,
843
-            lazy_bind_size: lazy_bind_bytes.len() as u32,
844
-            export_off,
845
-            export_size: export_bytes.len() as u32,
1019
+    let phase_started = std::time::Instant::now();
1020
+    let code_signature = Some(build_code_signature(layout, kind, opts, regular_end)?);
1021
+    timings.code_signature += phase_started.elapsed();
1022
+    Ok((
1023
+        LinkEditPlan {
1024
+            base_off,
1025
+            symtab: SymtabCmd {
1026
+                symoff,
1027
+                nsyms: symbol_plan.symbols.len() as u32,
1028
+                stroff,
1029
+                strsize: symbol_plan.strtab_bytes.len() as u32,
1030
+            },
1031
+            dysymtab: DysymtabCmd {
1032
+                indirectsymoff,
1033
+                nindirectsyms: indirect_symbols.len() as u32,
1034
+                ..symbol_plan.dysymtab
1035
+            },
1036
+            dyld_info: DyldInfoCmd {
1037
+                rebase_off,
1038
+                rebase_size: rebase_bytes.len() as u32,
1039
+                bind_off: bindoff,
1040
+                bind_size: bind_bytes.len() as u32,
1041
+                weak_bind_off,
1042
+                weak_bind_size: weak_bind_bytes.len() as u32,
1043
+                lazy_bind_off,
1044
+                lazy_bind_size: lazy_bind_bytes.len() as u32,
1045
+                export_off,
1046
+                export_size: export_bytes.len() as u32,
1047
+            },
1048
+            loh,
1049
+            function_starts,
1050
+            data_in_code,
1051
+            symtab_bytes,
1052
+            indirect_bytes,
1053
+            rebase_bytes,
1054
+            bind_bytes,
1055
+            weak_bind_bytes,
1056
+            lazy_bind_bytes,
1057
+            export_bytes,
1058
+            loh_bytes,
1059
+            function_starts_bytes,
1060
+            data_in_code_bytes,
1061
+            strtab_bytes: symbol_plan.strtab_bytes,
1062
+            code_signature,
1063
+            indirect_starts,
1064
+            lazy_bind_offsets: bind_streams.lazy_offsets,
1065
+            map_symbols: symbol_plan.map_symbols,
8461066
         },
847
-        function_starts,
848
-        data_in_code,
849
-        symtab_bytes,
850
-        indirect_bytes,
851
-        rebase_bytes,
852
-        bind_bytes,
853
-        weak_bind_bytes,
854
-        lazy_bind_bytes,
855
-        export_bytes,
856
-        function_starts_bytes,
857
-        data_in_code_bytes,
858
-        strtab_bytes: symbol_plan.strtab_bytes,
859
-        code_signature: Some(build_code_signature(layout, kind, opts, regular_end)?),
860
-        indirect_starts,
861
-        lazy_bind_offsets: bind_streams.lazy_offsets,
862
-    })
1067
+        timings,
1068
+    ))
8631069
 }
8641070
 
8651071
 fn build_code_signature(
@@ -909,11 +1115,51 @@ struct OutputSymbolSpec {
9091115
     n_sect: u8,
9101116
     n_desc: u16,
9111117
     n_value: u64,
1118
+    size: u64,
1119
+    file_index: usize,
1120
+}
1121
+
1122
+#[derive(Debug, Clone)]
1123
+struct SymbolVisibilityPolicy {
1124
+    exported: Vec<String>,
1125
+    unexported: Vec<String>,
1126
+}
1127
+
1128
+impl SymbolVisibilityPolicy {
1129
+    fn from_opts(opts: &LinkOptions) -> Result<Self, WriteError> {
1130
+        let mut exported = opts.exported_symbols.clone();
1131
+        let mut unexported = opts.unexported_symbols.clone();
1132
+        for path in &opts.exported_symbols_lists {
1133
+            exported.extend(read_symbol_patterns(path)?);
1134
+        }
1135
+        for path in &opts.unexported_symbols_lists {
1136
+            unexported.extend(read_symbol_patterns(path)?);
1137
+        }
1138
+        Ok(Self {
1139
+            exported,
1140
+            unexported,
1141
+        })
1142
+    }
1143
+
1144
+    fn hides(&self, name: &str) -> bool {
1145
+        if !self.exported.is_empty()
1146
+            && !self
1147
+                .exported
1148
+                .iter()
1149
+                .any(|pattern| wildcard_matches(pattern, name))
1150
+        {
1151
+            return true;
1152
+        }
1153
+        self.unexported
1154
+            .iter()
1155
+            .any(|pattern| wildcard_matches(pattern, name))
1156
+    }
9121157
 }
9131158
 
9141159
 #[derive(Debug, Clone)]
9151160
 struct SymbolTablePlan {
9161161
     symbols: Vec<InputSymbol>,
1162
+    map_symbols: Vec<LinkMapSymbol>,
9171163
     strtab_bytes: Vec<u8>,
9181164
     symbol_indices: HashMap<SymbolId, u32>,
9191165
     exports: Vec<ExportEntry>,
@@ -988,36 +1234,13 @@ fn collect_rebase_sites(
9881234
         synthetic_plan,
9891235
         inputs.0.sym_table,
9901236
     )?);
991
-    let mut reloc_cache: HashMap<(InputId, u8), Vec<Reloc>> = HashMap::new();
9921237
     let input_map: HashMap<InputId, &ObjectFile> = inputs
9931238
         .0
9941239
         .layout_inputs
9951240
         .iter()
9961241
         .map(|input| (input.id, input.object))
9971242
         .collect();
998
-
999
-    for input in inputs.0.layout_inputs {
1000
-        for (sect_idx, section) in input.object.sections.iter().enumerate() {
1001
-            if section.raw_relocs.is_empty() {
1002
-                continue;
1003
-            }
1004
-            let raws = parse_raw_relocs(&section.raw_relocs, 0, section.nreloc).map_err(|err| {
1005
-                WriteError::MalformedRelocations(
1006
-                    input.object.path.clone(),
1007
-                    (sect_idx + 1) as u8,
1008
-                    err.to_string(),
1009
-                )
1010
-            })?;
1011
-            let relocs = parse_relocs(&raws).map_err(|err| {
1012
-                WriteError::MalformedRelocations(
1013
-                    input.object.path.clone(),
1014
-                    (sect_idx + 1) as u8,
1015
-                    err.to_string(),
1016
-                )
1017
-            })?;
1018
-            reloc_cache.insert((input.id, (sect_idx + 1) as u8), relocs);
1019
-        }
1020
-    }
1243
+    let symbol_name_index = build_symbol_name_index(inputs.0.sym_table);
10211244
 
10221245
     for section in &layout.sections {
10231246
         if !matches!(section.segment.as_str(), "__DATA" | "__DATA_CONST") {
@@ -1035,12 +1258,14 @@ fn collect_rebase_sites(
10351258
             let Some(obj) = input_map.get(&atom.origin).copied() else {
10361259
                 continue;
10371260
             };
1038
-            let relocs = reloc_cache
1261
+            let relocs = inputs
1262
+                .0
1263
+                .parsed_relocs
10391264
                 .get(&(atom.origin, atom.input_section))
10401265
                 .map(Vec::as_slice)
10411266
                 .unwrap_or(&[]);
10421267
             for reloc in relocs_for_rebase(relocs, atom) {
1043
-                if !reloc_needs_rebase(obj, reloc, inputs.0.sym_table) {
1268
+                if !reloc_needs_rebase(obj, reloc, inputs.0.sym_table, &symbol_name_index) {
10441269
                     continue;
10451270
                 }
10461271
                 let local_offset = reloc.offset.saturating_sub(atom.input_offset) as u64;
@@ -1125,7 +1350,12 @@ fn relocs_for_rebase<'a>(
11251350
     })
11261351
 }
11271352
 
1128
-fn reloc_needs_rebase(obj: &ObjectFile, reloc: Reloc, sym_table: &SymbolTable) -> bool {
1353
+fn reloc_needs_rebase(
1354
+    obj: &ObjectFile,
1355
+    reloc: Reloc,
1356
+    sym_table: &SymbolTable,
1357
+    symbol_name_index: &HashMap<String, SymbolId>,
1358
+) -> bool {
11291359
     if reloc.kind != RelocKind::Unsigned
11301360
         || reloc.length != RelocLength::Quad
11311361
         || reloc.pcrel
@@ -1140,7 +1370,7 @@ fn reloc_needs_rebase(obj: &ObjectFile, reloc: Reloc, sym_table: &SymbolTable) -
11401370
             let Some(input_sym) = obj.symbols.get(sym_idx as usize) else {
11411371
                 return false;
11421372
             };
1143
-            match symbol_referent_id(obj, reloc.referent, sym_table) {
1373
+            match symbol_referent_id(obj, reloc.referent, symbol_name_index) {
11441374
                 Some(symbol_id) => match sym_table.get(symbol_id) {
11451375
                     Symbol::DylibImport { .. } => false,
11461376
                     Symbol::Defined { atom, .. } => atom.0 != 0,
@@ -1153,20 +1383,29 @@ fn reloc_needs_rebase(obj: &ObjectFile, reloc: Reloc, sym_table: &SymbolTable) -
11531383
     }
11541384
 }
11551385
 
1386
+fn build_symbol_name_index(sym_table: &SymbolTable) -> HashMap<String, SymbolId> {
1387
+    sym_table
1388
+        .iter()
1389
+        .map(|(symbol_id, symbol)| {
1390
+            (
1391
+                sym_table.interner.resolve(symbol.name()).to_string(),
1392
+                symbol_id,
1393
+            )
1394
+        })
1395
+        .collect()
1396
+}
1397
+
11561398
 fn symbol_referent_id(
11571399
     obj: &ObjectFile,
11581400
     referent: Referent,
1159
-    sym_table: &SymbolTable,
1401
+    symbol_name_index: &HashMap<String, SymbolId>,
11601402
 ) -> Option<SymbolId> {
11611403
     let Referent::Symbol(sym_idx) = referent else {
11621404
         return None;
11631405
     };
11641406
     let input_sym = obj.symbols.get(sym_idx as usize)?;
11651407
     let name = obj.symbol_name(input_sym).ok()?;
1166
-    let (symbol_id, _) = sym_table
1167
-        .iter()
1168
-        .find(|(_, symbol)| sym_table.interner.resolve(symbol.name()) == name)?;
1169
-    Some(symbol_id)
1408
+    symbol_name_index.get(name).copied()
11701409
 }
11711410
 
11721411
 fn build_function_starts(
@@ -1253,6 +1492,7 @@ fn build_data_in_code(
12531492
     layout: &Layout,
12541493
     inputs: &[LayoutInput<'_>],
12551494
     atom_table: &AtomTable,
1495
+    icf_redirects: Option<&HashMap<crate::resolve::AtomId, crate::resolve::AtomId>>,
12561496
 ) -> Result<Vec<u8>, WriteError> {
12571497
     #[derive(Clone, Copy)]
12581498
     struct RemappedEntry {
@@ -1264,14 +1504,14 @@ fn build_data_in_code(
12641504
     }
12651505
 
12661506
     let atoms_by_input_section = atom_table.by_input_section();
1507
+    let atom_ranges = build_atom_range_index(atom_table, &atoms_by_input_section, icf_redirects);
12671508
     let mut remapped = Vec::new();
12681509
     for (input_order, input) in inputs.iter().enumerate() {
12691510
         for (input_entry_index, entry) in input.object.data_in_code.iter().copied().enumerate() {
12701511
             let (section_index, section_relative) =
12711512
                 remap_data_in_code_to_section(input.object, entry)?;
12721513
             let (atom_id, atom_delta) = find_containing_atom_range(
1273
-                atom_table,
1274
-                &atoms_by_input_section,
1514
+                &atom_ranges,
12751515
                 input.id,
12761516
                 section_index,
12771517
                 section_relative,
@@ -1321,6 +1561,17 @@ fn build_data_in_code(
13211561
     Ok(out)
13221562
 }
13231563
 
1564
+fn build_loh(
1565
+    _layout: &Layout,
1566
+    _inputs: &[LayoutInput<'_>],
1567
+    _atom_table: &AtomTable,
1568
+    _icf_redirects: Option<&HashMap<crate::resolve::AtomId, crate::resolve::AtomId>>,
1569
+) -> Result<Vec<u8>, WriteError> {
1570
+    // Current Apple ld omits LC_LINKER_OPTIMIZATION_HINT from final linked
1571
+    // executables and dylibs on our parity corpus, so we do the same.
1572
+    Ok(Vec::new())
1573
+}
1574
+
13241575
 fn remap_data_in_code_to_section(
13251576
     object: &ObjectFile,
13261577
     entry: DataInCodeEntry,
@@ -1424,17 +1675,39 @@ fn collect_imports(
14241675
     Ok(out)
14251676
 }
14261677
 
1427
-fn build_output_symbols(
1678
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1679
+struct SymbolPlanBuildTimings {
1680
+    locals: Duration,
1681
+    globals: Duration,
1682
+    strtab: Duration,
1683
+}
1684
+
1685
+fn build_output_symbols_profiled(
14281686
     layout: &Layout,
14291687
     kind: OutputKind,
1688
+    dead_strip: bool,
14301689
     strip_locals: bool,
1690
+    visibility: &SymbolVisibilityPolicy,
14311691
     inputs: LinkEditInputs<'_>,
14321692
     imports: &[ImportSymbolRecord],
1433
-) -> Result<SymbolTablePlan, WriteError> {
1693
+) -> Result<(SymbolTablePlan, SymbolPlanBuildTimings), WriteError> {
14341694
     let sym_table = inputs.0.sym_table;
14351695
     let atom_sections = atom_section_ordinals(layout);
14361696
     let atoms_by_input_section = inputs.0.atom_table.by_input_section();
1697
+    let atom_ranges = build_atom_range_index(
1698
+        inputs.0.atom_table,
1699
+        &atoms_by_input_section,
1700
+        inputs.0.icf_redirects,
1701
+    );
1702
+    let file_index_by_input: HashMap<InputId, usize> = inputs
1703
+        .0
1704
+        .layout_inputs
1705
+        .iter()
1706
+        .enumerate()
1707
+        .map(|(idx, input)| (input.id, idx + 1))
1708
+        .collect();
14371709
     let image_base = layout.segment("__TEXT").map(|seg| seg.vm_addr).unwrap_or(0);
1710
+    let mut timings = SymbolPlanBuildTimings::default();
14381711
     let mut locals = Vec::new();
14391712
     let mut external_defineds = Vec::new();
14401713
     let mut undefineds = Vec::with_capacity(imports.len());
@@ -1444,34 +1717,50 @@ fn build_output_symbols(
14441717
             .segment("__TEXT")
14451718
             .ok_or(WriteError::MissingSegment("__TEXT"))?
14461719
             .vm_addr;
1447
-        external_defineds.push(OutputSymbolSpec {
1720
+        let hide_header = visibility.hides("__mh_execute_header");
1721
+        let header_partition = if hide_header {
1722
+            OutputSymbolPartition::Local
1723
+        } else {
1724
+            OutputSymbolPartition::ExternalDefined
1725
+        };
1726
+        let header_type = defined_symbol_type(hide_header);
1727
+        let target = if hide_header {
1728
+            &mut locals
1729
+        } else {
1730
+            &mut external_defineds
1731
+        };
1732
+        target.push(OutputSymbolSpec {
14481733
             symbol: None,
14491734
             name: "__mh_execute_header".to_string(),
1450
-            partition: OutputSymbolPartition::ExternalDefined,
1451
-            n_type: N_SECT | N_EXT,
1735
+            partition: header_partition,
1736
+            n_type: header_type,
14521737
             n_sect: 1,
14531738
             n_desc: REFERENCED_DYNAMICALLY,
14541739
             n_value: text_vmaddr,
1740
+            size: 0,
1741
+            file_index: 0,
14551742
         });
14561743
     }
14571744
 
1745
+    let phase_started = std::time::Instant::now();
14581746
     for input in inputs.0.layout_inputs {
1459
-        collect_local_symbols(
1460
-            layout,
1461
-            inputs.0.atom_table,
1462
-            &atoms_by_input_section,
1463
-            &atom_sections,
1464
-            input.id,
1465
-            input.object,
1466
-            &mut locals,
1467
-        )?;
1747
+        let ctx = LocalSymbolContext {
1748
+            atom_table: inputs.0.atom_table,
1749
+            atom_ranges: &atom_ranges,
1750
+            atom_sections: &atom_sections,
1751
+            input_id: input.id,
1752
+            file_index: file_index_by_input[&input.id],
1753
+        };
1754
+        collect_local_symbols(layout, &ctx, input.object, &mut locals)?;
14681755
     }
14691756
     collect_synthetic_local_symbols(layout, inputs.0.synthetic_plan, &mut locals)?;
1470
-    sort_local_symbols(&mut locals);
1757
+    timings.locals += phase_started.elapsed();
14711758
 
1759
+    let phase_started = std::time::Instant::now();
14721760
     for (symbol_id, symbol) in sym_table.iter() {
14731761
         let Symbol::Defined {
14741762
             name,
1763
+            origin,
14751764
             atom,
14761765
             value,
14771766
             weak,
@@ -1486,16 +1775,30 @@ fn build_output_symbols(
14861775
             continue;
14871776
         }
14881777
         let name = sym_table.interner.resolve(*name).to_string();
1778
+        let hidden = visibility.hides(&name);
14891779
         let (n_type, n_sect, n_value) = if atom.0 == 0 {
1490
-            (absolute_symbol_type(*private_extern), NO_SECT, *value)
1780
+            (absolute_symbol_type(hidden), NO_SECT, *value)
14911781
         } else {
1782
+            if dead_strip && layout.atom_addr(*atom).is_none() {
1783
+                continue;
1784
+            }
14921785
             let addr = layout
14931786
                 .atom_addr(*atom)
14941787
                 .ok_or(WriteError::DefinedSymbolAtomMissing(symbol_id, *atom))?;
14951788
             let sect = *atom_sections
14961789
                 .get(atom)
14971790
                 .ok_or(WriteError::DefinedSymbolSectionMissing(symbol_id, *atom))?;
1498
-            (defined_symbol_type(*private_extern), sect, addr + *value)
1791
+            (defined_symbol_type(hidden), sect, addr + *value)
1792
+        };
1793
+        let size = if atom.0 == 0 {
1794
+            0
1795
+        } else {
1796
+            inputs
1797
+                .0
1798
+                .atom_table
1799
+                .get(*atom)
1800
+                .size
1801
+                .saturating_sub(*value as u32) as u64
14991802
         };
15001803
         let mut n_desc = 0;
15011804
         if *weak {
@@ -1504,17 +1807,30 @@ fn build_output_symbols(
15041807
         if *no_dead_strip {
15051808
             n_desc |= N_NO_DEAD_STRIP;
15061809
         }
1507
-        external_defineds.push(OutputSymbolSpec {
1810
+        let partition = if hidden {
1811
+            OutputSymbolPartition::Local
1812
+        } else {
1813
+            OutputSymbolPartition::ExternalDefined
1814
+        };
1815
+        let target = if hidden {
1816
+            &mut locals
1817
+        } else {
1818
+            &mut external_defineds
1819
+        };
1820
+        target.push(OutputSymbolSpec {
15081821
             symbol: Some(symbol_id),
15091822
             name,
1510
-            partition: OutputSymbolPartition::ExternalDefined,
1823
+            partition,
15111824
             n_type,
15121825
             n_sect,
15131826
             n_desc,
15141827
             n_value,
1828
+            size,
1829
+            file_index: file_index_by_input.get(origin).copied().unwrap_or(0),
15151830
         });
15161831
     }
15171832
 
1833
+    sort_local_symbols(&mut locals);
15181834
     external_defineds.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
15191835
     for import in imports {
15201836
         let mut n_desc = import.ordinal << 8;
@@ -1529,9 +1845,12 @@ fn build_output_symbols(
15291845
             n_sect: NO_SECT,
15301846
             n_desc,
15311847
             n_value: 0,
1848
+            size: 0,
1849
+            file_index: 0,
15321850
         });
15331851
     }
15341852
     undefineds.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
1853
+    timings.globals += phase_started.elapsed();
15351854
 
15361855
     let exports = if matches!(kind, OutputKind::Dylib | OutputKind::Executable) {
15371856
         external_defineds
@@ -1552,6 +1871,7 @@ fn build_output_symbols(
15521871
         Vec::new()
15531872
     };
15541873
 
1874
+    let phase_started = std::time::Instant::now();
15551875
     let local_count = if strip_locals { 0 } else { locals.len() };
15561876
     let mut specs = Vec::with_capacity(local_count + external_defineds.len() + undefineds.len());
15571877
     if !strip_locals {
@@ -1581,6 +1901,16 @@ fn build_output_symbols(
15811901
 
15821902
     let mut symbols = Vec::with_capacity(specs.len());
15831903
     let mut symbol_indices = HashMap::new();
1904
+    let map_symbols = specs
1905
+        .iter()
1906
+        .filter(|spec| spec.partition != OutputSymbolPartition::Undefined)
1907
+        .map(|spec| LinkMapSymbol {
1908
+            name: spec.name.clone(),
1909
+            addr: spec.n_value,
1910
+            size: spec.size,
1911
+            file_index: spec.file_index,
1912
+        })
1913
+        .collect();
15841914
     for (idx, spec) in specs.into_iter().enumerate() {
15851915
         let strx = *strx_by_name
15861916
             .get(&spec.name)
@@ -1596,22 +1926,27 @@ fn build_output_symbols(
15961926
             symbol_indices.insert(symbol, idx as u32);
15971927
         }
15981928
     }
1599
-
1600
-    Ok(SymbolTablePlan {
1601
-        symbols,
1602
-        strtab_bytes,
1603
-        symbol_indices,
1604
-        exports,
1605
-        dysymtab: DysymtabCmd {
1606
-            ilocalsym: 0,
1607
-            nlocalsym,
1608
-            iextdefsym: nlocalsym,
1609
-            nextdefsym,
1610
-            iundefsym: nlocalsym + nextdefsym,
1611
-            nundefsym,
1612
-            ..DysymtabCmd::default()
1929
+    timings.strtab += phase_started.elapsed();
1930
+
1931
+    Ok((
1932
+        SymbolTablePlan {
1933
+            symbols,
1934
+            map_symbols,
1935
+            strtab_bytes,
1936
+            symbol_indices,
1937
+            exports,
1938
+            dysymtab: DysymtabCmd {
1939
+                ilocalsym: 0,
1940
+                nlocalsym,
1941
+                iextdefsym: nlocalsym,
1942
+                nextdefsym,
1943
+                iundefsym: nlocalsym + nextdefsym,
1944
+                nundefsym,
1945
+                ..DysymtabCmd::default()
1946
+            },
16131947
         },
1614
-    })
1948
+        timings,
1949
+    ))
16151950
 }
16161951
 
16171952
 fn sort_local_symbols(locals: &mut [OutputSymbolSpec]) {
@@ -1650,16 +1985,15 @@ fn collect_synthetic_local_symbols(
16501985
         n_sect: u8::try_from(section_index + 1).expect("section index should fit in n_sect"),
16511986
         n_desc: 0,
16521987
         n_value: section.addr + section.synthetic_offset,
1988
+        size: 8,
1989
+        file_index: 0,
16531990
     });
16541991
     Ok(())
16551992
 }
16561993
 
16571994
 fn collect_local_symbols(
16581995
     layout: &Layout,
1659
-    atom_table: &AtomTable,
1660
-    atoms_by_input_section: &HashMap<(InputId, u8), Vec<crate::resolve::AtomId>>,
1661
-    atom_sections: &HashMap<crate::resolve::AtomId, u8>,
1662
-    input_id: InputId,
1996
+    ctx: &LocalSymbolContext<'_>,
16631997
     object: &ObjectFile,
16641998
     out: &mut Vec<OutputSymbolSpec>,
16651999
 ) -> Result<(), WriteError> {
@@ -1681,9 +2015,8 @@ fn collect_local_symbols(
16812015
                     .expect("section symbol without section");
16822016
                 let offset = input_sym.value().saturating_sub(section.addr) as u32;
16832017
                 let (atom_id, delta) = find_containing_atom(
1684
-                    atom_table,
1685
-                    atoms_by_input_section,
1686
-                    input_id,
2018
+                    ctx.atom_ranges,
2019
+                    ctx.input_id,
16872020
                     input_sym.sect_idx(),
16882021
                     offset,
16892022
                 )
@@ -1696,13 +2029,9 @@ fn collect_local_symbols(
16962029
                             atom_id,
16972030
                         ))?
16982031
                         + delta as u64;
1699
-                let n_sect =
1700
-                    *atom_sections
1701
-                        .get(&atom_id)
1702
-                        .ok_or(WriteError::DefinedSymbolSectionMissing(
1703
-                            SymbolId(u32::MAX),
1704
-                            atom_id,
1705
-                        ))?;
2032
+                let n_sect = *ctx.atom_sections.get(&atom_id).ok_or(
2033
+                    WriteError::DefinedSymbolSectionMissing(SymbolId(u32::MAX), atom_id),
2034
+                )?;
17062035
                 out.push(OutputSymbolSpec {
17072036
                     symbol: None,
17082037
                     name,
@@ -1711,6 +2040,8 @@ fn collect_local_symbols(
17112040
                     n_sect,
17122041
                     n_desc: input_sym.raw.n_desc,
17132042
                     n_value: addr,
2043
+                    size: ctx.atom_table.get(atom_id).size.saturating_sub(delta) as u64,
2044
+                    file_index: ctx.file_index,
17142045
                 });
17152046
             }
17162047
             SymKind::Abs => {
@@ -1722,6 +2053,8 @@ fn collect_local_symbols(
17222053
                     n_sect: NO_SECT,
17232054
                     n_desc: input_sym.raw.n_desc,
17242055
                     n_value: input_sym.value(),
2056
+                    size: 0,
2057
+                    file_index: ctx.file_index,
17252058
                 });
17262059
             }
17272060
             SymKind::Undef | SymKind::Indirect => {}
@@ -1730,46 +2063,91 @@ fn collect_local_symbols(
17302063
     Ok(())
17312064
 }
17322065
 
2066
+struct LocalSymbolContext<'a> {
2067
+    atom_table: &'a AtomTable,
2068
+    atom_ranges: &'a AtomRangeIndex,
2069
+    atom_sections: &'a HashMap<crate::resolve::AtomId, u8>,
2070
+    input_id: InputId,
2071
+    file_index: usize,
2072
+}
2073
+
2074
+#[derive(Debug, Clone, Copy)]
2075
+struct AtomRange {
2076
+    atom: crate::resolve::AtomId,
2077
+    start: u32,
2078
+    end: u32,
2079
+}
2080
+
2081
+type AtomRangeIndex = HashMap<(InputId, u8), Vec<AtomRange>>;
2082
+
17332083
 fn is_assembler_temporary_symbol(name: &str) -> bool {
17342084
     name.starts_with('L') || name.starts_with("ltmp")
17352085
 }
17362086
 
1737
-fn find_containing_atom(
2087
+fn build_atom_range_index(
17382088
     atom_table: &AtomTable,
17392089
     atoms_by_input_section: &HashMap<(InputId, u8), Vec<crate::resolve::AtomId>>,
2090
+    icf_redirects: Option<&HashMap<crate::resolve::AtomId, crate::resolve::AtomId>>,
2091
+) -> AtomRangeIndex {
2092
+    let mut out = HashMap::with_capacity(atoms_by_input_section.len());
2093
+    for (&key, ids) in atoms_by_input_section {
2094
+        let mut ranges = Vec::with_capacity(ids.len());
2095
+        for atom_id in ids {
2096
+            let atom = atom_table.get(*atom_id);
2097
+            ranges.push(AtomRange {
2098
+                atom: canonical_atom(*atom_id, icf_redirects),
2099
+                start: atom.input_offset,
2100
+                end: atom.input_offset.saturating_add(atom.size),
2101
+            });
2102
+        }
2103
+        ranges.sort_by(|lhs, rhs| {
2104
+            lhs.start
2105
+                .cmp(&rhs.start)
2106
+                .then_with(|| lhs.end.cmp(&rhs.end))
2107
+        });
2108
+        out.insert(key, ranges);
2109
+    }
2110
+    out
2111
+}
2112
+
2113
+fn find_containing_atom(
2114
+    atom_ranges: &AtomRangeIndex,
17402115
     input_id: InputId,
17412116
     input_section: u8,
17422117
     offset: u32,
17432118
 ) -> Option<(crate::resolve::AtomId, u32)> {
1744
-    find_containing_atom_range(
1745
-        atom_table,
1746
-        atoms_by_input_section,
1747
-        input_id,
1748
-        input_section,
1749
-        offset,
1750
-        1,
1751
-    )
2119
+    find_containing_atom_range(atom_ranges, input_id, input_section, offset, 1)
17522120
 }
17532121
 
17542122
 fn find_containing_atom_range(
1755
-    atom_table: &AtomTable,
1756
-    atoms_by_input_section: &HashMap<(InputId, u8), Vec<crate::resolve::AtomId>>,
2123
+    atom_ranges: &AtomRangeIndex,
17572124
     input_id: InputId,
17582125
     input_section: u8,
17592126
     offset: u32,
17602127
     len: u32,
17612128
 ) -> Option<(crate::resolve::AtomId, u32)> {
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
-            })
1772
-        })
2129
+    let ranges = atom_ranges.get(&(input_id, input_section))?;
2130
+    let range_end = offset.checked_add(len)?;
2131
+    let idx = ranges.partition_point(|range| range.start <= offset);
2132
+    let range = idx.checked_sub(1).and_then(|idx| ranges.get(idx))?;
2133
+    (range.start <= offset && range_end <= range.end).then_some((range.atom, offset - range.start))
2134
+}
2135
+
2136
+fn canonical_atom(
2137
+    atom_id: crate::resolve::AtomId,
2138
+    redirects: Option<&HashMap<crate::resolve::AtomId, crate::resolve::AtomId>>,
2139
+) -> crate::resolve::AtomId {
2140
+    let Some(redirects) = redirects else {
2141
+        return atom_id;
2142
+    };
2143
+    let mut current = atom_id;
2144
+    while let Some(&next) = redirects.get(&current) {
2145
+        if next == current {
2146
+            break;
2147
+        }
2148
+        current = next;
2149
+    }
2150
+    current
17732151
 }
17742152
 
17752153
 fn input_symbol_type(input_sym: &InputSymbol) -> u8 {
@@ -1848,19 +2226,61 @@ fn section_is_thread_local(layout: &Layout, n_sect: u8) -> bool {
18482226
 }
18492227
 
18502228
 fn defined_symbol_type(private_extern: bool) -> u8 {
1851
-    let mut n_type = N_SECT | N_EXT;
18522229
     if private_extern {
1853
-        n_type |= N_PEXT;
2230
+        N_SECT | N_PEXT
2231
+    } else {
2232
+        N_SECT | N_EXT
18542233
     }
1855
-    n_type
18562234
 }
18572235
 
18582236
 fn absolute_symbol_type(private_extern: bool) -> u8 {
1859
-    let mut n_type = N_ABS | N_EXT;
18602237
     if private_extern {
1861
-        n_type |= N_PEXT;
2238
+        N_ABS | N_PEXT
2239
+    } else {
2240
+        N_ABS | N_EXT
18622241
     }
1863
-    n_type
2242
+}
2243
+
2244
+fn read_symbol_patterns(path: &PathBuf) -> Result<Vec<String>, WriteError> {
2245
+    let contents = fs::read_to_string(path)
2246
+        .map_err(|err| WriteError::SymbolListRead(path.clone(), err.to_string()))?;
2247
+    Ok(contents
2248
+        .lines()
2249
+        .map(str::trim)
2250
+        .filter(|line| !line.is_empty())
2251
+        .map(ToString::to_string)
2252
+        .collect())
2253
+}
2254
+
2255
+fn wildcard_matches(pattern: &str, value: &str) -> bool {
2256
+    let pattern = pattern.as_bytes();
2257
+    let value = value.as_bytes();
2258
+    let mut p = 0usize;
2259
+    let mut v = 0usize;
2260
+    let mut star = None;
2261
+    let mut backtrack = 0usize;
2262
+
2263
+    while v < value.len() {
2264
+        if p < pattern.len() && (pattern[p] == b'?' || pattern[p] == value[v]) {
2265
+            p += 1;
2266
+            v += 1;
2267
+        } else if p < pattern.len() && pattern[p] == b'*' {
2268
+            star = Some(p);
2269
+            p += 1;
2270
+            backtrack = v;
2271
+        } else if let Some(star_idx) = star {
2272
+            p = star_idx + 1;
2273
+            backtrack += 1;
2274
+            v = backtrack;
2275
+        } else {
2276
+            return false;
2277
+        }
2278
+    }
2279
+
2280
+    while p < pattern.len() && pattern[p] == b'*' {
2281
+        p += 1;
2282
+    }
2283
+    p == pattern.len()
18642284
 }
18652285
 
18662286
 fn place_optional_block(
@@ -1899,6 +2319,17 @@ fn place_linkedit_data_block(
18992319
     })
19002320
 }
19012321
 
2322
+fn place_optional_linkedit_data_block(
2323
+    cursor: &mut u64,
2324
+    size: usize,
2325
+    context: &'static str,
2326
+) -> Result<Option<LinkEditDataCmd>, WriteError> {
2327
+    if size == 0 {
2328
+        return Ok(None);
2329
+    }
2330
+    Ok(Some(place_linkedit_data_block(cursor, size, context)?))
2331
+}
2332
+
19022333
 fn push_indirect_section(
19032334
     indirect_symbols: &mut Vec<u32>,
19042335
     indirect_starts: &mut HashMap<(String, String), u32>,
@@ -2463,12 +2894,13 @@ mod tests {
24632894
         });
24642895
 
24652896
         let by_input_section = atoms.by_input_section();
2897
+        let atom_ranges = build_atom_range_index(&atoms, &by_input_section, None);
24662898
         assert_eq!(
2467
-            find_containing_atom(&atoms, &by_input_section, InputId(7), 3, 4),
2899
+            find_containing_atom(&atom_ranges, InputId(7), 3, 4),
24682900
             Some((first, 4))
24692901
         );
24702902
         assert_eq!(
2471
-            find_containing_atom_range(&atoms, &by_input_section, InputId(7), 3, 10, 2),
2903
+            find_containing_atom_range(&atom_ranges, InputId(7), 3, 10, 2),
24722904
             Some((second, 2))
24732905
         );
24742906
     }
src/main.rsmodified
76 lines changed — click to load
@@ -2,6 +2,60 @@ use std::process::ExitCode;
22
 
33
 use afs_ld::{args, diag, dump, LinkError, Linker};
44
 
5
+fn usage() -> &'static str {
6
+    "\
7
+Usage: afs-ld [options] <inputs...>
8
+
9
+Options:
10
+  -o <path>                       Write output to <path>
11
+  -dylib                          Emit a dylib instead of an executable
12
+  -e <symbol>                     Set the entry symbol
13
+  -arch arm64                     Select the arm64 target
14
+  -map <path>                     Emit text link map
15
+  -why_live <symbol>              Print a reachability chain for <symbol>
16
+  -l<name> / -l <name>            Search for library
17
+  -L <dir>                        Add library search path
18
+  -framework <name>               Link framework
19
+  -weak_framework <name>          Link weak framework
20
+  -ObjC                           Objective-C archive loading mode (currently a no-op warning)
21
+  -syslibroot <path>              Prefix SDK search roots
22
+  -platform_version macos <min> <sdk>
23
+                                  Set LC_BUILD_VERSION payload
24
+  -r                              Relocatable output (deferred; errors)
25
+  -bundle                         Bundle output (deferred; errors)
26
+  -undefined <error|warning|suppress|dynamic_lookup>
27
+                                  Control unresolved-symbol treatment
28
+  -rpath <path>                   Add LC_RPATH
29
+  -install_name <path>            Override dylib install name
30
+  -current_version <v>            Override dylib current version
31
+  -compatibility_version <v>      Override dylib compatibility version
32
+  -exported_symbols_list <file>   Export only symbols matching file patterns
33
+  -unexported_symbols_list <file> Hide symbols matching file patterns
34
+  -exported_symbol <sym>          Export one symbol/pattern
35
+  -unexported_symbol <sym>        Hide one symbol/pattern
36
+  -x                              Strip local symbols
37
+  -S                              Strip debug symbols (currently a no-op warning)
38
+  -no_uuid                        Omit LC_UUID
39
+  -no_loh                         Accepted for compatibility (currently warns; no effect)
40
+  -thunks=<none|safe|all>         Configure branch thunks
41
+  -dead_strip                     Dead-strip unreferenced code/data
42
+  -icf=safe | -icf=none | -icf=all
43
+                                  Configure identical code folding (`all` currently errors)
44
+  -fixup_chains | -no_fixup_chains
45
+                                  Select chained fixups vs classic dyld info
46
+  -all_load                       Force-load every archive member
47
+  -force_load <archive>           Force-load one archive
48
+  -Wl,<arg,arg,...>               Normalize comma-separated driver flags
49
+  --dump <path>                   Dump a Mach-O file summary
50
+  --dump-archive <path>           Dump an archive summary
51
+  --dump-dylib <path>             Dump a dylib summary
52
+  --dump-tbd <path>               Dump a TBD summary
53
+  -t, -trace                      Print input paths as they are loaded
54
+  -h, --help                      Show this help
55
+  -v, --version                   Show afs-ld version
56
+"
57
+}
58
+
559
 fn main() -> ExitCode {
660
     let argv: Vec<String> = std::env::args().collect();
761
 
@@ -13,6 +67,16 @@ fn main() -> ExitCode {
1367
         }
1468
     };
1569
 
70
+    if opts.show_help {
71
+        print!("{}", usage());
72
+        return ExitCode::SUCCESS;
73
+    }
74
+
75
+    if opts.show_version {
76
+        println!("afs-ld {}", env!("CARGO_PKG_VERSION"));
77
+        return ExitCode::SUCCESS;
78
+    }
79
+
1680
     if let Some(path) = &opts.dump {
1781
         return match dump::dump_file(path) {
1882
             Ok(()) => ExitCode::SUCCESS,
src/reloc/arm64.rsmodified
1267 lines changed — click to load
@@ -2,16 +2,18 @@ use std::collections::HashMap;
22
 use std::fmt;
33
 use std::path::PathBuf;
44
 
5
-use crate::atom::{Atom, AtomTable};
5
+use crate::atom::{Atom, AtomSection, AtomTable};
66
 use crate::input::ObjectFile;
7
-use crate::layout::{Layout, LayoutInput};
7
+use crate::layout::{ExtraOutputSection, ExtraSectionAnchor, Layout, LayoutInput};
88
 use crate::macho::writer::LinkEditPlan;
99
 use crate::reloc::{parse_raw_relocs, parse_relocs, Referent, Reloc, RelocKind, RelocLength};
1010
 use crate::resolve::{InputId, Symbol, SymbolId, SymbolTable};
11
+use crate::section::{OutputSection, SectionKind};
1112
 use crate::symbol::{InputSymbol, SymKind};
1213
 use crate::synth::stubs::{STUB_HELPER_ENTRY_SIZE, STUB_HELPER_HEADER_SIZE, STUB_SIZE};
1314
 use crate::synth::tlv::THREAD_VARIABLE_DESCRIPTOR_SIZE;
1415
 use crate::synth::SyntheticPlan;
16
+use crate::{LinkOptions, ThunkMode};
1517
 
1618
 #[derive(Debug, Clone, PartialEq, Eq)]
1719
 pub struct RelocError {
@@ -42,6 +44,7 @@ impl std::error::Error for RelocError {}
4244
 
4345
 struct ResolveView<'a> {
4446
     sym_table: &'a SymbolTable,
47
+    symbol_name_index: &'a HashMap<String, SymbolId>,
4548
     atom_table: &'a AtomTable,
4649
     atom_addrs: &'a HashMap<crate::resolve::AtomId, u64>,
4750
     atoms_by_input_section: &'a HashMap<(InputId, u8), Vec<crate::resolve::AtomId>>,
@@ -52,6 +55,7 @@ struct ResolveView<'a> {
5255
     stub_helper_entry_addrs: &'a HashMap<SymbolId, u64>,
5356
     stub_helper_header_addr: Option<u64>,
5457
     dyld_private_addr: Option<u64>,
58
+    icf_redirects: Option<&'a HashMap<crate::resolve::AtomId, crate::resolve::AtomId>>,
5559
 }
5660
 
5761
 struct SyntheticAddressMaps {
@@ -63,13 +67,140 @@ struct SyntheticAddressMaps {
6367
     dyld_private_addr: Option<u64>,
6468
 }
6569
 
70
+pub struct ApplyLayoutPlan<'a> {
71
+    pub synthetic_plan: Option<&'a SyntheticPlan>,
72
+    pub thunk_plan: Option<&'a ThunkPlan>,
73
+    pub linkedit: &'a LinkEditPlan,
74
+    pub icf_redirects: Option<&'a HashMap<crate::resolve::AtomId, crate::resolve::AtomId>>,
75
+}
76
+
77
+struct InputSectionResolveCtx<'a> {
78
+    obj: &'a ObjectFile,
79
+    atom: &'a Atom,
80
+    kind: RelocKind,
81
+    referent: &'a str,
82
+}
83
+
84
+const THUNK_SIZE: u64 = 12;
85
+const BR_X16: u32 = 0xd61f_0200;
86
+
87
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
88
+enum BranchTargetKey {
89
+    Symbol(SymbolId),
90
+    Stub(SymbolId),
91
+    InputSectionOffset {
92
+        origin: InputId,
93
+        input_section: u8,
94
+        input_offset: u32,
95
+    },
96
+}
97
+
98
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
99
+struct ThunkBucketKey {
100
+    island: usize,
101
+    target: BranchTargetKey,
102
+}
103
+
104
+#[derive(Debug, Clone, PartialEq, Eq)]
105
+struct ThunkIsland {
106
+    segment: String,
107
+    after_atom: crate::resolve::AtomId,
108
+}
109
+
110
+#[derive(Debug, Clone, PartialEq, Eq)]
111
+struct ThunkEntry {
112
+    island: usize,
113
+    slot_in_island: usize,
114
+    target: BranchTargetKey,
115
+}
116
+
117
+#[derive(Debug, Clone, PartialEq, Eq, Default)]
118
+pub struct ThunkPlan {
119
+    redirects: HashMap<(crate::resolve::AtomId, u32), usize>,
120
+    islands: Vec<ThunkIsland>,
121
+    entries: Vec<ThunkEntry>,
122
+}
123
+
124
+impl ThunkPlan {
125
+    pub fn split_after_atoms(&self) -> Vec<crate::resolve::AtomId> {
126
+        self.islands
127
+            .iter()
128
+            .map(|island| island.after_atom)
129
+            .collect()
130
+    }
131
+
132
+    pub fn output_sections(&self) -> Vec<ExtraOutputSection> {
133
+        if self.entries.is_empty() {
134
+            return Vec::new();
135
+        }
136
+        let mut counts = vec![0usize; self.islands.len()];
137
+        for entry in &self.entries {
138
+            counts[entry.island] += 1;
139
+        }
140
+        let mut sections = Vec::new();
141
+        for (island, island_desc) in self.islands.iter().enumerate() {
142
+            let count = counts[island];
143
+            if count == 0 {
144
+                continue;
145
+            }
146
+            sections.push(ExtraOutputSection {
147
+                after_section: Some(ExtraSectionAnchor::AfterAtom(island_desc.after_atom)),
148
+                section: OutputSection {
149
+                    segment: island_desc.segment.clone(),
150
+                    name: "__thunks".into(),
151
+                    kind: SectionKind::Text,
152
+                    align_pow2: 2,
153
+                    flags: crate::macho::constants::S_REGULAR
154
+                        | crate::macho::constants::S_ATTR_PURE_INSTRUCTIONS
155
+                        | crate::macho::constants::S_ATTR_SOME_INSTRUCTIONS,
156
+                    reserved1: 0,
157
+                    reserved2: 0,
158
+                    reserved3: 0,
159
+                    atoms: Vec::new(),
160
+                    synthetic_offset: 0,
161
+                    synthetic_data: vec![0; count * THUNK_SIZE as usize],
162
+                    addr: 0,
163
+                    size: (count as u64) * THUNK_SIZE,
164
+                    file_off: 0,
165
+                },
166
+            });
167
+        }
168
+        sections
169
+    }
170
+
171
+    fn redirect_for(&self, atom: crate::resolve::AtomId, atom_offset: u32) -> Option<usize> {
172
+        self.redirects.get(&(atom, atom_offset)).copied()
173
+    }
174
+
175
+    fn thunk_addrs(&self, layout: &Layout) -> HashMap<usize, u64> {
176
+        let bases: HashMap<_, _> = self
177
+            .islands
178
+            .iter()
179
+            .enumerate()
180
+            .filter_map(|(island_idx, island)| {
181
+                find_thunk_section_index(layout, island)
182
+                    .map(|section_idx| (island_idx, layout.sections[section_idx].addr))
183
+            })
184
+            .collect();
185
+        self.entries
186
+            .iter()
187
+            .enumerate()
188
+            .filter_map(|(index, entry)| {
189
+                bases
190
+                    .get(&entry.island)
191
+                    .copied()
192
+                    .map(|base| (index, base + (entry.slot_in_island as u64) * THUNK_SIZE))
193
+            })
194
+            .collect()
195
+    }
196
+}
197
+
66198
 pub fn apply_layout(
67199
     layout: &mut Layout,
68200
     inputs: &[LayoutInput<'_>],
69201
     atoms: &AtomTable,
70202
     sym_table: &SymbolTable,
71
-    synthetic_plan: Option<&SyntheticPlan>,
72
-    linkedit: &LinkEditPlan,
203
+    plan: ApplyLayoutPlan<'_>,
73204
 ) -> Result<(), RelocError> {
74205
     let input_map: HashMap<InputId, &ObjectFile> = inputs
75206
         .iter()
@@ -108,9 +239,11 @@ pub fn apply_layout(
108239
     let atom_addrs = atom_address_map(layout);
109240
     let atoms_by_input_section = atoms.by_input_section();
110241
     let section_addrs = input_section_address_map(layout, atoms);
111
-    let synth_addrs = synthetic_address_maps(layout, synthetic_plan);
242
+    let synth_addrs = synthetic_address_maps(layout, plan.synthetic_plan);
243
+    let symbol_name_index = build_symbol_name_index(sym_table);
112244
     let resolve = ResolveView {
113245
         sym_table,
246
+        symbol_name_index: &symbol_name_index,
114247
         atom_table: atoms,
115248
         atom_addrs: &atom_addrs,
116249
         atoms_by_input_section: &atoms_by_input_section,
@@ -121,7 +254,11 @@ pub fn apply_layout(
121254
         stub_helper_entry_addrs: &synth_addrs.stub_helper_entry_addrs,
122255
         stub_helper_header_addr: synth_addrs.stub_helper_header_addr,
123256
         dyld_private_addr: synth_addrs.dyld_private_addr,
257
+        icf_redirects: plan.icf_redirects,
124258
     };
259
+    let thunk_addrs = plan
260
+        .thunk_plan
261
+        .map(|thunk_plan| thunk_plan.thunk_addrs(layout));
125262
 
126263
     for out_section in &mut layout.sections {
127264
         for placed in &mut out_section.atoms {
@@ -139,34 +276,113 @@ pub fn apply_layout(
139276
                     "missing parsed object".to_string(),
140277
                 )
141278
             })?;
279
+            patch_eh_frame_cie_pointer(&mut placed.data, atom, &resolve)?;
142280
             let relocs = reloc_cache
143281
                 .get(&(atom.origin, atom.input_section))
144282
                 .map(Vec::as_slice)
145283
                 .unwrap_or(&[]);
146284
             for reloc in relocs_for_atom(relocs, atom) {
147
-                apply_one(&mut placed.data, atom, obj, reloc, &resolve)?;
285
+                apply_one(
286
+                    &mut placed.data,
287
+                    atom,
288
+                    obj,
289
+                    reloc,
290
+                    &resolve,
291
+                    plan.thunk_plan,
292
+                    thunk_addrs.as_ref(),
293
+                )?;
148294
             }
149295
         }
150296
     }
151297
 
152
-    if let Some(plan) = synthetic_plan {
298
+    if let Some(thunk_plan) = plan.thunk_plan {
299
+        synthesize_thunk_section(layout, thunk_plan, &resolve)?;
300
+    }
301
+
302
+    if let Some(synthetic_plan) = plan.synthetic_plan {
153303
         synthesize_thread_variable_section(
154304
             layout,
155
-            plan,
305
+            synthetic_plan,
156306
             atoms,
157307
             &input_map,
158308
             &reloc_cache,
159309
             &resolve,
160310
         )?;
161
-        synthesize_got_section(layout, plan, &resolve)?;
162
-        synthesize_stub_section(layout, plan, &resolve)?;
163
-        synthesize_lazy_pointer_section(layout, plan, &resolve)?;
164
-        synthesize_stub_helper_section(layout, plan, &resolve, linkedit)?;
311
+        synthesize_got_section(layout, synthetic_plan, &resolve)?;
312
+        synthesize_stub_section(layout, synthetic_plan, &resolve)?;
313
+        synthesize_lazy_pointer_section(layout, synthetic_plan, &resolve)?;
314
+        synthesize_stub_helper_section(layout, synthetic_plan, &resolve, plan.linkedit)?;
165315
     }
166316
 
167317
     Ok(())
168318
 }
169319
 
320
+fn patch_eh_frame_cie_pointer(
321
+    bytes: &mut [u8],
322
+    atom: &Atom,
323
+    resolve: &ResolveView<'_>,
324
+) -> Result<(), RelocError> {
325
+    if atom.section != AtomSection::EhFrame || bytes.len() < 8 {
326
+        return Ok(());
327
+    }
328
+    let mut buf = [0u8; 4];
329
+    buf.copy_from_slice(&bytes[4..8]);
330
+    let cie_delta = u32::from_le_bytes(buf);
331
+    if cie_delta == 0 {
332
+        return Ok(());
333
+    }
334
+
335
+    let cie_offset = atom
336
+        .input_offset
337
+        .checked_add(4)
338
+        .and_then(|value| value.checked_sub(cie_delta))
339
+        .ok_or_else(|| {
340
+            reloc_error(
341
+                atom,
342
+                &PathBuf::from("<eh_frame>"),
343
+                4,
344
+                RelocKind::Unsigned,
345
+                "__eh_frame CIE pointer",
346
+                "invalid CIE back-pointer".to_string(),
347
+            )
348
+        })?;
349
+    let cie_atom = resolve
350
+        .atoms_by_input_section
351
+        .get(&(atom.origin, atom.input_section))
352
+        .and_then(|atom_ids| {
353
+            atom_ids.iter().find_map(|atom_id| {
354
+                let candidate = resolve.atom_table.get(*atom_id);
355
+                let start = candidate.input_offset;
356
+                let end = candidate.input_offset.saturating_add(candidate.size);
357
+                (start <= cie_offset && cie_offset < end).then_some(*atom_id)
358
+            })
359
+        })
360
+        .and_then(|atom_id| resolve.atom_addrs.get(&atom_id).copied())
361
+        .ok_or_else(|| {
362
+            reloc_error(
363
+                atom,
364
+                &PathBuf::from("<eh_frame>"),
365
+                4,
366
+                RelocKind::Unsigned,
367
+                "__eh_frame CIE pointer",
368
+                "eh_frame CIE atom is missing from the final layout".to_string(),
369
+            )
370
+        })?;
371
+    let fde_field = resolve.atom_addrs.get(&atom.id).copied().ok_or_else(|| {
372
+        reloc_error(
373
+            atom,
374
+            &PathBuf::from("<eh_frame>"),
375
+            4,
376
+            RelocKind::Unsigned,
377
+            "__eh_frame CIE pointer",
378
+            "eh_frame atom is missing a final address".to_string(),
379
+        )
380
+    })? + 4;
381
+    let rewritten = fde_field.wrapping_sub(cie_atom) as u32;
382
+    bytes[4..8].copy_from_slice(&rewritten.to_le_bytes());
383
+    Ok(())
384
+}
385
+
170386
 fn relocs_for_atom<'a>(relocs: &'a [Reloc], atom: &Atom) -> impl Iterator<Item = Reloc> + 'a {
171387
     let start = atom.input_offset;
172388
     let end = atom.input_offset + atom.size;
@@ -198,6 +414,16 @@ fn input_section_address_map(layout: &Layout, atoms: &AtomTable) -> HashMap<(Inp
198414
     out
199415
 }
200416
 
417
+fn atom_output_segment_map(layout: &Layout) -> HashMap<crate::resolve::AtomId, String> {
418
+    let mut out = HashMap::new();
419
+    for section in &layout.sections {
420
+        for placed in &section.atoms {
421
+            out.insert(placed.atom, section.segment.clone());
422
+        }
423
+    }
424
+    out
425
+}
426
+
201427
 fn synthetic_address_maps(
202428
     layout: &Layout,
203429
     synthetic_plan: Option<&SyntheticPlan>,
@@ -284,12 +510,164 @@ fn synthetic_address_maps(
284510
     }
285511
 }
286512
 
513
+pub fn plan_thunks(
514
+    opts: &LinkOptions,
515
+    layout: &Layout,
516
+    inputs: &[LayoutInput<'_>],
517
+    atoms: &AtomTable,
518
+    sym_table: &SymbolTable,
519
+    synthetic_plan: Option<&SyntheticPlan>,
520
+    icf_redirects: Option<&HashMap<crate::resolve::AtomId, crate::resolve::AtomId>>,
521
+) -> Result<Option<ThunkPlan>, RelocError> {
522
+    if opts.thunks == ThunkMode::None {
523
+        return Ok(None);
524
+    }
525
+
526
+    let input_map: HashMap<InputId, &ObjectFile> = inputs
527
+        .iter()
528
+        .map(|input| (input.id, input.object))
529
+        .collect();
530
+    let mut reloc_cache: HashMap<(InputId, u8), Vec<Reloc>> = HashMap::new();
531
+    for input in inputs {
532
+        for (sect_idx, section) in input.object.sections.iter().enumerate() {
533
+            let relocs = if section.nreloc == 0 {
534
+                Vec::new()
535
+            } else {
536
+                let raws =
537
+                    parse_raw_relocs(&section.raw_relocs, 0, section.nreloc).map_err(|err| {
538
+                        RelocError {
539
+                            input: input.object.path.clone(),
540
+                            atom: crate::resolve::AtomId(0),
541
+                            atom_offset: 0,
542
+                            kind: RelocKind::Unsigned,
543
+                            referent: format!("section {},{}", section.segname, section.sectname),
544
+                            detail: err.to_string(),
545
+                        }
546
+                    })?;
547
+                parse_relocs(&raws).map_err(|err| RelocError {
548
+                    input: input.object.path.clone(),
549
+                    atom: crate::resolve::AtomId(0),
550
+                    atom_offset: 0,
551
+                    kind: RelocKind::Unsigned,
552
+                    referent: format!("section {},{}", section.segname, section.sectname),
553
+                    detail: err.to_string(),
554
+                })?
555
+            };
556
+            reloc_cache.insert((input.id, (sect_idx + 1) as u8), relocs);
557
+        }
558
+    }
559
+
560
+    let atom_addrs = atom_address_map(layout);
561
+    let atom_segments = atom_output_segment_map(layout);
562
+    let atoms_by_input_section = atoms.by_input_section();
563
+    let section_addrs = input_section_address_map(layout, atoms);
564
+    let synth_addrs = synthetic_address_maps(layout, synthetic_plan);
565
+    let symbol_name_index = build_symbol_name_index(sym_table);
566
+    let resolve = ResolveView {
567
+        sym_table,
568
+        symbol_name_index: &symbol_name_index,
569
+        atom_table: atoms,
570
+        atom_addrs: &atom_addrs,
571
+        atoms_by_input_section: &atoms_by_input_section,
572
+        section_addrs: &section_addrs,
573
+        stub_addrs: &synth_addrs.stub_addrs,
574
+        got_addrs: &synth_addrs.got_addrs,
575
+        lazy_pointer_addrs: &synth_addrs.lazy_pointer_addrs,
576
+        stub_helper_entry_addrs: &synth_addrs.stub_helper_entry_addrs,
577
+        stub_helper_header_addr: synth_addrs.stub_helper_header_addr,
578
+        dyld_private_addr: synth_addrs.dyld_private_addr,
579
+        icf_redirects,
580
+    };
581
+
582
+    let mut redirects = HashMap::new();
583
+    let mut island_index: HashMap<crate::resolve::AtomId, usize> = HashMap::new();
584
+    let mut index: HashMap<ThunkBucketKey, usize> = HashMap::new();
585
+    let mut islands: Vec<ThunkIsland> = Vec::new();
586
+    let mut entries: Vec<ThunkEntry> = Vec::new();
587
+    for (atom_id, atom) in atoms.iter() {
588
+        let Some(obj) = input_map.get(&atom.origin) else {
589
+            continue;
590
+        };
591
+        let relocs = reloc_cache
592
+            .get(&(atom.origin, atom.input_section))
593
+            .map(Vec::as_slice)
594
+            .unwrap_or(&[]);
595
+        for reloc in relocs_for_atom(relocs, atom) {
596
+            if reloc.kind != RelocKind::Branch26 {
597
+                continue;
598
+            }
599
+            let local_offset = reloc.offset.saturating_sub(atom.input_offset);
600
+            let Some(place) = resolve.atom_addrs.get(&atom.id).copied() else {
601
+                continue;
602
+            };
603
+            let Some(caller_segment) = atom_segments.get(&atom.id).cloned() else {
604
+                continue;
605
+            };
606
+            let place = place + local_offset as u64;
607
+            let target_key = resolve_branch_target_key(obj, atom, reloc, &resolve)?;
608
+            let target = resolve_branch_target_from_key(obj, atom, reloc, target_key, &resolve)?;
609
+            let needs_thunk = match opts.thunks {
610
+                ThunkMode::None => false,
611
+                ThunkMode::Safe => !branch26_in_range(place, target),
612
+                ThunkMode::All => true,
613
+            };
614
+            if !needs_thunk {
615
+                continue;
616
+            }
617
+            let island = if let Some(&existing) = island_index.get(&atom_id) {
618
+                existing
619
+            } else {
620
+                let next = islands.len();
621
+                islands.push(ThunkIsland {
622
+                    segment: caller_segment.clone(),
623
+                    after_atom: atom_id,
624
+                });
625
+                island_index.insert(atom_id, next);
626
+                next
627
+            };
628
+            let bucket_key = ThunkBucketKey {
629
+                island,
630
+                target: target_key,
631
+            };
632
+            let thunk_index = if let Some(&existing) = index.get(&bucket_key) {
633
+                existing
634
+            } else {
635
+                let next = entries.len();
636
+                let slot_in_island = entries
637
+                    .iter()
638
+                    .filter(|entry| entry.island == island)
639
+                    .count();
640
+                entries.push(ThunkEntry {
641
+                    island,
642
+                    slot_in_island,
643
+                    target: target_key,
644
+                });
645
+                index.insert(bucket_key, next);
646
+                next
647
+            };
648
+            redirects.insert((atom_id, local_offset), thunk_index);
649
+        }
650
+    }
651
+
652
+    if entries.is_empty() {
653
+        Ok(None)
654
+    } else {
655
+        Ok(Some(ThunkPlan {
656
+            redirects,
657
+            islands,
658
+            entries,
659
+        }))
660
+    }
661
+}
662
+
287663
 fn apply_one(
288664
     bytes: &mut [u8],
289665
     atom: &Atom,
290666
     obj: &ObjectFile,
291667
     reloc: Reloc,
292668
     resolve: &ResolveView<'_>,
669
+    thunk_plan: Option<&ThunkPlan>,
670
+    thunk_addrs: Option<&HashMap<usize, u64>>,
293671
 ) -> Result<(), RelocError> {
294672
     let local_offset = reloc.offset.checked_sub(atom.input_offset).ok_or_else(|| {
295673
         reloc_error(
@@ -313,7 +691,7 @@ fn apply_one(
313691
     })? + local_offset as u64;
314692
     match reloc.kind {
315693
         RelocKind::Unsigned => {
316
-            if dylib_import_symbol_id(obj, reloc.referent, resolve.sym_table).is_some() {
694
+            if dylib_import_symbol_id(obj, reloc.referent, resolve).is_some() {
317695
                 if direct_import_bind_supported(reloc) {
318696
                     clear_direct_import_slot(bytes, atom, obj, local_offset, reloc)
319697
                 } else {
@@ -347,15 +725,29 @@ fn apply_one(
347725
             resolve_referent(obj, atom, reloc.kind, reloc.referent, resolve)?,
348726
             resolve,
349727
         ),
350
-        RelocKind::Branch26 => patch_branch26(
351
-            bytes,
352
-            atom,
353
-            obj,
354
-            local_offset,
355
-            reloc,
356
-            place,
357
-            resolve_branch_target(obj, atom, reloc, resolve)?,
358
-        ),
728
+        RelocKind::Branch26 => {
729
+            let target = if let Some(plan) = thunk_plan {
730
+                if let Some(index) = plan.redirect_for(atom.id, local_offset) {
731
+                    thunk_addrs
732
+                        .and_then(|addrs| addrs.get(&index).copied())
733
+                        .ok_or_else(|| {
734
+                            reloc_error(
735
+                                atom,
736
+                                &obj.path,
737
+                                local_offset,
738
+                                reloc.kind,
739
+                                &describe_referent(obj, reloc.referent),
740
+                                "thunk section missing final address".to_string(),
741
+                            )
742
+                        })?
743
+                } else {
744
+                    resolve_branch_target(obj, atom, reloc, resolve)?
745
+                }
746
+            } else {
747
+                resolve_branch_target(obj, atom, reloc, resolve)?
748
+            };
749
+            patch_branch26(bytes, atom, obj, local_offset, reloc, place, target)
750
+        }
359751
         RelocKind::Page21 => patch_page21(
360752
             bytes,
361753
             atom,
@@ -417,7 +809,7 @@ fn apply_one(
417809
         ),
418810
         RelocKind::TlvpLoadPageOff12 => {
419811
             let target = resolve_tlvp_pageoff_target(obj, atom, reloc, resolve)?;
420
-            if dylib_import_symbol_id(obj, reloc.referent, resolve.sym_table).is_some() {
812
+            if dylib_import_symbol_id(obj, reloc.referent, resolve).is_some() {
421813
                 patch_pageoff12(bytes, atom, obj, local_offset, reloc, target)
422814
             } else {
423815
                 patch_tlvp_pageoff12(bytes, atom, obj, local_offset, reloc, target)
@@ -432,19 +824,263 @@ fn resolve_branch_target(
432824
     reloc: Reloc,
433825
     resolve: &ResolveView<'_>,
434826
 ) -> Result<u64, RelocError> {
435
-    if let Some(symbol_id) = dylib_import_symbol_id(obj, reloc.referent, resolve.sym_table) {
436
-        return resolve.stub_addrs.get(&symbol_id).copied().ok_or_else(|| {
437
-            reloc_error(
827
+    let key = resolve_branch_target_key(obj, atom, reloc, resolve)?;
828
+    resolve_branch_target_from_key(obj, atom, reloc, key, resolve)
829
+}
830
+
831
+fn resolve_branch_target_key(
832
+    obj: &ObjectFile,
833
+    atom: &Atom,
834
+    reloc: Reloc,
835
+    resolve: &ResolveView<'_>,
836
+) -> Result<BranchTargetKey, RelocError> {
837
+    if let Some(symbol_id) = dylib_import_symbol_id(obj, reloc.referent, resolve) {
838
+        return Ok(BranchTargetKey::Stub(symbol_id));
839
+    }
840
+    match reloc.referent {
841
+        Referent::Section(section_idx) => Ok(BranchTargetKey::InputSectionOffset {
842
+            origin: atom.origin,
843
+            input_section: section_idx,
844
+            input_offset: 0,
845
+        }),
846
+        Referent::Symbol(sym_idx) => {
847
+            let input_sym = obj.symbols.get(sym_idx as usize).ok_or_else(|| {
848
+                reloc_error(
849
+                    atom,
850
+                    &obj.path,
851
+                    0,
852
+                    reloc.kind,
853
+                    &format!("symbol #{sym_idx}"),
854
+                    "symbol index is out of range".to_string(),
855
+                )
856
+            })?;
857
+            if let Ok(name) = obj.symbol_name(input_sym) {
858
+                if let Some(symbol_id) = resolve.symbol_name_index.get(name).copied() {
859
+                    return Ok(BranchTargetKey::Symbol(symbol_id));
860
+                }
861
+            }
862
+            match input_sym.kind() {
863
+                SymKind::Sect => {
864
+                    let section = obj.section_for_symbol(input_sym).ok_or_else(|| {
865
+                        reloc_error(
866
+                            atom,
867
+                            &obj.path,
868
+                            0,
869
+                            reloc.kind,
870
+                            &describe_input_symbol(obj, input_sym),
871
+                            "section-backed symbol did not resolve to an input section".to_string(),
872
+                        )
873
+                    })?;
874
+                    Ok(BranchTargetKey::InputSectionOffset {
875
+                        origin: atom.origin,
876
+                        input_section: input_sym.sect_idx(),
877
+                        input_offset: input_sym.value().saturating_sub(section.addr) as u32,
878
+                    })
879
+                }
880
+                SymKind::Abs => Err(reloc_error(
881
+                    atom,
882
+                    &obj.path,
883
+                    0,
884
+                    reloc.kind,
885
+                    &describe_input_symbol(obj, input_sym),
886
+                    "absolute BRANCH26 targets are not supported".to_string(),
887
+                )),
888
+                SymKind::Undef => Err(reloc_error(
889
+                    atom,
890
+                    &obj.path,
891
+                    0,
892
+                    reloc.kind,
893
+                    &describe_input_symbol(obj, input_sym),
894
+                    "symbol remained undefined at relocation time".to_string(),
895
+                )),
896
+                SymKind::Indirect => Err(reloc_error(
897
+                    atom,
898
+                    &obj.path,
899
+                    0,
900
+                    reloc.kind,
901
+                    &describe_input_symbol(obj, input_sym),
902
+                    "indirect BRANCH26 targets are not yet supported".to_string(),
903
+                )),
904
+            }
905
+        }
906
+    }
907
+}
908
+
909
+fn resolve_branch_target_from_key(
910
+    obj: &ObjectFile,
911
+    atom: &Atom,
912
+    reloc: Reloc,
913
+    key: BranchTargetKey,
914
+    resolve: &ResolveView<'_>,
915
+) -> Result<u64, RelocError> {
916
+    match key {
917
+        BranchTargetKey::Symbol(symbol_id) => match resolve.sym_table.get(symbol_id) {
918
+            Symbol::Defined {
919
+                atom: target_atom,
920
+                value,
921
+                ..
922
+            } => resolve
923
+                .atom_addrs
924
+                .get(&canonical_atom(*target_atom, resolve.icf_redirects))
925
+                .copied()
926
+                .map(|addr| addr + *value)
927
+                .ok_or_else(|| {
928
+                    reloc_error(
929
+                        atom,
930
+                        &obj.path,
931
+                        reloc.offset.saturating_sub(atom.input_offset),
932
+                        reloc.kind,
933
+                        &describe_referent(obj, reloc.referent),
934
+                        "target atom missing final address".to_string(),
935
+                    )
936
+                }),
937
+            Symbol::DylibImport { .. } => Err(reloc_error(
438938
                 atom,
439939
                 &obj.path,
440940
                 reloc.offset.saturating_sub(atom.input_offset),
441941
                 reloc.kind,
442942
                 &describe_referent(obj, reloc.referent),
443943
                 "dylib import is missing synthetic stub".to_string(),
444
-            )
445
-        });
944
+            )),
945
+            other => Err(reloc_error(
946
+                atom,
947
+                &obj.path,
948
+                reloc.offset.saturating_sub(atom.input_offset),
949
+                reloc.kind,
950
+                &describe_referent(obj, reloc.referent),
951
+                format!("symbol resolved to unsupported state {:?}", other.kind()),
952
+            )),
953
+        },
954
+        BranchTargetKey::Stub(symbol_id) => {
955
+            resolve.stub_addrs.get(&symbol_id).copied().ok_or_else(|| {
956
+                reloc_error(
957
+                    atom,
958
+                    &obj.path,
959
+                    reloc.offset.saturating_sub(atom.input_offset),
960
+                    reloc.kind,
961
+                    &describe_referent(obj, reloc.referent),
962
+                    "dylib import is missing synthetic stub".to_string(),
963
+                )
964
+            })
965
+        }
966
+        BranchTargetKey::InputSectionOffset {
967
+            origin,
968
+            input_section,
969
+            input_offset,
970
+        } => resolve_input_section_offset(
971
+            origin,
972
+            input_section,
973
+            input_offset,
974
+            InputSectionResolveCtx {
975
+                obj,
976
+                atom,
977
+                kind: reloc.kind,
978
+                referent: &describe_referent(obj, reloc.referent),
979
+            },
980
+            resolve,
981
+        ),
446982
     }
447
-    resolve_referent(obj, atom, reloc.kind, reloc.referent, resolve)
983
+}
984
+
985
+fn branch26_in_range(place: u64, target: u64) -> bool {
986
+    let delta = target.wrapping_sub(place) as i64;
987
+    delta & 0b11 == 0 && fits_signed(delta >> 2, 26)
988
+}
989
+
990
+fn synthesize_thunk_section(
991
+    layout: &mut Layout,
992
+    plan: &ThunkPlan,
993
+    resolve: &ResolveView<'_>,
994
+) -> Result<(), RelocError> {
995
+    let mut counts: HashMap<usize, usize> = HashMap::new();
996
+    for entry in &plan.entries {
997
+        *counts.entry(entry.island).or_insert(0usize) += 1;
998
+    }
999
+    for (island_idx, island) in plan.islands.iter().enumerate() {
1000
+        let expected_len = counts.get(&island_idx).copied().unwrap_or(0) * THUNK_SIZE as usize;
1001
+        let section_idx = find_thunk_section_index(layout, island).ok_or_else(|| RelocError {
1002
+            input: PathBuf::from("<synthetic thunks>"),
1003
+            atom: crate::resolve::AtomId(0),
1004
+            atom_offset: (island_idx as u32) * THUNK_SIZE as u32,
1005
+            kind: RelocKind::Branch26,
1006
+            referent: "thunk section".to_string(),
1007
+            detail: format!(
1008
+                "missing thunk section for island after {},{}",
1009
+                island.segment, island.after_atom.0
1010
+            ),
1011
+        })?;
1012
+        let section = &mut layout.sections[section_idx];
1013
+        if section.synthetic_data.len() != expected_len {
1014
+            section.synthetic_data.resize(expected_len, 0);
1015
+        }
1016
+    }
1017
+    for (idx, entry) in plan.entries.iter().enumerate() {
1018
+        let island = &plan.islands[entry.island];
1019
+        let section_idx = find_thunk_section_index(layout, island).ok_or_else(|| RelocError {
1020
+            input: PathBuf::from("<synthetic thunks>"),
1021
+            atom: crate::resolve::AtomId(0),
1022
+            atom_offset: (idx as u32) * THUNK_SIZE as u32,
1023
+            kind: RelocKind::Branch26,
1024
+            referent: "thunk section".to_string(),
1025
+            detail: format!(
1026
+                "missing thunk section for island after {},{}",
1027
+                island.segment, island.after_atom.0
1028
+            ),
1029
+        })?;
1030
+        let section = &mut layout.sections[section_idx];
1031
+        let thunk_addr = section.addr + (entry.slot_in_island as u64) * THUNK_SIZE;
1032
+        let target = match entry.target {
1033
+            BranchTargetKey::Symbol(symbol_id) => match resolve.sym_table.get(symbol_id) {
1034
+                Symbol::Defined { atom, value, .. } => resolve
1035
+                    .atom_addrs
1036
+                    .get(&canonical_atom(*atom, resolve.icf_redirects))
1037
+                    .copied()
1038
+                    .map(|addr| addr + *value),
1039
+                _ => None,
1040
+            },
1041
+            BranchTargetKey::Stub(symbol_id) => resolve.stub_addrs.get(&symbol_id).copied(),
1042
+            BranchTargetKey::InputSectionOffset {
1043
+                origin,
1044
+                input_section,
1045
+                input_offset,
1046
+            } => resolve_input_section_offset_simple(origin, input_section, input_offset, resolve),
1047
+        }
1048
+        .ok_or_else(|| RelocError {
1049
+            input: PathBuf::from("<synthetic thunks>"),
1050
+            atom: crate::resolve::AtomId(0),
1051
+            atom_offset: (idx as u32) * THUNK_SIZE as u32,
1052
+            kind: RelocKind::Branch26,
1053
+            referent: "thunk target".to_string(),
1054
+            detail: "missing final target address".to_string(),
1055
+        })?;
1056
+        let adrp = encode_adrp_reg(16, thunk_addr, target, "thunk target")?;
1057
+        let add = encode_add_x_reg_pageoff(16, target, "thunk target")?;
1058
+        let start = entry.slot_in_island * THUNK_SIZE as usize;
1059
+        section.synthetic_data[start..start + 4].copy_from_slice(&adrp.to_le_bytes());
1060
+        section.synthetic_data[start + 4..start + 8].copy_from_slice(&add.to_le_bytes());
1061
+        section.synthetic_data[start + 8..start + 12].copy_from_slice(&BR_X16.to_le_bytes());
1062
+    }
1063
+    Ok(())
1064
+}
1065
+
1066
+fn find_thunk_section_index(layout: &Layout, island: &ThunkIsland) -> Option<usize> {
1067
+    layout
1068
+        .sections
1069
+        .iter()
1070
+        .enumerate()
1071
+        .skip(1)
1072
+        .find_map(|(idx, section)| {
1073
+            let prev = &layout.sections[idx - 1];
1074
+            (prev.segment == island.segment
1075
+                && prev
1076
+                    .atoms
1077
+                    .last()
1078
+                    .map(|placed| placed.atom == island.after_atom)
1079
+                    .unwrap_or(false)
1080
+                && section.segment == island.segment
1081
+                && section.name == "__thunks")
1082
+                .then_some(idx)
1083
+        })
4481084
 }
4491085
 
4501086
 fn resolve_got_target(
@@ -453,7 +1089,7 @@ fn resolve_got_target(
4531089
     reloc: Reloc,
4541090
     resolve: &ResolveView<'_>,
4551091
 ) -> Result<u64, RelocError> {
456
-    let Some(symbol_id) = symbol_referent_id(obj, reloc.referent, resolve.sym_table) else {
1092
+    let Some(symbol_id) = symbol_referent_id(obj, reloc.referent, resolve) else {
4571093
         return Err(reloc_error(
4581094
             atom,
4591095
             &obj.path,
@@ -476,9 +1112,14 @@ fn resolve_got_target(
4761112
 }
4771113
 
4781114
 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)
1115
+    match symbol_referent_id(obj, reloc.referent, resolve) {
1116
+        Some(symbol_id) => match resolve.sym_table.get(symbol_id) {
1117
+            Symbol::DylibImport { .. } => false,
1118
+            Symbol::Defined { .. } => true,
1119
+            _ => true,
1120
+        },
1121
+        None => true,
1122
+    }
4821123
 }
4831124
 
4841125
 fn resolve_tlvp_target(
@@ -487,7 +1128,7 @@ fn resolve_tlvp_target(
4871128
     reloc: Reloc,
4881129
     resolve: &ResolveView<'_>,
4891130
 ) -> Result<u64, RelocError> {
490
-    if dylib_import_symbol_id(obj, reloc.referent, resolve.sym_table).is_some() {
1131
+    if dylib_import_symbol_id(obj, reloc.referent, resolve).is_some() {
4911132
         return resolve_got_target(obj, atom, reloc, resolve);
4921133
     }
4931134
     resolve_referent(obj, atom, reloc.kind, reloc.referent, resolve)
@@ -499,7 +1140,7 @@ fn resolve_tlvp_pageoff_target(
4991140
     reloc: Reloc,
5001141
     resolve: &ResolveView<'_>,
5011142
 ) -> Result<u64, RelocError> {
502
-    if dylib_import_symbol_id(obj, reloc.referent, resolve.sym_table).is_some() {
1143
+    if dylib_import_symbol_id(obj, reloc.referent, resolve).is_some() {
5031144
         return resolve_got_target(obj, atom, reloc, resolve);
5041145
     }
5051146
     resolve_referent(obj, atom, reloc.kind, reloc.referent, resolve)
@@ -552,12 +1193,15 @@ fn resolve_symbol_referent(
5521193
     })?;
5531194
 
5541195
     if let Ok(name) = obj.symbol_name(input_sym) {
555
-        if let Some((_, symbol)) = resolve
556
-            .sym_table
557
-            .iter()
558
-            .find(|(_, symbol)| resolve.sym_table.interner.resolve(symbol.name()) == name)
559
-        {
560
-            return resolve_global_symbol(obj, atom, kind, name, symbol, resolve);
1196
+        if let Some(symbol_id) = resolve.symbol_name_index.get(name).copied() {
1197
+            return resolve_global_symbol(
1198
+                obj,
1199
+                atom,
1200
+                kind,
1201
+                name,
1202
+                resolve.sym_table.get(symbol_id),
1203
+                resolve,
1204
+            );
5611205
         }
5621206
     }
5631207
 
@@ -567,27 +1211,35 @@ fn resolve_symbol_referent(
5671211
 fn dylib_import_symbol_id(
5681212
     obj: &ObjectFile,
5691213
     referent: Referent,
570
-    sym_table: &SymbolTable,
1214
+    resolve: &ResolveView<'_>,
5711215
 ) -> 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)
1216
+    let symbol_id = symbol_referent_id(obj, referent, resolve)?;
1217
+    matches!(resolve.sym_table.get(symbol_id), Symbol::DylibImport { .. }).then_some(symbol_id)
5741218
 }
5751219
 
5761220
 fn symbol_referent_id(
5771221
     obj: &ObjectFile,
5781222
     referent: Referent,
579
-    sym_table: &SymbolTable,
1223
+    resolve: &ResolveView<'_>,
5801224
 ) -> Option<SymbolId> {
5811225
     let Referent::Symbol(sym_idx) = referent else {
5821226
         return None;
5831227
     };
5841228
     let input_sym = obj.symbols.get(sym_idx as usize)?;
5851229
     let name = obj.symbol_name(input_sym).ok()?;
586
-    let (symbol_id, symbol) = sym_table
1230
+    resolve.symbol_name_index.get(name).copied()
1231
+}
1232
+
1233
+fn build_symbol_name_index(sym_table: &SymbolTable) -> HashMap<String, SymbolId> {
1234
+    sym_table
5871235
         .iter()
588
-        .find(|(_, symbol)| sym_table.interner.resolve(symbol.name()) == name)?;
589
-    let _ = symbol;
590
-    Some(symbol_id)
1236
+        .map(|(symbol_id, symbol)| {
1237
+            (
1238
+                sym_table.interner.resolve(symbol.name()).to_string(),
1239
+                symbol_id,
1240
+            )
1241
+        })
1242
+        .collect()
5911243
 }
5921244
 
5931245
 fn resolve_global_symbol(
@@ -672,12 +1324,14 @@ fn resolve_input_symbol_at_origin(
6721324
             let section_offset = input_sym.value().saturating_sub(section.addr) as u32;
6731325
             resolve_input_section_offset(
6741326
                 origin,
675
-                obj,
676
-                atom,
677
-                kind,
6781327
                 input_sym.sect_idx(),
6791328
                 section_offset,
680
-                &describe_input_symbol(obj, input_sym),
1329
+                InputSectionResolveCtx {
1330
+                    obj,
1331
+                    atom,
1332
+                    kind,
1333
+                    referent: &describe_input_symbol(obj, input_sym),
1334
+                },
6811335
                 resolve,
6821336
             )
6831337
         }
@@ -702,12 +1356,9 @@ fn resolve_input_symbol_at_origin(
7021356
 
7031357
 fn resolve_input_section_offset(
7041358
     origin: InputId,
705
-    obj: &ObjectFile,
706
-    atom: &Atom,
707
-    kind: RelocKind,
7081359
     input_section: u8,
7091360
     input_offset: u32,
710
-    referent: &str,
1361
+    ctx: InputSectionResolveCtx<'_>,
7111362
     resolve: &ResolveView<'_>,
7121363
 ) -> Result<u64, RelocError> {
7131364
     if let Some(atom_ids) = resolve.atoms_by_input_section.get(&(origin, input_section)) {
@@ -723,17 +1374,18 @@ fn resolve_input_section_offset(
7231374
                 None
7241375
             }
7251376
         }) {
1377
+            let target_atom = canonical_atom(target_atom, resolve.icf_redirects);
7261378
             let atom_addr = resolve
7271379
                 .atom_addrs
7281380
                 .get(&target_atom)
7291381
                 .copied()
7301382
                 .ok_or_else(|| {
7311383
                     reloc_error(
732
-                        atom,
733
-                        &obj.path,
1384
+                        ctx.atom,
1385
+                        &ctx.obj.path,
7341386
                         0,
735
-                        kind,
736
-                        referent,
1387
+                        ctx.kind,
1388
+                        ctx.referent,
7371389
                         "section-backed symbol's containing atom is missing a final address"
7381390
                             .to_string(),
7391391
                     )
@@ -748,17 +1400,68 @@ fn resolve_input_section_offset(
7481400
         .copied()
7491401
         .ok_or_else(|| {
7501402
             reloc_error(
751
-                atom,
752
-                &obj.path,
1403
+                ctx.atom,
1404
+                &ctx.obj.path,
7531405
                 0,
754
-                kind,
755
-                referent,
1406
+                ctx.kind,
1407
+                ctx.referent,
7561408
                 "section-backed symbol's output section is missing".to_string(),
7571409
             )
7581410
         })?;
7591411
     Ok(section_addr + input_offset as u64)
7601412
 }
7611413
 
1414
+fn resolve_input_section_offset_simple(
1415
+    origin: InputId,
1416
+    input_section: u8,
1417
+    input_offset: u32,
1418
+    resolve: &ResolveView<'_>,
1419
+) -> Option<u64> {
1420
+    if let Some(atom_ids) = resolve.atoms_by_input_section.get(&(origin, input_section)) {
1421
+        if let Some((target_atom, delta)) = atom_ids.iter().find_map(|atom_id| {
1422
+            let candidate = resolve.atom_table.get(*atom_id);
1423
+            let start = candidate.input_offset;
1424
+            let end = candidate.input_offset.saturating_add(candidate.size);
1425
+            if start <= input_offset && input_offset < end {
1426
+                Some((*atom_id, input_offset - start))
1427
+            } else if input_offset == end {
1428
+                Some((*atom_id, candidate.size))
1429
+            } else {
1430
+                None
1431
+            }
1432
+        }) {
1433
+            let target_atom = canonical_atom(target_atom, resolve.icf_redirects);
1434
+            return resolve
1435
+                .atom_addrs
1436
+                .get(&target_atom)
1437
+                .copied()
1438
+                .map(|addr| addr + delta as u64);
1439
+        }
1440
+    }
1441
+    resolve
1442
+        .section_addrs
1443
+        .get(&(origin, input_section))
1444
+        .copied()
1445
+        .map(|section_addr| section_addr + input_offset as u64)
1446
+}
1447
+
1448
+fn canonical_atom(
1449
+    atom_id: crate::resolve::AtomId,
1450
+    redirects: Option<&HashMap<crate::resolve::AtomId, crate::resolve::AtomId>>,
1451
+) -> crate::resolve::AtomId {
1452
+    let Some(redirects) = redirects else {
1453
+        return atom_id;
1454
+    };
1455
+    let mut current = atom_id;
1456
+    while let Some(&next) = redirects.get(&current) {
1457
+        if next == current {
1458
+            break;
1459
+        }
1460
+        current = next;
1461
+    }
1462
+    current
1463
+}
1464
+
7621465
 fn patch_unsigned(
7631466
     bytes: &mut [u8],
7641467
     atom: &Atom,
@@ -1917,6 +2620,21 @@ fn reloc_error(
19172620
 #[cfg(test)]
19182621
 mod tests {
19192622
     use super::*;
2623
+    use std::path::PathBuf;
2624
+
2625
+    use crate::atom::{AtomFlags, AtomSection};
2626
+    use crate::input::ObjectFile;
2627
+    use crate::macho::constants::{
2628
+        CPU_SUBTYPE_ARM64_ALL, CPU_TYPE_ARM64, MH_MAGIC_64, MH_OBJECT, N_EXT, N_SECT,
2629
+        S_ATTR_PURE_INSTRUCTIONS, S_ATTR_SOME_INSTRUCTIONS, S_REGULAR,
2630
+    };
2631
+    use crate::macho::reader::MachHeader64;
2632
+    use crate::reloc::{write_raw_relocs, write_relocs};
2633
+    use crate::resolve::{InsertOutcome, Symbol, SymbolTable};
2634
+    use crate::section::InputSection;
2635
+    use crate::string_table::StringTable;
2636
+    use crate::symbol::{InputSymbol, RawNlist};
2637
+    use crate::OutputKind;
19202638
 
19212639
     #[test]
19222640
     fn branch26_patches_low_bits() {
@@ -1979,4 +2697,194 @@ mod tests {
19792697
         assert!(!fits_signed(1 << 25, 26));
19802698
         assert!(!fits_signed(-(1 << 25) - 1, 26));
19812699
     }
2700
+
2701
+    #[test]
2702
+    fn thunk_plan_splits_monolithic_text_section_into_multiple_islands() {
2703
+        let gap = 0x0900_0000u32;
2704
+        let caller2_offset = 4 + gap;
2705
+        let target_offset = 8 + gap * 2;
2706
+        let raw_relocs = branch26_raw_relocs(&[0, caller2_offset]);
2707
+        let object = thunk_test_object(raw_relocs, target_offset as u64, target_offset as u64 + 4);
2708
+
2709
+        let mut atoms = AtomTable::new();
2710
+        let caller1 = atoms.push(test_atom(0, 4));
2711
+        atoms.push(test_atom(4, gap));
2712
+        let caller2 = atoms.push(test_atom(caller2_offset, 4));
2713
+        atoms.push(test_atom(caller2_offset + 4, gap));
2714
+        let target = atoms.push(test_atom(target_offset, 4));
2715
+
2716
+        let mut sym_table = SymbolTable::new();
2717
+        let target_name = sym_table.intern("_target");
2718
+        let insert = sym_table
2719
+            .insert(Symbol::Defined {
2720
+                name: target_name,
2721
+                origin: crate::resolve::InputId(0),
2722
+                atom: target,
2723
+                value: 0,
2724
+                weak: false,
2725
+                private_extern: false,
2726
+                no_dead_strip: false,
2727
+            })
2728
+            .unwrap();
2729
+        assert!(matches!(insert, InsertOutcome::Inserted(_)));
2730
+
2731
+        let inputs = [LayoutInput {
2732
+            id: crate::resolve::InputId(0),
2733
+            object: &object,
2734
+            load_order: 0,
2735
+            archive_member_offset: None,
2736
+        }];
2737
+        let opts = LinkOptions {
2738
+            kind: OutputKind::Executable,
2739
+            ..LinkOptions::default()
2740
+        };
2741
+        let base_layout = Layout::build(OutputKind::Executable, &inputs, &atoms, 0);
2742
+        let plan = plan_thunks(&opts, &base_layout, &inputs, &atoms, &sym_table, None, None)
2743
+            .unwrap()
2744
+            .unwrap();
2745
+
2746
+        assert_eq!(
2747
+            plan.redirect_for(caller1, 0),
2748
+            Some(0),
2749
+            "expected first caller to use its own thunk"
2750
+        );
2751
+        assert_eq!(
2752
+            plan.redirect_for(caller2, 0),
2753
+            Some(1),
2754
+            "expected second caller to use its own thunk"
2755
+        );
2756
+
2757
+        let rebuilt = Layout::build_with_synthetics_and_extra_filtered(
2758
+            OutputKind::Executable,
2759
+            &inputs,
2760
+            &atoms,
2761
+            0,
2762
+            None,
2763
+            None,
2764
+            crate::layout::ExtraLayoutSections {
2765
+                extra_sections: &plan.output_sections(),
2766
+                split_after_atoms: &plan.split_after_atoms(),
2767
+            },
2768
+        );
2769
+        let text_section_count = rebuilt
2770
+            .sections
2771
+            .iter()
2772
+            .filter(|section| section.segment == "__TEXT" && section.name == "__text")
2773
+            .count();
2774
+        let thunk_section_count = rebuilt
2775
+            .sections
2776
+            .iter()
2777
+            .filter(|section| section.segment == "__TEXT" && section.name == "__thunks")
2778
+            .count();
2779
+        assert_eq!(
2780
+            text_section_count, 3,
2781
+            "expected the monolithic text section to split around the two caller atoms"
2782
+        );
2783
+        assert_eq!(
2784
+            thunk_section_count, 2,
2785
+            "expected one thunk island per far caller atom"
2786
+        );
2787
+        let text_sequence: Vec<_> = rebuilt
2788
+            .sections
2789
+            .iter()
2790
+            .filter(|section| section.segment == "__TEXT")
2791
+            .map(|section| section.name.as_str())
2792
+            .collect();
2793
+        assert_eq!(
2794
+            text_sequence,
2795
+            vec!["__text", "__thunks", "__text", "__thunks", "__text"]
2796
+        );
2797
+
2798
+        let replan = plan_thunks(&opts, &rebuilt, &inputs, &atoms, &sym_table, None, None)
2799
+            .unwrap()
2800
+            .unwrap();
2801
+        assert_eq!(
2802
+            replan, plan,
2803
+            "expected thunk planning to converge once the intra-section islands exist"
2804
+        );
2805
+    }
2806
+
2807
+    fn branch26_raw_relocs(offsets: &[u32]) -> Vec<u8> {
2808
+        let relocs: Vec<_> = offsets
2809
+            .iter()
2810
+            .copied()
2811
+            .map(|offset| crate::reloc::Reloc {
2812
+                offset,
2813
+                kind: RelocKind::Branch26,
2814
+                length: RelocLength::Word,
2815
+                pcrel: true,
2816
+                referent: Referent::Symbol(0),
2817
+                addend: 0,
2818
+                subtrahend: None,
2819
+            })
2820
+            .collect();
2821
+        let raws = write_relocs(&relocs).unwrap();
2822
+        let mut out = Vec::new();
2823
+        write_raw_relocs(&raws, &mut out);
2824
+        out
2825
+    }
2826
+
2827
+    fn thunk_test_object(raw_relocs: Vec<u8>, target_offset: u64, section_size: u64) -> ObjectFile {
2828
+        let strings = b"\0_target\0".to_vec();
2829
+        ObjectFile {
2830
+            path: PathBuf::from("/tmp/thunk-plan.o"),
2831
+            header: MachHeader64 {
2832
+                magic: MH_MAGIC_64,
2833
+                cputype: CPU_TYPE_ARM64,
2834
+                cpusubtype: CPU_SUBTYPE_ARM64_ALL,
2835
+                filetype: MH_OBJECT,
2836
+                ncmds: 0,
2837
+                sizeofcmds: 0,
2838
+                flags: 0,
2839
+                reserved: 0,
2840
+            },
2841
+            commands: Vec::new(),
2842
+            sections: vec![InputSection {
2843
+                segname: "__TEXT".into(),
2844
+                sectname: "__text".into(),
2845
+                kind: crate::section::SectionKind::Text,
2846
+                addr: 0,
2847
+                size: section_size,
2848
+                align_pow2: 2,
2849
+                flags: S_REGULAR | S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS,
2850
+                offset: 0,
2851
+                reloff: 0,
2852
+                nreloc: (raw_relocs.len() / 8) as u32,
2853
+                reserved1: 0,
2854
+                reserved2: 0,
2855
+                reserved3: 0,
2856
+                data: Vec::new(),
2857
+                raw_relocs,
2858
+            }],
2859
+            symbols: vec![InputSymbol::from_raw(RawNlist {
2860
+                strx: 1,
2861
+                n_type: N_SECT | N_EXT,
2862
+                n_sect: 1,
2863
+                n_desc: 0,
2864
+                n_value: target_offset,
2865
+            })],
2866
+            strings: StringTable::from_bytes(strings),
2867
+            symtab: None,
2868
+            dysymtab: None,
2869
+            loh: Vec::new(),
2870
+            data_in_code: Vec::new(),
2871
+        }
2872
+    }
2873
+
2874
+    fn test_atom(input_offset: u32, size: u32) -> Atom {
2875
+        Atom {
2876
+            id: crate::resolve::AtomId(0),
2877
+            origin: crate::resolve::InputId(0),
2878
+            input_section: 1,
2879
+            section: AtomSection::Text,
2880
+            input_offset,
2881
+            size,
2882
+            align_pow2: 2,
2883
+            owner: None,
2884
+            alt_entries: Vec::new(),
2885
+            data: Vec::new(),
2886
+            flags: AtomFlags::NONE,
2887
+            parent_of: None,
2888
+        }
2889
+    }
19822890
 }
src/reloc/mod.rsmodified
16 lines changed — click to load
@@ -114,7 +114,7 @@ pub fn write_raw_relocs(relocs: &[RawRelocation], out: &mut Vec<u8>) {
114114
 // Fused Reloc form. Sprint 11's reloc-application pass consumes this.
115115
 // ---------------------------------------------------------------------------
116116
 
117
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118118
 pub enum RelocKind {
119119
     Unsigned,
120120
     Branch26,
@@ -132,7 +132,7 @@ pub enum RelocKind {
132132
     Subtractor,
133133
 }
134134
 
135
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
136136
 #[repr(u8)]
137137
 pub enum RelocLength {
138138
     Byte = 0,
src/resolve.rsmodified
121 lines changed — click to load
@@ -1135,6 +1135,7 @@ impl From<SeedError> for FetchError {
11351135
 #[derive(Debug, Default)]
11361136
 pub struct DrainReport {
11371137
     pub fetched_members: usize,
1138
+    pub loaded_paths: Vec<PathBuf>,
11381139
     pub duplicates: Vec<InsertError>,
11391140
     pub referrers: ReferrerLog,
11401141
 }
@@ -1179,6 +1180,9 @@ fn ingest_member_bytes(
11791180
         bytes: member_bytes,
11801181
     });
11811182
     report.fetched_members += 1;
1183
+    report
1184
+        .loaded_paths
1185
+        .push(inputs.objects[input_id.0 as usize].path.clone());
11821186
 
11831187
     let mut sub_report = SeedReport::default();
11841188
     seed_object(inputs, input_id, table, &mut sub_report)?;
@@ -1292,9 +1296,11 @@ impl DylibId {
12921296
 pub struct ClassificationReport {
12931297
     /// Strong undefineds that triggered errors under `Error` treatment.
12941298
     pub errors: Vec<Unresolved>,
1295
-    /// Strong undefineds that produced warnings under `Warning` treatment.
1299
+    /// Strong undefineds that produced warnings under `Warning` treatment and
1300
+    /// were promoted to flat-lookup imports for final emission.
12961301
     pub warnings: Vec<Unresolved>,
1297
-    /// Strong undefineds that were silently accepted under `Suppress`.
1302
+    /// Strong undefineds that were silently accepted under `Suppress` and
1303
+    /// were promoted to flat-lookup imports for final emission.
12981304
     pub suppressed: Vec<Unresolved>,
12991305
     /// Undefineds promoted to flat-lookup DylibImport entries.
13001306
     pub promoted_to_dynamic: Vec<SymbolId>,
@@ -1368,16 +1374,17 @@ pub fn did_you_mean(table: &SymbolTable, query: &str, budget: usize, max: usize)
13681374
 /// Format the full undefined-symbol diagnostic block, one entry per
13691375
 /// unresolved name: the error line, every referrer, and an optional
13701376
 /// did-you-mean hint.
1371
-pub fn format_undefined_diagnostic(
1377
+fn format_undefined_diagnostic_with_level(
13721378
     table: &SymbolTable,
13731379
     inputs: &Inputs,
13741380
     referrers: &ReferrerLog,
13751381
     unresolved: &[Unresolved],
1382
+    level: &str,
13761383
 ) -> String {
13771384
     let mut out = String::new();
13781385
     for u in unresolved {
13791386
         let name = table.interner.resolve(u.name);
1380
-        out.push_str(&format!("afs-ld: error: undefined symbol: {name}\n"));
1387
+        out.push_str(&format!("afs-ld: {level}: undefined symbol: {name}\n"));
13811388
         for origin in referrers.get(u.name) {
13821389
             if let Some(oi) = inputs.objects.get(origin.0 as usize) {
13831390
                 out.push_str(&format!("      referenced by {}\n", oi.path.display()));
@@ -1398,6 +1405,24 @@ pub fn format_undefined_diagnostic(
13981405
     out
13991406
 }
14001407
 
1408
+pub fn format_undefined_diagnostic(
1409
+    table: &SymbolTable,
1410
+    inputs: &Inputs,
1411
+    referrers: &ReferrerLog,
1412
+    unresolved: &[Unresolved],
1413
+) -> String {
1414
+    format_undefined_diagnostic_with_level(table, inputs, referrers, unresolved, "error")
1415
+}
1416
+
1417
+pub fn format_undefined_warning_diagnostic(
1418
+    table: &SymbolTable,
1419
+    inputs: &Inputs,
1420
+    referrers: &ReferrerLog,
1421
+    unresolved: &[Unresolved],
1422
+) -> String {
1423
+    format_undefined_diagnostic_with_level(table, inputs, referrers, unresolved, "warning")
1424
+}
1425
+
14011426
 /// Format a `DuplicateStrong` insertion error for user consumption. Needs
14021427
 /// the incumbent symbol (from the table) plus the losing second symbol
14031428
 /// carried in the error itself.
@@ -1438,6 +1463,21 @@ pub fn classify_unresolved(
14381463
 ) -> ClassificationReport {
14391464
     let mut report = ClassificationReport::default();
14401465
 
1466
+    fn promote_to_flat_lookup(table: &mut SymbolTable, id: SymbolId, name: Istr) {
1467
+        table.symbols[id.0 as usize] = Symbol::DylibImport {
1468
+            name,
1469
+            dylib: DylibId::INVALID,
1470
+            ordinal: FLAT_LOOKUP_ORDINAL,
1471
+            weak_import: true,
1472
+        };
1473
+        table.transitions.push(Transition {
1474
+            id,
1475
+            from: SymbolKindTag::Undefined,
1476
+            to: SymbolKindTag::DylibImport,
1477
+            cause: TransitionCause::Replaced,
1478
+        });
1479
+    }
1480
+
14411481
     // Collect undefineds before mutating — avoids double-borrow grief.
14421482
     let undefs: Vec<(SymbolId, Istr, bool)> = table
14431483
         .iter()
@@ -1458,23 +1498,16 @@ pub fn classify_unresolved(
14581498
             }
14591499
             UndefinedTreatment::Warning => {
14601500
                 report.warnings.push(Unresolved { name, id });
1501
+                promote_to_flat_lookup(table, id, name);
1502
+                report.promoted_to_dynamic.push(id);
14611503
             }
14621504
             UndefinedTreatment::Suppress => {
14631505
                 report.suppressed.push(Unresolved { name, id });
1506
+                promote_to_flat_lookup(table, id, name);
1507
+                report.promoted_to_dynamic.push(id);
14641508
             }
14651509
             UndefinedTreatment::DynamicLookup => {
1466
-                table.symbols[id.0 as usize] = Symbol::DylibImport {
1467
-                    name,
1468
-                    dylib: DylibId::INVALID,
1469
-                    ordinal: FLAT_LOOKUP_ORDINAL,
1470
-                    weak_import: true,
1471
-                };
1472
-                table.transitions.push(Transition {
1473
-                    id,
1474
-                    from: SymbolKindTag::Undefined,
1475
-                    to: SymbolKindTag::DylibImport,
1476
-                    cause: TransitionCause::Replaced,
1477
-                });
1510
+                promote_to_flat_lookup(table, id, name);
14781511
                 report.promoted_to_dynamic.push(id);
14791512
             }
14801513
         }
src/string_table.rsmodified
75 lines changed — click to load
@@ -98,10 +98,17 @@ impl StringTable {
9898
 
9999
 #[derive(Debug, Clone, Default, PartialEq, Eq)]
100100
 pub struct StringTableBuilder {
101
-    roots: Vec<(String, u32)>,
101
+    roots: Vec<RootString>,
102
+    roots_by_last_byte: HashMap<u8, Vec<usize>>,
102103
     offsets: HashMap<String, u32>,
103104
 }
104105
 
106
+#[derive(Debug, Clone, PartialEq, Eq)]
107
+struct RootString {
108
+    name: String,
109
+    offset: u32,
110
+}
111
+
105112
 impl StringTableBuilder {
106113
     pub fn new() -> Self {
107114
         Self::default()
@@ -125,7 +132,17 @@ impl StringTableBuilder {
125132
             let offset = raw.len() as u32;
126133
             raw.extend_from_slice(name.as_bytes());
127134
             raw.push(0);
128
-            self.roots.push((name.clone(), offset));
135
+            let root_index = self.roots.len();
136
+            self.roots.push(RootString {
137
+                name: name.clone(),
138
+                offset,
139
+            });
140
+            if let Some(&last_byte) = name.as_bytes().last() {
141
+                self.roots_by_last_byte
142
+                    .entry(last_byte)
143
+                    .or_default()
144
+                    .push(root_index);
145
+            }
129146
             self.offsets.insert(name, offset);
130147
         }
131148
 
@@ -136,13 +153,15 @@ impl StringTableBuilder {
136153
     }
137154
 
138155
     fn find_suffix_offset(&self, name: &str) -> Option<u32> {
139
-        self.roots.iter().find_map(|(existing, offset)| {
140
-            if existing.ends_with(name) {
141
-                Some(*offset + (existing.len() - name.len()) as u32)
142
-            } else {
143
-                None
144
-            }
145
-        })
156
+        let last_byte = *name.as_bytes().last()?;
157
+        self.roots_by_last_byte
158
+            .get(&last_byte)?
159
+            .iter()
160
+            .find_map(|&idx| {
161
+                let existing = &self.roots[idx];
162
+                (existing.name.len() >= name.len() && existing.name.ends_with(name))
163
+                    .then(|| existing.offset + (existing.name.len() - name.len()) as u32)
164
+            })
146165
     }
147166
 }
148167
 
@@ -253,4 +272,17 @@ mod tests {
253272
         assert_eq!(array, afs + 4);
254273
         assert_eq!(table.as_bytes().len() % 8, 0);
255274
     }
275
+
276
+    #[test]
277
+    fn builder_ignores_same_last_byte_non_suffix_names() {
278
+        let mut builder = StringTableBuilder::new();
279
+        builder.insert("_alpha");
280
+        builder.insert("_beta");
281
+
282
+        let (bytes, offsets) = builder.finish();
283
+        let table = StringTable::from_bytes(bytes);
284
+
285
+        assert_eq!(table.get(offsets["_alpha"]).unwrap(), "_alpha");
286
+        assert_eq!(table.get(offsets["_beta"]).unwrap(), "_beta");
287
+    }
256288
 }
src/synth/dyld_info.rsmodified
77 lines changed — click to load
@@ -307,12 +307,18 @@ pub fn emit_bind_records(specs: &[BindRecordSpec<'_>]) -> Vec<u8> {
307307
         let run_len = bind_run_len(specs, idx);
308308
         if run_len > 1 {
309309
             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;
310
+            if run_len == 2 && stride == 8 {
311
+                out.byte(BIND_OPCODE_DO_BIND);
312
+                state.next_segment_offset = Some(spec.segment_offset + 8);
313
+                idx += 1;
314
+            } else {
315
+                let skip = stride - 8;
316
+                out.byte(BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB);
317
+                out.uleb(run_len as u64);
318
+                out.uleb(skip);
319
+                state.next_segment_offset = Some(spec.segment_offset + (run_len as u64) * stride);
320
+                idx += run_len;
321
+            }
316322
         } else {
317323
             out.byte(BIND_OPCODE_DO_BIND);
318324
             state.next_segment_offset = Some(spec.segment_offset + 8);
@@ -620,4 +626,53 @@ mod tests {
620626
         assert_eq!(stream[idx + 2], 0x10);
621627
         assert_eq!(stream.last().copied(), Some(0));
622628
     }
629
+
630
+    #[test]
631
+    fn bind_encoder_keeps_adjacent_pair_uncompressed_for_apple_parity() {
632
+        let stream = emit_bind_records(&[
633
+            BindRecordSpec {
634
+                segment_index: 2,
635
+                segment_offset: 0,
636
+                ordinal: 2,
637
+                name: "_ext_data",
638
+                weak_import: false,
639
+                addend: 0,
640
+                terminate: false,
641
+            },
642
+            BindRecordSpec {
643
+                segment_index: 2,
644
+                segment_offset: 8,
645
+                ordinal: 2,
646
+                name: "_ext_data",
647
+                weak_import: false,
648
+                addend: 0,
649
+                terminate: true,
650
+            },
651
+        ]);
652
+
653
+        assert!(!stream.contains(&BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB));
654
+        assert_eq!(
655
+            stream,
656
+            vec![
657
+                BIND_OPCODE_SET_DYLIB_ORDINAL_IMM | 2,
658
+                BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM,
659
+                b'_',
660
+                b'e',
661
+                b'x',
662
+                b't',
663
+                b'_',
664
+                b'd',
665
+                b'a',
666
+                b't',
667
+                b'a',
668
+                0,
669
+                BIND_OPCODE_SET_TYPE_IMM | BIND_TYPE_POINTER,
670
+                BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB | 2,
671
+                0,
672
+                BIND_OPCODE_DO_BIND,
673
+                BIND_OPCODE_DO_BIND,
674
+                0,
675
+            ]
676
+        );
677
+    }
623678
 }
src/synth/mod.rsmodified
117 lines changed — click to load
@@ -5,7 +5,7 @@ pub mod stubs;
55
 pub mod tlv;
66
 pub mod unwind;
77
 
8
-use std::collections::HashMap;
8
+use std::collections::{HashMap, HashSet};
99
 use std::fmt;
1010
 use std::path::PathBuf;
1111
 
@@ -82,6 +82,16 @@ impl SyntheticPlan {
8282
         atoms: &AtomTable,
8383
         sym_table: &mut SymbolTable,
8484
         dylibs: &[DylibInput],
85
+    ) -> Result<Self, SynthError> {
86
+        Self::build_filtered(inputs, atoms, sym_table, dylibs, None)
87
+    }
88
+
89
+    pub fn build_filtered(
90
+        inputs: &[LayoutInput<'_>],
91
+        atoms: &AtomTable,
92
+        sym_table: &mut SymbolTable,
93
+        dylibs: &[DylibInput],
94
+        live_atoms: Option<&HashSet<AtomId>>,
8595
     ) -> Result<Self, SynthError> {
8696
         let input_map: HashMap<InputId, &ObjectFile> = inputs
8797
             .iter()
@@ -121,6 +131,9 @@ impl SyntheticPlan {
121131
         let mut direct_binds = Vec::new();
122132
 
123133
         for (atom_id, atom) in atoms.iter() {
134
+            if live_atoms.is_some_and(|live_atoms| !live_atoms.contains(&atom_id)) {
135
+                continue;
136
+            }
124137
             let obj = input_map.get(&atom.origin).ok_or_else(|| SynthError {
125138
                 input: PathBuf::from("<missing object>"),
126139
                 atom: atom_id,
@@ -166,29 +179,23 @@ impl SyntheticPlan {
166179
                     RelocKind::GotLoadPage21
167180
                     | RelocKind::GotLoadPageOff12
168181
                     | RelocKind::PointerToGot => {
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
-                        }
181182
                         let Some(symbol_id) = symbol_referent_id(obj, reloc.referent, sym_table)
182183
                         else {
183184
                             continue;
184185
                         };
185
-                        if matches!(reloc.kind, RelocKind::PointerToGot)
186
-                            && matches!(sym_table.get(symbol_id), Symbol::DylibImport { .. })
187
-                        {
186
+                        let needs_slot = match reloc.kind {
187
+                            RelocKind::PointerToGot => {
188
+                                !matches!(sym_table.get(symbol_id), Symbol::LazyArchive { .. })
189
+                            }
190
+                            RelocKind::GotLoadPage21 | RelocKind::GotLoadPageOff12 => {
191
+                                got_page_symbol_needs_slot(sym_table, atoms, symbol_id)
192
+                            }
193
+                            _ => false,
194
+                        };
195
+                        if needs_slot {
188196
                             got.intern(symbol_id, dylib_import_is_weak(sym_table, symbol_id));
189197
                             continue;
190198
                         }
191
-                        got.intern(symbol_id, dylib_import_is_weak(sym_table, symbol_id));
192199
                     }
193200
                     RelocKind::Branch26 => {
194201
                         let Some(symbol_id) = dylib_import_referent(obj, reloc.referent, sym_table)
@@ -490,6 +497,24 @@ fn tlv_symbol_needs_got(sym_table: &SymbolTable, symbol_id: SymbolId) -> bool {
490497
     matches!(sym_table.get(symbol_id), Symbol::DylibImport { .. })
491498
 }
492499
 
500
+fn got_page_symbol_needs_slot(
501
+    sym_table: &SymbolTable,
502
+    atoms: &AtomTable,
503
+    symbol_id: SymbolId,
504
+) -> bool {
505
+    match sym_table.get(symbol_id) {
506
+        Symbol::DylibImport { .. } => true,
507
+        Symbol::Defined {
508
+            atom,
509
+            private_extern,
510
+            ..
511
+        } => {
512
+            atom.0 != 0 && !*private_extern && matches!(atoms.get(*atom).section, AtomSection::Data)
513
+        }
514
+        _ => false,
515
+    }
516
+}
517
+
493518
 fn direct_import_bind_supported(reloc: Reloc) -> bool {
494519
     matches!(reloc.length, RelocLength::Quad) && !reloc.pcrel && reloc.subtrahend.is_none()
495520
 }
@@ -1178,6 +1203,7 @@ mod tests {
11781203
             strings: StringTable::from_bytes(strings),
11791204
             symtab: None,
11801205
             dysymtab: None,
1206
+            loh: Vec::new(),
11811207
             data_in_code: Vec::new(),
11821208
         }
11831209
     }
@@ -1263,6 +1289,7 @@ mod tests {
12631289
             strings: StringTable::from_bytes(strings),
12641290
             symtab: None,
12651291
             dysymtab: None,
1292
+            loh: Vec::new(),
12661293
             data_in_code: Vec::new(),
12671294
         }
12681295
     }
@@ -1312,6 +1339,7 @@ mod tests {
13121339
             strings: StringTable::from_bytes(strings),
13131340
             symtab: None,
13141341
             dysymtab: None,
1342
+            loh: Vec::new(),
13151343
             data_in_code: Vec::new(),
13161344
         }
13171345
     }
src/synth/unwind.rsmodified
103 lines changed — click to load
@@ -1,4 +1,4 @@
1
-use std::collections::HashMap;
1
+use std::collections::{HashMap, HashSet};
22
 use std::fmt;
33
 use std::path::PathBuf;
44
 
@@ -158,11 +158,13 @@ pub fn synthesize(
158158
         atom: AtomId(0),
159159
         detail: err.to_string(),
160160
     })?;
161
-    validate_serialized_unwind_info(&bytes, &records).map_err(|err| UnwindError {
162
-        input: PathBuf::from("<synthetic unwind>"),
163
-        atom: AtomId(0),
164
-        detail: err.to_string(),
165
-    })?;
161
+    if should_validate_serialized_unwind_info() {
162
+        validate_serialized_unwind_info(&bytes, &records).map_err(|err| UnwindError {
163
+            input: PathBuf::from("<synthetic unwind>"),
164
+            atom: AtomId(0),
165
+            detail: err.to_string(),
166
+        })?;
167
+    }
166168
     let section_changed = upsert_unwind_info_section(layout, bytes);
167169
     if changed || section_changed {
168170
         prune_empty_segments(layout);
@@ -170,6 +172,10 @@ pub fn synthesize(
170172
     Ok(changed || section_changed)
171173
 }
172174
 
175
+fn should_validate_serialized_unwind_info() -> bool {
176
+    std::env::var_os("AFS_LD_VALIDATE_UNWIND_INFO").is_some()
177
+}
178
+
173179
 fn collect_records(
174180
     layout: &Layout,
175181
     inputs: &[LayoutInput<'_>],
@@ -185,26 +191,42 @@ fn collect_records(
185191
         .iter()
186192
         .map(|input| (input.id, input.object))
187193
         .collect();
194
+    let compact_unwind_sections: HashSet<(InputId, u8)> = atoms
195
+        .iter()
196
+        .filter(|(_, atom)| atom.section == AtomSection::CompactUnwind)
197
+        .map(|(_, atom)| (atom.origin, atom.input_section))
198
+        .collect();
188199
     let mut reloc_cache: HashMap<(InputId, u8), Vec<Reloc>> = HashMap::new();
189
-    for input in inputs {
190
-        for (section_idx, section) in input.object.sections.iter().enumerate() {
191
-            if section.nreloc == 0 {
192
-                continue;
193
-            }
194
-            let raws = parse_raw_relocs(&section.raw_relocs, 0, section.nreloc).map_err(|err| {
195
-                UnwindError {
196
-                    input: input.object.path.clone(),
197
-                    atom: AtomId(0),
198
-                    detail: err.to_string(),
199
-                }
200
-            })?;
201
-            let relocs = parse_relocs(&raws).map_err(|err| UnwindError {
202
-                input: input.object.path.clone(),
200
+    for (input_id, section_idx) in compact_unwind_sections {
201
+        let obj = input_map.get(&input_id).ok_or_else(|| UnwindError {
202
+            input: PathBuf::from("<missing object>"),
203
+            atom: AtomId(0),
204
+            detail: "missing parsed object".to_string(),
205
+        })?;
206
+        let section = obj
207
+            .sections
208
+            .get((section_idx as usize).saturating_sub(1))
209
+            .ok_or_else(|| UnwindError {
210
+                input: obj.path.clone(),
203211
                 atom: AtomId(0),
204
-                detail: err.to_string(),
212
+                detail: format!("compact-unwind section {} is out of range", section_idx),
205213
             })?;
206
-            reloc_cache.insert((input.id, (section_idx + 1) as u8), relocs);
214
+        if section.nreloc == 0 {
215
+            continue;
207216
         }
217
+        let raws = parse_raw_relocs(&section.raw_relocs, 0, section.nreloc).map_err(|err| {
218
+            UnwindError {
219
+                input: obj.path.clone(),
220
+                atom: AtomId(0),
221
+                detail: err.to_string(),
222
+            }
223
+        })?;
224
+        let relocs = parse_relocs(&raws).map_err(|err| UnwindError {
225
+            input: obj.path.clone(),
226
+            atom: AtomId(0),
227
+            detail: err.to_string(),
228
+        })?;
229
+        reloc_cache.insert((input_id, section_idx), relocs);
208230
     }
209231
 
210232
     let mut records = Vec::new();
@@ -212,6 +234,12 @@ fn collect_records(
212234
         if atom.section != AtomSection::CompactUnwind {
213235
             continue;
214236
         }
237
+        if atom
238
+            .parent_of
239
+            .is_some_and(|parent| layout.atom_addr(parent).is_none())
240
+        {
241
+            continue;
242
+        }
215243
         let Some(obj) = input_map.get(&atom.origin) else {
216244
             return Err(UnwindError {
217245
                 input: PathBuf::from("<missing object>"),
src/why_live.rsadded
838 lines changed — click to load
@@ -0,0 +1,838 @@
1
+use std::collections::{HashMap, HashSet, VecDeque};
2
+use std::fmt::Write as _;
3
+
4
+use crate::atom::{Atom, AtomSection, AtomTable};
5
+use crate::icf::FoldedSymbol;
6
+use crate::input::ObjectFile;
7
+use crate::layout::LayoutInput;
8
+use crate::reloc::{parse_raw_relocs, parse_relocs, Referent};
9
+use crate::resolve::{AtomId, InputId, Symbol, SymbolId, SymbolTable};
10
+use crate::{LinkOptions, OutputKind};
11
+
12
+#[derive(Debug, Clone)]
13
+enum RootReason {
14
+    Entry(String),
15
+    NoDeadStrip,
16
+    ExportedDylib,
17
+}
18
+
19
+impl RootReason {
20
+    fn describe(&self, symbol: &str) -> String {
21
+        match self {
22
+            RootReason::Entry(entry) => format!("{symbol} is in -e {entry} (GC root)"),
23
+            RootReason::NoDeadStrip => format!("{symbol} is marked N_NO_DEAD_STRIP (GC root)"),
24
+            RootReason::ExportedDylib => format!("{symbol} is exported from the dylib (GC root)"),
25
+        }
26
+    }
27
+}
28
+
29
+#[derive(Debug, Clone)]
30
+enum LiveCause {
31
+    Root(RootReason),
32
+    ReferencedBy(AtomId),
33
+    ParentOf(AtomId),
34
+}
35
+
36
+#[derive(Debug, Clone, PartialEq, Eq)]
37
+pub struct DeadStrippedSymbol {
38
+    pub name: String,
39
+    pub file_index: usize,
40
+}
41
+
42
+#[derive(Debug, Clone)]
43
+pub struct DeadStripAnalysis {
44
+    live_atoms: HashSet<AtomId>,
45
+    causes: HashMap<AtomId, LiveCause>,
46
+    resolved_by_name: HashMap<String, SymbolId>,
47
+    atom_symbols: HashMap<AtomId, Vec<SymbolId>>,
48
+}
49
+
50
+impl DeadStripAnalysis {
51
+    pub fn build(
52
+        opts: &LinkOptions,
53
+        layout_inputs: &[LayoutInput<'_>],
54
+        atom_table: &AtomTable,
55
+        sym_table: &SymbolTable,
56
+        entry_symbol: Option<SymbolId>,
57
+    ) -> Self {
58
+        let resolved_by_name = resolved_symbol_map(sym_table);
59
+        let atom_symbols = atom_symbol_sets(atom_table);
60
+        let roots = root_atoms(opts, atom_table, sym_table, entry_symbol);
61
+        let forward_edges =
62
+            build_forward_edges(layout_inputs, atom_table, sym_table, &resolved_by_name);
63
+        let parent_edges = parent_edges(atom_table);
64
+
65
+        let mut live_atoms = HashSet::new();
66
+        let mut causes = HashMap::new();
67
+        let mut worklist = VecDeque::new();
68
+        for (atom_id, reason) in roots {
69
+            if live_atoms.insert(atom_id) {
70
+                causes.insert(atom_id, LiveCause::Root(reason));
71
+                worklist.push_back(atom_id);
72
+            }
73
+        }
74
+
75
+        while let Some(atom_id) = worklist.pop_front() {
76
+            if let Some(children) = parent_edges.get(&atom_id) {
77
+                for &child in children {
78
+                    if live_atoms.insert(child) {
79
+                        causes.insert(child, LiveCause::ParentOf(atom_id));
80
+                        worklist.push_back(child);
81
+                    }
82
+                }
83
+            }
84
+            if let Some(targets) = forward_edges.get(&atom_id) {
85
+                for &target in targets {
86
+                    if live_atoms.insert(target) {
87
+                        causes.insert(target, LiveCause::ReferencedBy(atom_id));
88
+                        worklist.push_back(target);
89
+                    }
90
+                }
91
+            }
92
+        }
93
+
94
+        Self {
95
+            live_atoms,
96
+            causes,
97
+            resolved_by_name,
98
+            atom_symbols,
99
+        }
100
+    }
101
+
102
+    pub fn live_atoms(&self) -> &HashSet<AtomId> {
103
+        &self.live_atoms
104
+    }
105
+
106
+    pub fn dead_stripped_symbols(
107
+        &self,
108
+        atom_table: &AtomTable,
109
+        sym_table: &SymbolTable,
110
+        layout_inputs: &[LayoutInput<'_>],
111
+    ) -> Vec<DeadStrippedSymbol> {
112
+        let file_index_by_input: HashMap<InputId, usize> = layout_inputs
113
+            .iter()
114
+            .enumerate()
115
+            .map(|(idx, input)| (input.id, idx + 1))
116
+            .collect();
117
+        let mut out = Vec::new();
118
+        for (atom_id, _atom) in atom_table.iter() {
119
+            if self.live_atoms.contains(&atom_id) {
120
+                continue;
121
+            }
122
+            let Some(symbols) = self.atom_symbols.get(&atom_id) else {
123
+                continue;
124
+            };
125
+            for &symbol_id in symbols {
126
+                let Symbol::Defined { origin, .. } = sym_table.get(symbol_id) else {
127
+                    continue;
128
+                };
129
+                out.push(DeadStrippedSymbol {
130
+                    name: self.symbol_name(sym_table, symbol_id),
131
+                    file_index: file_index_by_input.get(origin).copied().unwrap_or(0),
132
+                });
133
+            }
134
+        }
135
+        out.sort_by(|lhs, rhs| {
136
+            lhs.name
137
+                .cmp(&rhs.name)
138
+                .then(lhs.file_index.cmp(&rhs.file_index))
139
+        });
140
+        out.dedup_by(|lhs, rhs| lhs.name == rhs.name && lhs.file_index == rhs.file_index);
141
+        out
142
+    }
143
+
144
+    fn symbol_name(&self, sym_table: &SymbolTable, symbol_id: SymbolId) -> String {
145
+        sym_table
146
+            .interner
147
+            .resolve(sym_table.get(symbol_id).name())
148
+            .to_string()
149
+    }
150
+
151
+    fn atom_name(&self, sym_table: &SymbolTable, atom_id: AtomId) -> String {
152
+        self.atom_symbols
153
+            .get(&atom_id)
154
+            .and_then(|symbols| symbols.first().copied())
155
+            .map(|symbol_id| self.symbol_name(sym_table, symbol_id))
156
+            .unwrap_or_else(|| format!("<atom {:?}>", atom_id))
157
+    }
158
+
159
+    fn is_live_symbol(&self, sym_table: &SymbolTable, symbol_id: SymbolId) -> bool {
160
+        match sym_table.get(symbol_id) {
161
+            Symbol::Defined { atom, .. } if atom.0 != 0 => self.live_atoms.contains(atom),
162
+            _ => false,
163
+        }
164
+    }
165
+
166
+    fn format_symbol_explanation(
167
+        &self,
168
+        sym_table: &SymbolTable,
169
+        requested_symbol: SymbolId,
170
+    ) -> String {
171
+        let requested_name = self.symbol_name(sym_table, requested_symbol);
172
+        if !self.is_live_symbol(sym_table, requested_symbol) {
173
+            return format!("{requested_name} is not live (dead-stripped)\n");
174
+        }
175
+
176
+        let Symbol::Defined { atom, .. } = sym_table.get(requested_symbol) else {
177
+            return format!("{requested_name} is not backed by a dead-strip eligible input atom\n");
178
+        };
179
+        let mut out = String::new();
180
+        writeln!(&mut out, "{requested_name} is live because:").unwrap();
181
+
182
+        let mut cursor = *atom;
183
+        let mut first = true;
184
+        loop {
185
+            let Some(cause) = self.causes.get(&cursor).cloned() else {
186
+                writeln!(
187
+                    &mut out,
188
+                    "  no reachability chain from a known GC root was found"
189
+                )
190
+                .unwrap();
191
+                break;
192
+            };
193
+            match cause {
194
+                LiveCause::Root(reason) => {
195
+                    let name = if first {
196
+                        requested_name.as_str()
197
+                    } else {
198
+                        &self.atom_name(sym_table, cursor)
199
+                    };
200
+                    writeln!(&mut out, "  {}", reason.describe(name)).unwrap();
201
+                    break;
202
+                }
203
+                LiveCause::ReferencedBy(parent) => {
204
+                    let child_name = if first {
205
+                        requested_name.as_str()
206
+                    } else {
207
+                        &self.atom_name(sym_table, cursor)
208
+                    };
209
+                    writeln!(
210
+                        &mut out,
211
+                        "  {child_name} is reachable from {}",
212
+                        self.atom_name(sym_table, parent)
213
+                    )
214
+                    .unwrap();
215
+                    cursor = parent;
216
+                }
217
+                LiveCause::ParentOf(parent) => {
218
+                    let child_name = if first {
219
+                        requested_name.as_str()
220
+                    } else {
221
+                        &self.atom_name(sym_table, cursor)
222
+                    };
223
+                    writeln!(
224
+                        &mut out,
225
+                        "  {child_name} is reachable via unwind parent from {}",
226
+                        self.atom_name(sym_table, parent)
227
+                    )
228
+                    .unwrap();
229
+                    cursor = parent;
230
+                }
231
+            }
232
+            first = false;
233
+        }
234
+
235
+        out
236
+    }
237
+}
238
+
239
+pub fn format_explanations(
240
+    opts: &LinkOptions,
241
+    layout_inputs: &[LayoutInput<'_>],
242
+    atom_table: &AtomTable,
243
+    sym_table: &SymbolTable,
244
+    entry_symbol: Option<SymbolId>,
245
+    dead_strip: Option<&DeadStripAnalysis>,
246
+    folded_symbols: &[FoldedSymbol],
247
+) -> Result<Option<String>, String> {
248
+    if opts.why_live.is_empty() {
249
+        return Ok(None);
250
+    }
251
+
252
+    let folded_by_name: HashMap<&str, &str> = folded_symbols
253
+        .iter()
254
+        .map(|symbol| (symbol.name.as_str(), symbol.winner.as_str()))
255
+        .collect();
256
+
257
+    if let Some(dead_strip) = dead_strip {
258
+        let mut out = String::new();
259
+        for (idx, requested) in opts.why_live.iter().enumerate() {
260
+            let winner = folded_by_name
261
+                .get(requested.as_str())
262
+                .copied()
263
+                .unwrap_or(requested.as_str());
264
+            let Some(&target) = dead_strip.resolved_by_name.get(winner) else {
265
+                return Err(format!("`-why_live` symbol `{requested}` was not found"));
266
+            };
267
+            if idx > 0 {
268
+                out.push('\n');
269
+            }
270
+            if winner != requested {
271
+                writeln!(&mut out, "{requested} was folded to {winner} by -icf=safe").unwrap();
272
+            }
273
+            out.push_str(&dead_strip.format_symbol_explanation(sym_table, target));
274
+        }
275
+        return Ok(Some(out));
276
+    }
277
+
278
+    let graph = WhyLiveGraph::build(opts, layout_inputs, atom_table, sym_table, entry_symbol);
279
+    let mut out = String::new();
280
+    for (idx, requested) in opts.why_live.iter().enumerate() {
281
+        let winner = folded_by_name
282
+            .get(requested.as_str())
283
+            .copied()
284
+            .unwrap_or(requested.as_str());
285
+        let Some(&target) = graph.resolved_by_name.get(winner) else {
286
+            return Err(format!("`-why_live` symbol `{requested}` was not found"));
287
+        };
288
+        if idx > 0 {
289
+            out.push('\n');
290
+        }
291
+        if winner != requested {
292
+            writeln!(&mut out, "{requested} was folded to {winner} by -icf=safe").unwrap();
293
+        }
294
+        let target_name = graph.symbol_name(target);
295
+        writeln!(&mut out, "{target_name} is live because:").unwrap();
296
+        writeln!(
297
+            &mut out,
298
+            "  note: -dead_strip was not requested; showing the current reachability roots"
299
+        )
300
+        .unwrap();
301
+        if let Some(reason) = graph.roots.get(&target) {
302
+            writeln!(&mut out, "  {}", reason.describe(&target_name)).unwrap();
303
+            continue;
304
+        }
305
+        if let Some(path) = graph.find_chain(target) {
306
+            for pair in path.windows(2).rev() {
307
+                let parent = graph.symbol_name(pair[0]);
308
+                let child = graph.symbol_name(pair[1]);
309
+                writeln!(&mut out, "  {child} is reachable from {parent}").unwrap();
310
+            }
311
+            let root = path[0];
312
+            let root_name = graph.symbol_name(root);
313
+            writeln!(
314
+                &mut out,
315
+                "  {}",
316
+                graph
317
+                    .roots
318
+                    .get(&root)
319
+                    .expect("root path starts from a root")
320
+                    .describe(&root_name)
321
+            )
322
+            .unwrap();
323
+        } else {
324
+            writeln!(
325
+                &mut out,
326
+                "  no reachability chain from a known GC root was found; linked inputs are currently retained as loaded"
327
+            )
328
+            .unwrap();
329
+        }
330
+    }
331
+    Ok(Some(out))
332
+}
333
+
334
+struct WhyLiveGraph<'a> {
335
+    sym_table: &'a SymbolTable,
336
+    resolved_by_name: HashMap<String, SymbolId>,
337
+    roots: HashMap<SymbolId, RootReason>,
338
+    reverse_edges: HashMap<SymbolId, Vec<SymbolId>>,
339
+}
340
+
341
+impl<'a> WhyLiveGraph<'a> {
342
+    fn build(
343
+        opts: &LinkOptions,
344
+        layout_inputs: &[LayoutInput<'_>],
345
+        atom_table: &AtomTable,
346
+        sym_table: &'a SymbolTable,
347
+        entry_symbol: Option<SymbolId>,
348
+    ) -> Self {
349
+        let resolved_by_name = resolved_symbol_map(sym_table);
350
+        let roots = root_symbols(opts, sym_table, entry_symbol);
351
+        let reverse_edges = build_reverse_edges(layout_inputs, atom_table, &resolved_by_name);
352
+        Self {
353
+            sym_table,
354
+            resolved_by_name,
355
+            roots,
356
+            reverse_edges,
357
+        }
358
+    }
359
+
360
+    fn symbol_name(&self, symbol_id: SymbolId) -> String {
361
+        self.sym_table
362
+            .interner
363
+            .resolve(self.sym_table.get(symbol_id).name())
364
+            .to_string()
365
+    }
366
+
367
+    fn find_chain(&self, target: SymbolId) -> Option<Vec<SymbolId>> {
368
+        let mut queue = VecDeque::from([target]);
369
+        let mut seen = HashSet::from([target]);
370
+        let mut next_toward_target = HashMap::<SymbolId, SymbolId>::new();
371
+
372
+        while let Some(current) = queue.pop_front() {
373
+            let Some(predecessors) = self.reverse_edges.get(&current) else {
374
+                continue;
375
+            };
376
+            for &pred in predecessors {
377
+                if !seen.insert(pred) {
378
+                    continue;
379
+                }
380
+                next_toward_target.insert(pred, current);
381
+                if self.roots.contains_key(&pred) {
382
+                    let mut path = vec![pred];
383
+                    let mut cursor = pred;
384
+                    while let Some(&next) = next_toward_target.get(&cursor) {
385
+                        path.push(next);
386
+                        if next == target {
387
+                            break;
388
+                        }
389
+                        cursor = next;
390
+                    }
391
+                    return Some(path);
392
+                }
393
+                queue.push_back(pred);
394
+            }
395
+        }
396
+
397
+        None
398
+    }
399
+}
400
+
401
+fn resolved_symbol_map(sym_table: &SymbolTable) -> HashMap<String, SymbolId> {
402
+    let mut out = HashMap::new();
403
+    for (symbol_id, symbol) in sym_table.iter() {
404
+        let name = sym_table.interner.resolve(symbol.name()).to_string();
405
+        let resolved = match symbol {
406
+            Symbol::Alias { name, .. } => sym_table
407
+                .resolve_chain(*name)
408
+                .map(|(resolved_id, _)| resolved_id)
409
+                .unwrap_or(symbol_id),
410
+            _ => symbol_id,
411
+        };
412
+        out.insert(name, resolved);
413
+    }
414
+    out
415
+}
416
+
417
+fn root_symbols(
418
+    opts: &LinkOptions,
419
+    sym_table: &SymbolTable,
420
+    entry_symbol: Option<SymbolId>,
421
+) -> HashMap<SymbolId, RootReason> {
422
+    let mut roots = HashMap::new();
423
+    if let Some(entry_symbol) = entry_symbol {
424
+        roots.insert(
425
+            entry_symbol,
426
+            RootReason::Entry(symbol_name(sym_table, entry_symbol)),
427
+        );
428
+    }
429
+    for (symbol_id, symbol) in sym_table.iter() {
430
+        match symbol {
431
+            Symbol::Defined {
432
+                no_dead_strip: true,
433
+                ..
434
+            } => {
435
+                roots.entry(symbol_id).or_insert(RootReason::NoDeadStrip);
436
+            }
437
+            Symbol::Defined {
438
+                private_extern: false,
439
+                ..
440
+            } if opts.kind == OutputKind::Dylib => {
441
+                roots.entry(symbol_id).or_insert(RootReason::ExportedDylib);
442
+            }
443
+            _ => {}
444
+        }
445
+    }
446
+    roots
447
+}
448
+
449
+fn root_atoms(
450
+    opts: &LinkOptions,
451
+    atom_table: &AtomTable,
452
+    sym_table: &SymbolTable,
453
+    entry_symbol: Option<SymbolId>,
454
+) -> HashMap<AtomId, RootReason> {
455
+    let mut roots = HashMap::new();
456
+    if let Some(entry_symbol) = entry_symbol {
457
+        if let Symbol::Defined { atom, .. } = sym_table.get(entry_symbol) {
458
+            if atom.0 != 0 {
459
+                roots.insert(
460
+                    *atom,
461
+                    RootReason::Entry(symbol_name(sym_table, entry_symbol)),
462
+                );
463
+            }
464
+        }
465
+    }
466
+
467
+    for (atom_id, atom) in atom_table.iter() {
468
+        if atom.flags.has(crate::atom::AtomFlags::NO_DEAD_STRIP) {
469
+            roots.entry(atom_id).or_insert(RootReason::NoDeadStrip);
470
+        }
471
+    }
472
+
473
+    for (_, symbol) in sym_table.iter() {
474
+        match symbol {
475
+            Symbol::Defined {
476
+                atom,
477
+                no_dead_strip: true,
478
+                ..
479
+            } if atom.0 != 0 => {
480
+                roots.entry(*atom).or_insert(RootReason::NoDeadStrip);
481
+            }
482
+            Symbol::Defined {
483
+                atom,
484
+                private_extern: false,
485
+                ..
486
+            } if opts.kind == OutputKind::Dylib && atom.0 != 0 => {
487
+                roots.entry(*atom).or_insert(RootReason::ExportedDylib);
488
+            }
489
+            _ => {}
490
+        }
491
+    }
492
+
493
+    roots
494
+}
495
+
496
+fn build_reverse_edges(
497
+    layout_inputs: &[LayoutInput<'_>],
498
+    atom_table: &AtomTable,
499
+    resolved_by_name: &HashMap<String, SymbolId>,
500
+) -> HashMap<SymbolId, Vec<SymbolId>> {
501
+    let atoms_by_input_section = atom_table.by_input_section();
502
+    let atom_symbols = atom_symbol_sets(atom_table);
503
+    let mut edge_set = HashSet::<(SymbolId, SymbolId)>::new();
504
+
505
+    for input in layout_inputs {
506
+        for (section_idx_zero, section) in input.object.sections.iter().enumerate() {
507
+            if section.raw_relocs.is_empty() {
508
+                continue;
509
+            }
510
+            let Ok(raws) = parse_raw_relocs(&section.raw_relocs, 0, section.nreloc) else {
511
+                continue;
512
+            };
513
+            let Ok(relocs) = parse_relocs(&raws) else {
514
+                continue;
515
+            };
516
+            let input_section = (section_idx_zero + 1) as u8;
517
+            for reloc in relocs {
518
+                let Some(source_atom) = find_atom_for_offset(
519
+                    atom_table,
520
+                    &atoms_by_input_section,
521
+                    input.id,
522
+                    input_section,
523
+                    reloc.offset,
524
+                ) else {
525
+                    continue;
526
+                };
527
+                let Some(source_symbols) = atom_symbols.get(&source_atom) else {
528
+                    continue;
529
+                };
530
+                let Some(target_symbols) =
531
+                    target_symbols_for_reloc(input.object, reloc.referent, resolved_by_name)
532
+                else {
533
+                    continue;
534
+                };
535
+                for &source in source_symbols {
536
+                    for &target in &target_symbols {
537
+                        if source != target {
538
+                            edge_set.insert((target, source));
539
+                        }
540
+                    }
541
+                }
542
+            }
543
+        }
544
+    }
545
+
546
+    let mut reverse_edges = HashMap::<SymbolId, Vec<SymbolId>>::new();
547
+    for (target, source) in edge_set {
548
+        reverse_edges.entry(target).or_default().push(source);
549
+    }
550
+    for predecessors in reverse_edges.values_mut() {
551
+        predecessors.sort_by_key(|sid| sid.0);
552
+    }
553
+    reverse_edges
554
+}
555
+
556
+fn build_forward_edges(
557
+    layout_inputs: &[LayoutInput<'_>],
558
+    atom_table: &AtomTable,
559
+    sym_table: &SymbolTable,
560
+    resolved_by_name: &HashMap<String, SymbolId>,
561
+) -> HashMap<AtomId, Vec<AtomId>> {
562
+    let atoms_by_input_section = atom_table.by_input_section();
563
+    let mut edge_set = HashSet::<(AtomId, AtomId)>::new();
564
+
565
+    for input in layout_inputs {
566
+        for (section_idx_zero, section) in input.object.sections.iter().enumerate() {
567
+            if section.raw_relocs.is_empty() {
568
+                continue;
569
+            }
570
+            let Ok(raws) = parse_raw_relocs(&section.raw_relocs, 0, section.nreloc) else {
571
+                continue;
572
+            };
573
+            let Ok(relocs) = parse_relocs(&raws) else {
574
+                continue;
575
+            };
576
+            let input_section = (section_idx_zero + 1) as u8;
577
+            for reloc in relocs {
578
+                let Some(source_atom) = find_atom_for_offset(
579
+                    atom_table,
580
+                    &atoms_by_input_section,
581
+                    input.id,
582
+                    input_section,
583
+                    reloc.offset,
584
+                ) else {
585
+                    continue;
586
+                };
587
+                for target_atom in target_atoms_for_reloc(
588
+                    input.id,
589
+                    input.object,
590
+                    atom_table.get(source_atom),
591
+                    reloc,
592
+                    reloc.referent,
593
+                    reloc.subtrahend,
594
+                    atom_table,
595
+                    sym_table,
596
+                    resolved_by_name,
597
+                    &atoms_by_input_section,
598
+                ) {
599
+                    if source_atom != target_atom {
600
+                        edge_set.insert((source_atom, target_atom));
601
+                    }
602
+                }
603
+            }
604
+        }
605
+    }
606
+
607
+    for (atom_id, atom) in atom_table.iter() {
608
+        if atom.section != AtomSection::EhFrame {
609
+            continue;
610
+        }
611
+        let Some(cie_atom) = eh_frame_cie_atom(atom_table, &atoms_by_input_section, atom) else {
612
+            continue;
613
+        };
614
+        if atom_id != cie_atom {
615
+            edge_set.insert((atom_id, cie_atom));
616
+        }
617
+    }
618
+
619
+    let mut forward_edges = HashMap::<AtomId, Vec<AtomId>>::new();
620
+    for (source, target) in edge_set {
621
+        forward_edges.entry(source).or_default().push(target);
622
+    }
623
+    for targets in forward_edges.values_mut() {
624
+        targets.sort_by_key(|aid| aid.0);
625
+    }
626
+    forward_edges
627
+}
628
+
629
+fn parent_edges(atom_table: &AtomTable) -> HashMap<AtomId, Vec<AtomId>> {
630
+    let mut out = HashMap::<AtomId, Vec<AtomId>>::new();
631
+    for (atom_id, atom) in atom_table.iter() {
632
+        if let Some(parent) = atom.parent_of {
633
+            out.entry(parent).or_default().push(atom_id);
634
+        }
635
+    }
636
+    for children in out.values_mut() {
637
+        children.sort_by_key(|aid| aid.0);
638
+    }
639
+    out
640
+}
641
+
642
+fn atom_symbol_sets(atom_table: &AtomTable) -> HashMap<crate::resolve::AtomId, Vec<SymbolId>> {
643
+    let mut out = HashMap::new();
644
+    for (atom_id, atom) in atom_table.iter() {
645
+        let mut symbols = Vec::new();
646
+        if let Some(owner) = atom.owner {
647
+            symbols.push(owner);
648
+        }
649
+        for alt in &atom.alt_entries {
650
+            symbols.push(alt.symbol);
651
+        }
652
+        symbols.sort_by_key(|sid| sid.0);
653
+        symbols.dedup();
654
+        if !symbols.is_empty() {
655
+            out.insert(atom_id, symbols);
656
+        }
657
+    }
658
+    out
659
+}
660
+
661
+fn find_atom_for_offset(
662
+    atom_table: &AtomTable,
663
+    atoms_by_input_section: &HashMap<(InputId, u8), Vec<AtomId>>,
664
+    input_id: InputId,
665
+    input_section: u8,
666
+    offset: u32,
667
+) -> Option<AtomId> {
668
+    atoms_by_input_section
669
+        .get(&(input_id, input_section))
670
+        .and_then(|ids| {
671
+            ids.iter().find_map(|atom_id| {
672
+                let atom = atom_table.get(*atom_id);
673
+                let start = atom.input_offset;
674
+                let end = atom.input_offset.saturating_add(atom.size);
675
+                (start <= offset && offset < end).then_some(*atom_id)
676
+            })
677
+        })
678
+}
679
+
680
+#[allow(clippy::too_many_arguments)]
681
+fn target_atoms_for_reloc(
682
+    input_id: InputId,
683
+    object: &ObjectFile,
684
+    source_atom: &Atom,
685
+    reloc: crate::reloc::Reloc,
686
+    referent: Referent,
687
+    subtrahend: Option<Referent>,
688
+    atom_table: &AtomTable,
689
+    sym_table: &SymbolTable,
690
+    resolved_by_name: &HashMap<String, SymbolId>,
691
+    atoms_by_input_section: &HashMap<(InputId, u8), Vec<AtomId>>,
692
+) -> Vec<AtomId> {
693
+    let mut out = referent_atoms(
694
+        input_id,
695
+        object,
696
+        source_atom,
697
+        reloc,
698
+        referent,
699
+        atom_table,
700
+        sym_table,
701
+        resolved_by_name,
702
+        atoms_by_input_section,
703
+    );
704
+    if let Some(subtrahend) = subtrahend {
705
+        out.extend(referent_atoms(
706
+            input_id,
707
+            object,
708
+            source_atom,
709
+            reloc,
710
+            subtrahend,
711
+            atom_table,
712
+            sym_table,
713
+            resolved_by_name,
714
+            atoms_by_input_section,
715
+        ));
716
+    }
717
+    out.sort_by_key(|aid| aid.0);
718
+    out.dedup();
719
+    out
720
+}
721
+
722
+#[allow(clippy::too_many_arguments)]
723
+fn referent_atoms(
724
+    input_id: InputId,
725
+    object: &ObjectFile,
726
+    source_atom: &Atom,
727
+    reloc: crate::reloc::Reloc,
728
+    referent: Referent,
729
+    atom_table: &AtomTable,
730
+    sym_table: &SymbolTable,
731
+    resolved_by_name: &HashMap<String, SymbolId>,
732
+    atoms_by_input_section: &HashMap<(InputId, u8), Vec<AtomId>>,
733
+) -> Vec<AtomId> {
734
+    match referent {
735
+        Referent::Symbol(symbol_index) => {
736
+            let Some(input_sym) = object.symbols.get(symbol_index as usize) else {
737
+                return Vec::new();
738
+            };
739
+            let Some(name) = object.symbol_name(input_sym).ok() else {
740
+                return Vec::new();
741
+            };
742
+            let Some(&symbol_id) = resolved_by_name.get(name) else {
743
+                return Vec::new();
744
+            };
745
+            match sym_table.get(symbol_id) {
746
+                Symbol::Defined { atom, .. } if atom.0 != 0 => vec![*atom],
747
+                _ => Vec::new(),
748
+            }
749
+        }
750
+        Referent::Section(section_index) => {
751
+            if let Some(atom_id) = section_referent_atom(
752
+                input_id,
753
+                source_atom,
754
+                reloc,
755
+                section_index,
756
+                atom_table,
757
+                atoms_by_input_section,
758
+            ) {
759
+                vec![atom_id]
760
+            } else {
761
+                atoms_by_input_section
762
+                    .get(&(input_id, section_index))
763
+                    .cloned()
764
+                    .unwrap_or_default()
765
+            }
766
+        }
767
+    }
768
+}
769
+
770
+fn section_referent_atom(
771
+    input_id: InputId,
772
+    source_atom: &Atom,
773
+    reloc: crate::reloc::Reloc,
774
+    section_index: u8,
775
+    atom_table: &AtomTable,
776
+    atoms_by_input_section: &HashMap<(InputId, u8), Vec<AtomId>>,
777
+) -> Option<AtomId> {
778
+    if source_atom.section == AtomSection::CompactUnwind
779
+        && reloc.offset == source_atom.input_offset
780
+        && source_atom.data.len() >= 8
781
+    {
782
+        let mut buf = [0u8; 8];
783
+        buf.copy_from_slice(&source_atom.data[..8]);
784
+        let target_offset = u64::from_le_bytes(buf) as u32;
785
+        return find_atom_for_offset(
786
+            atom_table,
787
+            atoms_by_input_section,
788
+            input_id,
789
+            section_index,
790
+            target_offset,
791
+        );
792
+    }
793
+    None
794
+}
795
+
796
+fn eh_frame_cie_atom(
797
+    atom_table: &AtomTable,
798
+    atoms_by_input_section: &HashMap<(InputId, u8), Vec<AtomId>>,
799
+    atom: &Atom,
800
+) -> Option<AtomId> {
801
+    if atom.section != AtomSection::EhFrame || atom.data.len() < 8 {
802
+        return None;
803
+    }
804
+    let mut buf = [0u8; 4];
805
+    buf.copy_from_slice(&atom.data[4..8]);
806
+    let cie_delta = u32::from_le_bytes(buf);
807
+    if cie_delta == 0 {
808
+        return None;
809
+    }
810
+    let cie_offset = atom.input_offset.checked_add(4)?.checked_sub(cie_delta)?;
811
+    find_atom_for_offset(
812
+        atom_table,
813
+        atoms_by_input_section,
814
+        atom.origin,
815
+        atom.input_section,
816
+        cie_offset,
817
+    )
818
+}
819
+
820
+fn target_symbols_for_reloc(
821
+    object: &ObjectFile,
822
+    referent: Referent,
823
+    resolved_by_name: &HashMap<String, SymbolId>,
824
+) -> Option<Vec<SymbolId>> {
825
+    let Referent::Symbol(symbol_index) = referent else {
826
+        return None;
827
+    };
828
+    let input_sym = object.symbols.get(symbol_index as usize)?;
829
+    let name = object.symbol_name(input_sym).ok()?;
830
+    resolved_by_name.get(name).copied().map(|sid| vec![sid])
831
+}
832
+
833
+fn symbol_name(sym_table: &SymbolTable, symbol_id: SymbolId) -> String {
834
+    sym_table
835
+        .interner
836
+        .resolve(sym_table.get(symbol_id).name())
837
+        .to_string()
838
+}
tests/cli_diagnostics.rsmodified
927 lines changed — click to load
@@ -2,6 +2,11 @@ use std::fs;
22
 use std::path::PathBuf;
33
 use std::process::Command;
44
 
5
+use afs_ld::macho::constants::LC_UUID;
6
+use afs_ld::macho::reader::{parse_commands, parse_header, LoadCommand};
7
+
8
+const EXPECTED_HELP: &str = include_str!("snapshots/help.txt");
9
+
510
 fn have_xcrun() -> bool {
611
     Command::new("xcrun")
712
         .arg("-f")
@@ -11,6 +16,28 @@ fn have_xcrun() -> bool {
1116
         .unwrap_or(false)
1217
 }
1318
 
19
+fn sdk_path() -> Option<String> {
20
+    let out = Command::new("xcrun")
21
+        .args(["--sdk", "macosx", "--show-sdk-path"])
22
+        .output()
23
+        .ok()?;
24
+    if !out.status.success() {
25
+        return None;
26
+    }
27
+    Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
28
+}
29
+
30
+fn minimal_main_src() -> &'static str {
31
+    r#"
32
+        .section __TEXT,__text,regular,pure_instructions
33
+        .globl _main
34
+        _main:
35
+            mov w0, #0
36
+            ret
37
+        .subsections_via_symbols
38
+    "#
39
+}
40
+
1441
 fn assemble(src: &str, out: &PathBuf) -> Result<(), String> {
1542
     let tmp = std::env::temp_dir().join(format!(
1643
         "afs-ld-cli-diag-{}-{}.s",
@@ -39,6 +66,511 @@ fn scratch(name: &str) -> PathBuf {
3966
     std::env::temp_dir().join(format!("afs-ld-cli-diag-{}-{name}", std::process::id()))
4067
 }
4168
 
69
+fn assemble_minimal_main(name: &str) -> Result<PathBuf, String> {
70
+    let obj = scratch(name);
71
+    assemble(minimal_main_src(), &obj)?;
72
+    Ok(obj)
73
+}
74
+
75
+fn assert_flag_errors(flag: &str, expected: &str, name: &str) {
76
+    if !have_xcrun() {
77
+        eprintln!("skipping: xcrun as unavailable");
78
+        return;
79
+    }
80
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
81
+    let obj = match assemble_minimal_main(&format!("{name}.o")) {
82
+        Ok(obj) => obj,
83
+        Err(e) => {
84
+            eprintln!("skipping: assemble failed: {e}");
85
+            return;
86
+        }
87
+    };
88
+    let out_path = scratch(&format!("{name}.out"));
89
+    let out = Command::new(exe)
90
+        .arg(flag)
91
+        .arg("-o")
92
+        .arg(&out_path)
93
+        .arg(&obj)
94
+        .output()
95
+        .expect("afs-ld should run");
96
+    assert!(!out.status.success(), "{flag} should fail");
97
+    let stderr = String::from_utf8_lossy(&out.stderr);
98
+    assert!(
99
+        stderr.contains(expected),
100
+        "missing expected `{expected}` in stderr:\n{stderr}"
101
+    );
102
+    let _ = fs::remove_file(obj);
103
+    let _ = fs::remove_file(out_path);
104
+}
105
+
106
+fn archive(objects: &[&PathBuf], out: &PathBuf) -> Result<(), String> {
107
+    let output = Command::new("libtool")
108
+        .arg("-static")
109
+        .arg("-o")
110
+        .arg(out)
111
+        .args(objects)
112
+        .output()
113
+        .map_err(|e| format!("spawn libtool: {e}"))?;
114
+    if !output.status.success() {
115
+        return Err(format!(
116
+            "libtool failed: {}",
117
+            String::from_utf8_lossy(&output.stderr)
118
+        ));
119
+    }
120
+    Ok(())
121
+}
122
+
123
+fn nm_defined_names(path: &PathBuf) -> Result<Vec<String>, String> {
124
+    let output = Command::new("xcrun")
125
+        .args(["nm", "-gj"])
126
+        .arg(path)
127
+        .output()
128
+        .map_err(|e| format!("spawn xcrun nm: {e}"))?;
129
+    if !output.status.success() {
130
+        return Err(format!(
131
+            "xcrun nm failed: {}",
132
+            String::from_utf8_lossy(&output.stderr)
133
+        ));
134
+    }
135
+    Ok(String::from_utf8_lossy(&output.stdout)
136
+        .lines()
137
+        .map(str::trim)
138
+        .filter(|line| !line.is_empty())
139
+        .map(ToOwned::to_owned)
140
+        .collect())
141
+}
142
+
143
+#[test]
144
+fn help_flag_prints_usage_and_exits_successfully() {
145
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
146
+    let out = Command::new(exe)
147
+        .arg("--help")
148
+        .output()
149
+        .expect("afs-ld should run");
150
+    assert!(out.status.success(), "help should succeed");
151
+    let stdout = String::from_utf8_lossy(&out.stdout);
152
+    assert_eq!(stdout.as_ref(), EXPECTED_HELP);
153
+    assert!(
154
+        out.stderr.is_empty(),
155
+        "help should not write to stderr:\n{}",
156
+        String::from_utf8_lossy(&out.stderr)
157
+    );
158
+}
159
+
160
+#[test]
161
+fn version_flag_prints_version_and_exits_successfully() {
162
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
163
+    let out = Command::new(exe)
164
+        .arg("--version")
165
+        .output()
166
+        .expect("afs-ld should run");
167
+    assert!(out.status.success(), "version should succeed");
168
+    let stdout = String::from_utf8_lossy(&out.stdout);
169
+    assert_eq!(
170
+        stdout.trim(),
171
+        format!("afs-ld {}", env!("CARGO_PKG_VERSION"))
172
+    );
173
+}
174
+
175
+#[test]
176
+fn no_uuid_flag_omits_uuid_load_command() {
177
+    if !have_xcrun() {
178
+        eprintln!("skipping: xcrun as unavailable");
179
+        return;
180
+    }
181
+
182
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
183
+    let obj = match assemble_minimal_main("no-uuid-main.o") {
184
+        Ok(obj) => obj,
185
+        Err(e) => {
186
+            eprintln!("skipping: assemble failed: {e}");
187
+            return;
188
+        }
189
+    };
190
+    let out_path = scratch("no-uuid.out");
191
+    let out = Command::new(exe)
192
+        .arg("-no_uuid")
193
+        .arg("-o")
194
+        .arg(&out_path)
195
+        .arg(&obj)
196
+        .output()
197
+        .expect("afs-ld should run");
198
+    assert!(
199
+        out.status.success(),
200
+        "-no_uuid link should succeed:\nstderr:\n{}",
201
+        String::from_utf8_lossy(&out.stderr)
202
+    );
203
+
204
+    let bytes = fs::read(&out_path).expect("read linked output");
205
+    let header = parse_header(&bytes).expect("parse header");
206
+    let commands = parse_commands(&header, &bytes).expect("parse commands");
207
+    assert!(
208
+        commands.iter().all(|cmd| match cmd {
209
+            LoadCommand::Raw { cmd, .. } => *cmd != LC_UUID,
210
+            _ => true,
211
+        }),
212
+        "expected -no_uuid output to omit LC_UUID"
213
+    );
214
+
215
+    let _ = fs::remove_file(obj);
216
+    let _ = fs::remove_file(out_path);
217
+}
218
+
219
+#[test]
220
+fn no_loh_flag_warns_but_links_successfully() {
221
+    if !have_xcrun() {
222
+        eprintln!("skipping: xcrun as unavailable");
223
+        return;
224
+    }
225
+
226
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
227
+    let obj = match assemble_minimal_main("no-loh-main.o") {
228
+        Ok(obj) => obj,
229
+        Err(e) => {
230
+            eprintln!("skipping: assemble failed: {e}");
231
+            return;
232
+        }
233
+    };
234
+    let out_path = scratch("no-loh.out");
235
+    let out = Command::new(exe)
236
+        .arg("-no_loh")
237
+        .arg("-o")
238
+        .arg(&out_path)
239
+        .arg(&obj)
240
+        .output()
241
+        .expect("afs-ld should run");
242
+    assert!(
243
+        out.status.success(),
244
+        "-no_loh link should succeed:\nstderr:\n{}",
245
+        String::from_utf8_lossy(&out.stderr)
246
+    );
247
+    let stderr = String::from_utf8_lossy(&out.stderr);
248
+    assert!(
249
+        stderr.contains("afs-ld: warning: `-no_loh` requested"),
250
+        "expected -no_loh warning:\n{stderr}"
251
+    );
252
+    assert!(
253
+        out_path.is_file(),
254
+        "expected -no_loh link to produce {}",
255
+        out_path.display()
256
+    );
257
+
258
+    let _ = fs::remove_file(obj);
259
+    let _ = fs::remove_file(out_path);
260
+}
261
+
262
+#[test]
263
+fn strip_debug_flag_warns_but_links_successfully() {
264
+    if !have_xcrun() {
265
+        eprintln!("skipping: xcrun as unavailable");
266
+        return;
267
+    }
268
+
269
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
270
+    let obj = match assemble_minimal_main("strip-debug-main.o") {
271
+        Ok(obj) => obj,
272
+        Err(e) => {
273
+            eprintln!("skipping: assemble failed: {e}");
274
+            return;
275
+        }
276
+    };
277
+    let out_path = scratch("strip-debug.out");
278
+    let out = Command::new(exe)
279
+        .arg("-S")
280
+        .arg("-o")
281
+        .arg(&out_path)
282
+        .arg(&obj)
283
+        .output()
284
+        .expect("afs-ld should run");
285
+    assert!(
286
+        out.status.success(),
287
+        "-S link should succeed:\nstderr:\n{}",
288
+        String::from_utf8_lossy(&out.stderr)
289
+    );
290
+    let stderr = String::from_utf8_lossy(&out.stderr);
291
+    assert!(
292
+        stderr.contains("afs-ld: warning: `-S` requested"),
293
+        "expected -S warning:\n{stderr}"
294
+    );
295
+
296
+    let _ = fs::remove_file(obj);
297
+    let _ = fs::remove_file(out_path);
298
+}
299
+
300
+#[test]
301
+fn objc_flag_warns_but_links_successfully() {
302
+    if !have_xcrun() {
303
+        eprintln!("skipping: xcrun as unavailable");
304
+        return;
305
+    }
306
+
307
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
308
+    let obj = match assemble_minimal_main("objc-main.o") {
309
+        Ok(obj) => obj,
310
+        Err(e) => {
311
+            eprintln!("skipping: assemble failed: {e}");
312
+            return;
313
+        }
314
+    };
315
+    let out_path = scratch("objc.out");
316
+    let out = Command::new(exe)
317
+        .arg("-ObjC")
318
+        .arg("-o")
319
+        .arg(&out_path)
320
+        .arg(&obj)
321
+        .output()
322
+        .expect("afs-ld should run");
323
+    assert!(
324
+        out.status.success(),
325
+        "-ObjC link should succeed:\nstderr:\n{}",
326
+        String::from_utf8_lossy(&out.stderr)
327
+    );
328
+    let stderr = String::from_utf8_lossy(&out.stderr);
329
+    assert!(
330
+        stderr.contains("afs-ld: warning: `-ObjC` requested"),
331
+        "expected -ObjC warning:\n{stderr}"
332
+    );
333
+
334
+    let _ = fs::remove_file(obj);
335
+    let _ = fs::remove_file(out_path);
336
+}
337
+
338
+#[test]
339
+fn relocatable_flag_errors_loudly() {
340
+    assert_flag_errors(
341
+        "-r",
342
+        "`-r` relocatable output is not yet supported",
343
+        "relocatable",
344
+    );
345
+}
346
+
347
+#[test]
348
+fn bundle_flag_errors_loudly() {
349
+    assert_flag_errors("-bundle", "`-bundle` output is not yet supported", "bundle");
350
+}
351
+
352
+#[test]
353
+fn dead_strip_removes_unreferenced_symbols_and_reports_why_live() {
354
+    if !have_xcrun() {
355
+        eprintln!("skipping: xcrun as unavailable");
356
+        return;
357
+    }
358
+
359
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
360
+    let main_obj = scratch("dead-strip-main.o");
361
+    let helper_obj = scratch("dead-strip-helper.o");
362
+    let unused_obj = scratch("dead-strip-unused.o");
363
+    let out_path = scratch("dead-strip.out");
364
+    let main_src = r#"
365
+        .section __TEXT,__text,regular,pure_instructions
366
+        .globl _main
367
+        _main:
368
+            bl _helper
369
+            mov w0, #0
370
+            ret
371
+        .subsections_via_symbols
372
+    "#;
373
+    let helper_src = r#"
374
+        .section __TEXT,__text,regular,pure_instructions
375
+        .globl _helper
376
+        _helper:
377
+            ret
378
+        .subsections_via_symbols
379
+    "#;
380
+    let unused_src = r#"
381
+        .section __TEXT,__text,regular,pure_instructions
382
+        .globl _unused
383
+        _unused:
384
+            ret
385
+        .subsections_via_symbols
386
+    "#;
387
+    if let Err(e) = assemble(main_src, &main_obj) {
388
+        eprintln!("skipping: assemble failed: {e}");
389
+        return;
390
+    }
391
+    if let Err(e) = assemble(helper_src, &helper_obj) {
392
+        eprintln!("skipping: assemble failed: {e}");
393
+        let _ = fs::remove_file(main_obj);
394
+        return;
395
+    }
396
+    if let Err(e) = assemble(unused_src, &unused_obj) {
397
+        eprintln!("skipping: assemble failed: {e}");
398
+        let _ = fs::remove_file(main_obj);
399
+        let _ = fs::remove_file(helper_obj);
400
+        return;
401
+    }
402
+
403
+    let out = Command::new(exe)
404
+        .arg("-dead_strip")
405
+        .arg("-why_live")
406
+        .arg("_helper")
407
+        .arg("-why_live")
408
+        .arg("_unused")
409
+        .arg("-o")
410
+        .arg(&out_path)
411
+        .arg(&main_obj)
412
+        .arg(&helper_obj)
413
+        .arg(&unused_obj)
414
+        .output()
415
+        .expect("afs-ld should run");
416
+    assert!(
417
+        out.status.success(),
418
+        "-dead_strip link should succeed:\nstderr:\n{}",
419
+        String::from_utf8_lossy(&out.stderr)
420
+    );
421
+    let stdout = String::from_utf8_lossy(&out.stdout);
422
+    assert!(stdout.contains("_helper is live because:"));
423
+    assert!(stdout.contains("_helper is reachable from _main"));
424
+    assert!(stdout.contains("_main is in -e _main (GC root)"));
425
+    assert!(stdout.contains("_unused is not live (dead-stripped)"));
426
+
427
+    let symbols = match nm_defined_names(&out_path) {
428
+        Ok(symbols) => symbols,
429
+        Err(e) => {
430
+            panic!("nm failed: {e}");
431
+        }
432
+    };
433
+    assert!(symbols.contains(&"_main".to_string()));
434
+    assert!(symbols.contains(&"_helper".to_string()));
435
+    assert!(
436
+        !symbols.contains(&"_unused".to_string()),
437
+        "dead-stripped symbol still present:\n{}",
438
+        symbols.join("\n")
439
+    );
440
+
441
+    let _ = fs::remove_file(main_obj);
442
+    let _ = fs::remove_file(helper_obj);
443
+    let _ = fs::remove_file(unused_obj);
444
+    let _ = fs::remove_file(out_path);
445
+}
446
+
447
+#[test]
448
+fn dead_strip_keeps_no_dead_strip_roots() {
449
+    if !have_xcrun() {
450
+        eprintln!("skipping: xcrun as unavailable");
451
+        return;
452
+    }
453
+
454
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
455
+    let obj = scratch("dead-strip-no-dead-strip.o");
456
+    let out_path = scratch("dead-strip-no-dead-strip.out");
457
+    let src = r#"
458
+        .section __TEXT,__text,regular,pure_instructions
459
+        .globl _main
460
+        _main:
461
+            mov w0, #0
462
+            ret
463
+
464
+        .globl _keep
465
+        _keep:
466
+            ret
467
+        .desc _keep, 0x20
468
+
469
+        .globl _drop
470
+        _drop:
471
+            ret
472
+        .subsections_via_symbols
473
+    "#;
474
+    if let Err(e) = assemble(src, &obj) {
475
+        eprintln!("skipping: assemble failed: {e}");
476
+        return;
477
+    }
478
+
479
+    let out = Command::new(exe)
480
+        .arg("-dead_strip")
481
+        .arg("-why_live")
482
+        .arg("_keep")
483
+        .arg("-why_live")
484
+        .arg("_drop")
485
+        .arg("-o")
486
+        .arg(&out_path)
487
+        .arg(&obj)
488
+        .output()
489
+        .expect("afs-ld should run");
490
+    assert!(
491
+        out.status.success(),
492
+        "-dead_strip link should succeed:\nstderr:\n{}",
493
+        String::from_utf8_lossy(&out.stderr)
494
+    );
495
+    let stdout = String::from_utf8_lossy(&out.stdout);
496
+    assert!(stdout.contains("_keep is live because:"));
497
+    assert!(stdout.contains("_keep is marked N_NO_DEAD_STRIP (GC root)"));
498
+    assert!(stdout.contains("_drop is not live (dead-stripped)"));
499
+
500
+    let symbols = match nm_defined_names(&out_path) {
501
+        Ok(symbols) => symbols,
502
+        Err(e) => {
503
+            panic!("nm failed: {e}");
504
+        }
505
+    };
506
+    assert!(symbols.contains(&"_main".to_string()));
507
+    assert!(symbols.contains(&"_keep".to_string()));
508
+    assert!(
509
+        !symbols.contains(&"_drop".to_string()),
510
+        "dead-stripped symbol still present:\n{}",
511
+        symbols.join("\n")
512
+    );
513
+
514
+    let _ = fs::remove_file(obj);
515
+    let _ = fs::remove_file(out_path);
516
+}
517
+
518
+#[test]
519
+fn icf_safe_flag_links_successfully() {
520
+    if !have_xcrun() {
521
+        eprintln!("skipping: xcrun as unavailable");
522
+        return;
523
+    }
524
+
525
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
526
+    let obj = match assemble_minimal_main("icf-safe-main.o") {
527
+        Ok(obj) => obj,
528
+        Err(e) => {
529
+            eprintln!("skipping: assemble failed: {e}");
530
+            return;
531
+        }
532
+    };
533
+    let out_path = scratch("icf-safe.out");
534
+    let out = Command::new(exe)
535
+        .arg("-icf=safe")
536
+        .arg("-o")
537
+        .arg(&out_path)
538
+        .arg(&obj)
539
+        .output()
540
+        .expect("afs-ld should run");
541
+    assert!(
542
+        out.status.success(),
543
+        "-icf=safe link should succeed:\nstderr:\n{}",
544
+        String::from_utf8_lossy(&out.stderr)
545
+    );
546
+    assert!(
547
+        out_path.is_file(),
548
+        "expected -icf=safe link to produce {}",
549
+        out_path.display()
550
+    );
551
+
552
+    let _ = fs::remove_file(obj);
553
+    let _ = fs::remove_file(out_path);
554
+}
555
+
556
+#[test]
557
+fn icf_all_flag_errors_loudly() {
558
+    assert_flag_errors(
559
+        "-icf=all",
560
+        "`-icf=all` is not yet supported; use `-icf=safe` or `-icf=none`",
561
+        "icf-all",
562
+    );
563
+}
564
+
565
+#[test]
566
+fn fixup_chains_flag_errors_loudly() {
567
+    assert_flag_errors(
568
+        "-fixup_chains",
569
+        "`-fixup_chains` is not yet supported",
570
+        "fixup-chains",
571
+    );
572
+}
573
+
42574
 #[test]
43575
 fn undefined_symbol_diagnostic_is_not_double_prefixed() {
44576
     if !have_xcrun() {
@@ -79,3 +611,377 @@ fn undefined_symbol_diagnostic_is_not_double_prefixed() {
79611
 
80612
     let _ = fs::remove_file(obj);
81613
 }
614
+
615
+#[test]
616
+fn undefined_warning_mode_links_and_warns_once() {
617
+    if !have_xcrun() {
618
+        eprintln!("skipping: xcrun as unavailable");
619
+        return;
620
+    }
621
+    let Some(sdk) = sdk_path() else {
622
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
623
+        return;
624
+    };
625
+
626
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
627
+    let obj = scratch("missing-warning.o");
628
+    let src = r#"
629
+        .section __TEXT,__text,regular,pure_instructions
630
+        .globl _main
631
+        _main:
632
+            bl _missing
633
+            ret
634
+        .subsections_via_symbols
635
+    "#;
636
+    if let Err(e) = assemble(src, &obj) {
637
+        eprintln!("skipping: assemble failed: {e}");
638
+        return;
639
+    }
640
+
641
+    let out_path = scratch("missing-warning.out");
642
+    let out = Command::new(exe)
643
+        .arg("-undefined")
644
+        .arg("warning")
645
+        .arg("-syslibroot")
646
+        .arg(&sdk)
647
+        .arg("-lSystem")
648
+        .arg("-o")
649
+        .arg(&out_path)
650
+        .arg(&obj)
651
+        .output()
652
+        .expect("afs-ld should run");
653
+    assert!(
654
+        out.status.success(),
655
+        "-undefined warning should link successfully:\nstderr:\n{}",
656
+        String::from_utf8_lossy(&out.stderr)
657
+    );
658
+    let stderr = String::from_utf8_lossy(&out.stderr);
659
+    assert!(
660
+        stderr.contains("afs-ld: warning: undefined symbol: _missing"),
661
+        "missing expected undefined-symbol warning:\n{stderr}"
662
+    );
663
+    assert!(
664
+        !stderr.contains("afs-ld: warning: afs-ld: warning:"),
665
+        "warning diagnostic was double-prefixed:\n{stderr}"
666
+    );
667
+    assert!(out_path.exists(), "expected linked output to be written");
668
+
669
+    let _ = fs::remove_file(obj);
670
+    let _ = fs::remove_file(out_path);
671
+}
672
+
673
+#[test]
674
+fn undefined_suppress_mode_links_silently() {
675
+    if !have_xcrun() {
676
+        eprintln!("skipping: xcrun as unavailable");
677
+        return;
678
+    }
679
+    let Some(sdk) = sdk_path() else {
680
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
681
+        return;
682
+    };
683
+
684
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
685
+    let obj = scratch("missing-suppress.o");
686
+    let src = r#"
687
+        .section __TEXT,__text,regular,pure_instructions
688
+        .globl _main
689
+        _main:
690
+            bl _missing
691
+            ret
692
+        .subsections_via_symbols
693
+    "#;
694
+    if let Err(e) = assemble(src, &obj) {
695
+        eprintln!("skipping: assemble failed: {e}");
696
+        return;
697
+    }
698
+
699
+    let out_path = scratch("missing-suppress.out");
700
+    let out = Command::new(exe)
701
+        .arg("-undefined")
702
+        .arg("suppress")
703
+        .arg("-syslibroot")
704
+        .arg(&sdk)
705
+        .arg("-lSystem")
706
+        .arg("-o")
707
+        .arg(&out_path)
708
+        .arg(&obj)
709
+        .output()
710
+        .expect("afs-ld should run");
711
+    assert!(
712
+        out.status.success(),
713
+        "-undefined suppress should link successfully:\nstderr:\n{}",
714
+        String::from_utf8_lossy(&out.stderr)
715
+    );
716
+    let stderr = String::from_utf8_lossy(&out.stderr);
717
+    assert!(
718
+        !stderr.contains("undefined symbol: _missing"),
719
+        "expected -undefined suppress to omit undefined diagnostic:\n{stderr}"
720
+    );
721
+    assert!(out_path.exists(), "expected linked output to be written");
722
+
723
+    let _ = fs::remove_file(obj);
724
+    let _ = fs::remove_file(out_path);
725
+}
726
+
727
+#[test]
728
+fn trace_flag_prints_loaded_inputs_and_archive_members() {
729
+    if !have_xcrun() {
730
+        eprintln!("skipping: xcrun as unavailable");
731
+        return;
732
+    }
733
+
734
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
735
+    let main_obj = scratch("trace-main.o");
736
+    let helper_obj = scratch("trace-helper.o");
737
+    let archive_path = scratch("libtracehelpers.a");
738
+    let out_path = scratch("trace.out");
739
+    let main_src = r#"
740
+        .section __TEXT,__text,regular,pure_instructions
741
+        .globl _main
742
+        _main:
743
+            bl _helper
744
+            mov w0, #0
745
+            ret
746
+        .subsections_via_symbols
747
+    "#;
748
+    let helper_src = r#"
749
+        .section __TEXT,__text,regular,pure_instructions
750
+        .globl _helper
751
+        _helper:
752
+            ret
753
+        .subsections_via_symbols
754
+    "#;
755
+    if let Err(e) = assemble(main_src, &main_obj) {
756
+        eprintln!("skipping: assemble failed: {e}");
757
+        return;
758
+    }
759
+    if let Err(e) = assemble(helper_src, &helper_obj) {
760
+        eprintln!("skipping: assemble failed: {e}");
761
+        return;
762
+    }
763
+    if let Err(e) = archive(&[&helper_obj], &archive_path) {
764
+        eprintln!("skipping: archive failed: {e}");
765
+        let _ = fs::remove_file(main_obj);
766
+        let _ = fs::remove_file(helper_obj);
767
+        return;
768
+    }
769
+
770
+    let out = Command::new(exe)
771
+        .arg("-t")
772
+        .arg("-o")
773
+        .arg(&out_path)
774
+        .arg(&main_obj)
775
+        .arg(&archive_path)
776
+        .output()
777
+        .expect("afs-ld should run");
778
+    assert!(
779
+        out.status.success(),
780
+        "trace link should succeed:\nstderr:\n{}",
781
+        String::from_utf8_lossy(&out.stderr)
782
+    );
783
+    let stderr = String::from_utf8_lossy(&out.stderr);
784
+    assert!(
785
+        stderr.contains(&format!("afs-ld: loading {}", main_obj.display())),
786
+        "missing main object trace:\n{stderr}"
787
+    );
788
+    assert!(
789
+        stderr.contains(&format!("afs-ld: loading {}", archive_path.display())),
790
+        "missing archive trace:\n{stderr}"
791
+    );
792
+    assert!(
793
+        stderr.contains("libtracehelpers.a("),
794
+        "missing fetched archive member trace:\n{stderr}"
795
+    );
796
+
797
+    let _ = fs::remove_file(main_obj);
798
+    let _ = fs::remove_file(helper_obj);
799
+    let _ = fs::remove_file(archive_path);
800
+    let _ = fs::remove_file(out_path);
801
+}
802
+
803
+#[test]
804
+fn why_live_reports_root_entry_symbol() {
805
+    if !have_xcrun() {
806
+        eprintln!("skipping: xcrun as unavailable");
807
+        return;
808
+    }
809
+
810
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
811
+    let main_obj = scratch("why-live-root-main.o");
812
+    let out_path = scratch("why-live-root.out");
813
+    let src = r#"
814
+        .section __TEXT,__text,regular,pure_instructions
815
+        .globl _main
816
+        _main:
817
+            mov w0, #0
818
+            ret
819
+        .subsections_via_symbols
820
+    "#;
821
+    if let Err(e) = assemble(src, &main_obj) {
822
+        eprintln!("skipping: assemble failed: {e}");
823
+        return;
824
+    }
825
+
826
+    let out = Command::new(exe)
827
+        .arg("-why_live")
828
+        .arg("_main")
829
+        .arg("-o")
830
+        .arg(&out_path)
831
+        .arg(&main_obj)
832
+        .output()
833
+        .expect("afs-ld should run");
834
+    assert!(
835
+        out.status.success(),
836
+        "why_live link should succeed:\nstderr:\n{}",
837
+        String::from_utf8_lossy(&out.stderr)
838
+    );
839
+    let stdout = String::from_utf8_lossy(&out.stdout);
840
+    assert!(stdout.contains("_main is live because:"));
841
+    assert!(stdout.contains("-dead_strip was not requested"));
842
+    assert!(stdout.contains("_main is in -e _main (GC root)"));
843
+
844
+    let _ = fs::remove_file(main_obj);
845
+    let _ = fs::remove_file(out_path);
846
+}
847
+
848
+#[test]
849
+fn why_live_reports_transitive_symbol_chain() {
850
+    if !have_xcrun() {
851
+        eprintln!("skipping: xcrun as unavailable");
852
+        return;
853
+    }
854
+
855
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
856
+    let main_obj = scratch("why-live-main.o");
857
+    let helper_obj = scratch("why-live-helper.o");
858
+    let leaf_obj = scratch("why-live-leaf.o");
859
+    let out_path = scratch("why-live.out");
860
+    let main_src = r#"
861
+        .section __TEXT,__text,regular,pure_instructions
862
+        .globl _main
863
+        _main:
864
+            bl _helper
865
+            mov w0, #0
866
+            ret
867
+        .subsections_via_symbols
868
+    "#;
869
+    let helper_src = r#"
870
+        .section __TEXT,__text,regular,pure_instructions
871
+        .globl _helper
872
+        _helper:
873
+            bl _leaf
874
+            ret
875
+        .subsections_via_symbols
876
+    "#;
877
+    let leaf_src = r#"
878
+        .section __TEXT,__text,regular,pure_instructions
879
+        .globl _leaf
880
+        _leaf:
881
+            ret
882
+        .subsections_via_symbols
883
+    "#;
884
+    if let Err(e) = assemble(main_src, &main_obj) {
885
+        eprintln!("skipping: assemble failed: {e}");
886
+        return;
887
+    }
888
+    if let Err(e) = assemble(helper_src, &helper_obj) {
889
+        eprintln!("skipping: assemble failed: {e}");
890
+        let _ = fs::remove_file(main_obj);
891
+        return;
892
+    }
893
+    if let Err(e) = assemble(leaf_src, &leaf_obj) {
894
+        eprintln!("skipping: assemble failed: {e}");
895
+        let _ = fs::remove_file(main_obj);
896
+        let _ = fs::remove_file(helper_obj);
897
+        return;
898
+    }
899
+
900
+    let out = Command::new(exe)
901
+        .arg("-why_live")
902
+        .arg("_leaf")
903
+        .arg("-o")
904
+        .arg(&out_path)
905
+        .arg(&main_obj)
906
+        .arg(&helper_obj)
907
+        .arg(&leaf_obj)
908
+        .output()
909
+        .expect("afs-ld should run");
910
+    assert!(
911
+        out.status.success(),
912
+        "why_live link should succeed:\nstderr:\n{}",
913
+        String::from_utf8_lossy(&out.stderr)
914
+    );
915
+    let stdout = String::from_utf8_lossy(&out.stdout);
916
+    assert!(stdout.contains("_leaf is live because:"));
917
+    assert!(stdout.contains("-dead_strip was not requested"));
918
+    assert!(stdout.contains("_leaf is reachable from _helper"));
919
+    assert!(stdout.contains("_helper is reachable from _main"));
920
+    assert!(stdout.contains("_main is in -e _main (GC root)"));
921
+
922
+    let _ = fs::remove_file(main_obj);
923
+    let _ = fs::remove_file(helper_obj);
924
+    let _ = fs::remove_file(leaf_obj);
925
+    let _ = fs::remove_file(out_path);
926
+}
927
+
928
+#[test]
929
+fn why_live_reports_folded_symbol_winner_chain() {
930
+    if !have_xcrun() {
931
+        eprintln!("skipping: xcrun as unavailable");
932
+        return;
933
+    }
934
+
935
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
936
+    let obj = scratch("why-live-folded.o");
937
+    let out_path = scratch("why-live-folded.out");
938
+    let src = r#"
939
+        .section __TEXT,__text,regular,pure_instructions
940
+        .globl _main
941
+        _main:
942
+            stp x29, x30, [sp, #-16]!
943
+            mov x29, sp
944
+            bl _helper1
945
+            bl _helper2
946
+            mov w0, #0
947
+            ldp x29, x30, [sp], #16
948
+            ret
949
+
950
+        .private_extern _helper1
951
+        _helper1:
952
+            mov w0, #0
953
+            ret
954
+
955
+        .private_extern _helper2
956
+        _helper2:
957
+            mov w0, #0
958
+            ret
959
+        .subsections_via_symbols
960
+    "#;
961
+    if let Err(e) = assemble(src, &obj) {
962
+        eprintln!("skipping: assemble failed: {e}");
963
+        return;
964
+    }
965
+
966
+    let out = Command::new(exe)
967
+        .arg("-icf=safe")
968
+        .arg("-why_live")
969
+        .arg("_helper2")
970
+        .arg("-o")
971
+        .arg(&out_path)
972
+        .arg(&obj)
973
+        .output()
974
+        .expect("afs-ld should run");
975
+    assert!(
976
+        out.status.success(),
977
+        "why_live folded-symbol link should succeed:\nstderr:\n{}",
978
+        String::from_utf8_lossy(&out.stderr)
979
+    );
980
+    let stdout = String::from_utf8_lossy(&out.stdout);
981
+    assert!(stdout.contains("_helper2 was folded to _helper1 by -icf=safe"));
982
+    assert!(stdout.contains("_helper1 is live because:"));
983
+    assert!(stdout.contains("_helper1 is reachable from _main"));
984
+
985
+    let _ = fs::remove_file(obj);
986
+    let _ = fs::remove_file(out_path);
987
+}
tests/common/harness.rsmodified
2041 lines changed — click to load
@@ -1,28 +1,127 @@
1
-//! Differential harness: compare afs-ld output against Apple `ld` output.
1
+//! Differential harness shared by parity-oriented integration tests.
22
 //!
3
-//! Sprint 0 lands the diffing surface. The `link_both` function that actually
4
-//! shells out to both linkers arrives once afs-ld can produce a real binary
5
-//! (Sprint 18). Until then, tests exercise `diff_macho` directly against
6
-//! synthesized byte slices.
3
+//! The early scaffold only diffed arbitrary byte slices. Sprint 27 starts
4
+//! turning it into a real Apple-`ld` matrix harness with a tiny corpus, basic
5
+//! tolerated-diff rules, and reusable link/runtime helpers.
76
 
87
 #![allow(dead_code)]
98
 
10
-use std::path::PathBuf;
9
+use std::collections::{BTreeMap, HashSet};
10
+use std::fs;
11
+use std::path::{Path, PathBuf};
12
+use std::process::Command;
13
+use std::time::{SystemTime, UNIX_EPOCH};
1114
 
15
+use afs_ld::leb::read_uleb;
16
+use afs_ld::macho::constants::{
17
+    INDIRECT_SYMBOL_ABS, INDIRECT_SYMBOL_LOCAL, LC_BUILD_VERSION, LC_CODE_SIGNATURE,
18
+    LC_DATA_IN_CODE, LC_DYLD_CHAINED_FIXUPS, LC_DYLD_EXPORTS_TRIE, LC_DYLD_INFO_ONLY, LC_DYSYMTAB,
19
+    LC_FUNCTION_STARTS, LC_ID_DYLIB, LC_LOAD_DYLIB, LC_LOAD_UPWARD_DYLIB, LC_LOAD_WEAK_DYLIB,
20
+    LC_REEXPORT_DYLIB, LC_SEGMENT_64, LC_SYMTAB, LC_UUID,
21
+};
22
+use afs_ld::macho::dylib::DylibFile;
23
+use afs_ld::macho::exports::ExportKind;
24
+use afs_ld::macho::reader::{
25
+    parse_commands, parse_header, u32_le, BuildVersionCmd, DyldInfoCmd, LoadCommand,
26
+    Section64Header,
27
+};
28
+use afs_ld::string_table::StringTable;
29
+use afs_ld::symbol::{parse_nlist_table, SymKind};
30
+use afs_ld::synth::unwind::decode_unwind_info;
31
+
32
+#[derive(Debug, Clone)]
1233
 pub struct LinkCase {
13
-    pub name: &'static str,
34
+    pub name: String,
35
+    pub dir: PathBuf,
1436
     pub inputs: Vec<PathBuf>,
1537
     pub args: Vec<String>,
38
+    pub section_checks: Vec<(String, String)>,
39
+    pub absent_sections: Vec<(String, String)>,
40
+    pub page_ref_checks: Vec<PageRefCheck>,
41
+    pub command_checks: Vec<CommandCheck>,
42
+    artifacts: Vec<ArtifactSpec>,
43
+    pub ignored_load_commands: Vec<u32>,
44
+    pub absent_load_commands: Vec<u32>,
45
+    pub runtime_args: Vec<String>,
46
+    pub notes: Option<String>,
47
+    pub case_tolerances: Vec<CaseTolerance>,
48
+}
49
+
50
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51
+pub enum CommandCheck {
52
+    BuildVersion,
53
+    LoadDylibNames,
54
+    ExportRecords,
55
+    SymbolRecordMap,
56
+    IndirectSymbolIdentities,
57
+    SymbolPartitionNames,
58
+    StringTableNearParity,
59
+    FunctionStarts,
60
+    NormalizedFunctionStarts,
61
+    DataInCode,
62
+    RebasedUnwindBytes,
63
+    DyldInfoRebase,
64
+    DyldInfoBind,
65
+    DyldInfoWeakBind,
66
+    DyldInfoLazyBind,
67
+}
68
+
69
+#[derive(Debug, Clone, PartialEq, Eq)]
70
+pub struct PageRefCheck {
71
+    pub segname: String,
72
+    pub sectname: String,
73
+    pub site_offset: u64,
74
+    pub kind: PageRefKind,
75
+    pub symbol: String,
76
+}
77
+
78
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79
+pub enum PageRefKind {
80
+    Add,
81
+    Load,
82
+}
83
+
84
+#[derive(Debug, Clone, PartialEq, Eq)]
85
+pub struct CaseTolerance {
86
+    pub region: ToleranceRegion,
87
+    pub reason: String,
88
+}
89
+
90
+#[derive(Debug, Clone, PartialEq, Eq)]
91
+pub enum ToleranceRegion {
92
+    SectionBytes {
93
+        segname: String,
94
+        sectname: Option<String>,
95
+        start: usize,
96
+        end_inclusive: usize,
97
+    },
98
+}
99
+
100
+#[derive(Debug, Clone, PartialEq, Eq)]
101
+struct ArtifactSpec {
102
+    src_name: String,
103
+    out_name: String,
104
+    kind: ArtifactKind,
105
+    dep_name: Option<String>,
106
+}
107
+
108
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109
+enum ArtifactKind {
110
+    ClangDylib,
111
+    ClangArchive,
112
+    ClangReexportDylib,
16113
 }
17114
 
18115
 pub struct LinkOutputs {
19116
     pub ours: Vec<u8>,
20117
     pub theirs: Vec<u8>,
118
+    pub our_path: PathBuf,
119
+    pub their_path: PathBuf,
21120
 }
22121
 
23122
 #[derive(Debug, Clone, PartialEq, Eq)]
24123
 pub enum DiffCategory {
25
-    /// A diff we expect: UUID bytes, timestamps, hash-backed temp paths, etc.
124
+    /// A diff we expect: UUID bytes, code-signature hashes, etc.
26125
     Tolerated(&'static str),
27126
     /// Anything else. Fails the parity test.
28127
     Critical,
@@ -48,9 +147,806 @@ impl DiffReport {
48147
     }
49148
 }
50149
 
51
-/// Byte-level diff between two Mach-O images. Sprint 0 treats every byte diff
52
-/// as Critical; later sprints layer in the tolerated-diff predicates (UUID,
53
-/// timestamp, code-signature hashes, string-table suffix-dedup variance).
150
+#[derive(Debug, Clone, PartialEq, Eq)]
151
+pub struct ProgramOutput {
152
+    pub exit_code: Option<i32>,
153
+    pub stdout: Vec<u8>,
154
+    pub stderr: Vec<u8>,
155
+}
156
+
157
+type NormalizedBuildVersion = (u32, u32, u32, Vec<u32>);
158
+
159
+pub fn have_xcrun() -> bool {
160
+    Command::new("xcrun")
161
+        .arg("-f")
162
+        .arg("as")
163
+        .output()
164
+        .map(|o| o.status.success())
165
+        .unwrap_or(false)
166
+}
167
+
168
+pub fn have_xcrun_tool(tool: &str) -> bool {
169
+    Command::new("xcrun")
170
+        .arg("-f")
171
+        .arg(tool)
172
+        .output()
173
+        .map(|o| o.status.success())
174
+        .unwrap_or(false)
175
+}
176
+
177
+pub fn have_tool(tool: &str) -> bool {
178
+    Command::new(tool)
179
+        .arg("--version")
180
+        .output()
181
+        .map(|o| o.status.success() || !o.stderr.is_empty())
182
+        .unwrap_or(false)
183
+}
184
+
185
+pub fn sdk_path() -> Option<String> {
186
+    let out = Command::new("xcrun")
187
+        .args(["--sdk", "macosx", "--show-sdk-path"])
188
+        .output()
189
+        .ok()?;
190
+    if !out.status.success() {
191
+        return None;
192
+    }
193
+    Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
194
+}
195
+
196
+pub fn sdk_version() -> Option<String> {
197
+    let out = Command::new("xcrun")
198
+        .args(["--sdk", "macosx", "--show-sdk-version"])
199
+        .output()
200
+        .ok()?;
201
+    if !out.status.success() {
202
+        return None;
203
+    }
204
+    Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
205
+}
206
+
207
+pub fn scratch(name: &str) -> PathBuf {
208
+    std::env::temp_dir().join(format!("afs-ld-parity-{}-{name}", std::process::id()))
209
+}
210
+
211
+pub fn assemble(src: &str, out: &PathBuf) -> Result<(), String> {
212
+    let tmp = std::env::temp_dir().join(format!(
213
+        "afs-ld-parity-{}-{}.s",
214
+        std::process::id(),
215
+        out.file_stem().and_then(|s| s.to_str()).unwrap_or("t")
216
+    ));
217
+    fs::write(&tmp, src).map_err(|e| format!("write {}: {e}", tmp.display()))?;
218
+    let output = Command::new("xcrun")
219
+        .args(["--sdk", "macosx", "as", "-arch", "arm64"])
220
+        .arg(&tmp)
221
+        .arg("-o")
222
+        .arg(out)
223
+        .output()
224
+        .map_err(|e| format!("spawn xcrun as: {e}"))?;
225
+    let _ = fs::remove_file(&tmp);
226
+    if !output.status.success() {
227
+        return Err(format!(
228
+            "xcrun as failed: {}",
229
+            String::from_utf8_lossy(&output.stderr)
230
+        ));
231
+    }
232
+    Ok(())
233
+}
234
+
235
+pub fn compile_c(src: &str, out: &PathBuf) -> Result<(), String> {
236
+    let tmp = std::env::temp_dir().join(format!(
237
+        "afs-ld-parity-{}-{}.c",
238
+        std::process::id(),
239
+        out.file_stem().and_then(|s| s.to_str()).unwrap_or("t")
240
+    ));
241
+    fs::write(&tmp, src).map_err(|e| format!("write {}: {e}", tmp.display()))?;
242
+    let output = Command::new("xcrun")
243
+        .args(["--sdk", "macosx", "clang", "-arch", "arm64", "-c"])
244
+        .arg(&tmp)
245
+        .arg("-o")
246
+        .arg(out)
247
+        .output()
248
+        .map_err(|e| format!("spawn xcrun clang: {e}"))?;
249
+    let _ = fs::remove_file(&tmp);
250
+    if !output.status.success() {
251
+        return Err(format!(
252
+            "xcrun clang failed: {}",
253
+            String::from_utf8_lossy(&output.stderr)
254
+        ));
255
+    }
256
+    Ok(())
257
+}
258
+
259
+fn compile_dylib_c(src: &str, out: &PathBuf) -> Result<(), String> {
260
+    let tmp = std::env::temp_dir().join(format!(
261
+        "afs-ld-parity-{}-{}.c",
262
+        std::process::id(),
263
+        out.file_stem().and_then(|s| s.to_str()).unwrap_or("lib")
264
+    ));
265
+    fs::write(&tmp, src).map_err(|e| format!("write {}: {e}", tmp.display()))?;
266
+    let install_name = out.to_string_lossy().to_string();
267
+    let output = Command::new("xcrun")
268
+        .args(["--sdk", "macosx", "clang", "-arch", "arm64", "-dynamiclib"])
269
+        .arg(&tmp)
270
+        .arg(format!("-Wl,-install_name,{install_name}"))
271
+        .arg("-o")
272
+        .arg(out)
273
+        .output()
274
+        .map_err(|e| format!("spawn xcrun clang dylib: {e}"))?;
275
+    let _ = fs::remove_file(&tmp);
276
+    if !output.status.success() {
277
+        return Err(format!(
278
+            "xcrun clang dylib failed: {}",
279
+            String::from_utf8_lossy(&output.stderr)
280
+        ));
281
+    }
282
+    Ok(())
283
+}
284
+
285
+fn compile_archive_c(src: &str, out: &PathBuf) -> Result<(), String> {
286
+    let obj = out.with_extension("o");
287
+    compile_c(src, &obj)?;
288
+    let output = Command::new("libtool")
289
+        .args(["-static", "-o"])
290
+        .arg(out)
291
+        .arg(&obj)
292
+        .output()
293
+        .map_err(|e| format!("spawn libtool archive: {e}"))?;
294
+    let _ = fs::remove_file(&obj);
295
+    if !output.status.success() {
296
+        return Err(format!(
297
+            "libtool archive failed: {}",
298
+            String::from_utf8_lossy(&output.stderr)
299
+        ));
300
+    }
301
+    Ok(())
302
+}
303
+
304
+fn compile_reexport_dylib_c(src: &str, out: &PathBuf, dep: &Path) -> Result<(), String> {
305
+    let tmp = std::env::temp_dir().join(format!(
306
+        "afs-ld-parity-{}-{}.c",
307
+        std::process::id(),
308
+        out.file_stem()
309
+            .and_then(|s| s.to_str())
310
+            .unwrap_or("reexport")
311
+    ));
312
+    fs::write(&tmp, src).map_err(|e| format!("write {}: {e}", tmp.display()))?;
313
+    let install_name = out.to_string_lossy().to_string();
314
+    let output = Command::new("xcrun")
315
+        .args(["--sdk", "macosx", "clang", "-arch", "arm64", "-dynamiclib"])
316
+        .arg(&tmp)
317
+        .arg(format!("-Wl,-install_name,{install_name}"))
318
+        .arg(format!("-Wl,-reexport_library,{}", dep.display()))
319
+        .arg("-o")
320
+        .arg(out)
321
+        .output()
322
+        .map_err(|e| format!("spawn xcrun clang reexport dylib: {e}"))?;
323
+    let _ = fs::remove_file(&tmp);
324
+    if !output.status.success() {
325
+        return Err(format!(
326
+            "xcrun clang reexport dylib failed: {}",
327
+            String::from_utf8_lossy(&output.stderr)
328
+        ));
329
+    }
330
+    Ok(())
331
+}
332
+
333
+pub fn load_corpus(root: &Path) -> Result<Vec<LinkCase>, String> {
334
+    let mut cases = Vec::new();
335
+    let entries =
336
+        fs::read_dir(root).map_err(|e| format!("read parity corpus {}: {e}", root.display()))?;
337
+    for entry in entries {
338
+        let entry = entry.map_err(|e| format!("read parity corpus entry: {e}"))?;
339
+        let path = entry.path();
340
+        if !path.is_dir() {
341
+            continue;
342
+        }
343
+
344
+        let name = path
345
+            .file_name()
346
+            .and_then(|s| s.to_str())
347
+            .ok_or_else(|| format!("invalid UTF-8 case directory {}", path.display()))?
348
+            .to_string();
349
+        let inputs_dir = path.join("inputs");
350
+        let mut inputs = Vec::new();
351
+        let input_entries = fs::read_dir(&inputs_dir)
352
+            .map_err(|e| format!("read inputs for {}: {e}", path.display()))?;
353
+        for input in input_entries {
354
+            let input = input.map_err(|e| format!("read input entry for {}: {e}", name))?;
355
+            let input_path = input.path();
356
+            match input_path.extension().and_then(|s| s.to_str()) {
357
+                Some("s") | Some("c") | Some("o") | Some("a") | Some("tbd") => {
358
+                    inputs.push(input_path)
359
+                }
360
+                _ => {}
361
+            }
362
+        }
363
+        inputs.sort();
364
+        if inputs.is_empty() {
365
+            return Err(format!(
366
+                "parity corpus case {} has no supported source inputs",
367
+                path.display()
368
+            ));
369
+        }
370
+
371
+        let args = read_tokens(&path.join("args.txt"))?;
372
+        let section_checks = read_sections(&path.join("sections.txt"))?;
373
+        let absent_sections = read_sections_if_present(&path.join("absent_sections.txt"))?;
374
+        let page_ref_checks = read_page_refs(&path.join("page_refs.txt"))?;
375
+        let command_checks = read_command_checks(&path.join("command_checks.txt"))?;
376
+        let artifacts = read_artifacts(&path.join("artifacts.txt"))?;
377
+        let artifact_srcs: HashSet<&str> = artifacts
378
+            .iter()
379
+            .map(|artifact| artifact.src_name.as_str())
380
+            .collect();
381
+        inputs.retain(|input| {
382
+            input
383
+                .file_name()
384
+                .and_then(|s| s.to_str())
385
+                .map(|name| !artifact_srcs.contains(name))
386
+                .unwrap_or(true)
387
+        });
388
+        let ignored_load_commands =
389
+            read_load_command_names(&path.join("ignored_load_commands.txt"))?;
390
+        let absent_load_commands = read_load_command_names(&path.join("absent_load_commands.txt"))?;
391
+        let runtime_args = read_tokens_if_present(&path.join("runtime.txt"))?;
392
+        let notes = fs::read_to_string(path.join("notes.md")).ok();
393
+        let case_tolerances = parse_case_tolerances(notes.as_deref())?;
394
+
395
+        cases.push(LinkCase {
396
+            name,
397
+            dir: path,
398
+            inputs,
399
+            args,
400
+            section_checks,
401
+            absent_sections,
402
+            page_ref_checks,
403
+            command_checks,
404
+            artifacts,
405
+            ignored_load_commands,
406
+            absent_load_commands,
407
+            runtime_args,
408
+            notes,
409
+            case_tolerances,
410
+        });
411
+    }
412
+
413
+    cases.sort_by(|a, b| a.name.cmp(&b.name));
414
+    Ok(cases)
415
+}
416
+
417
+pub fn link_both(case: &LinkCase) -> Result<LinkOutputs, String> {
418
+    let sdk = sdk_path().ok_or_else(|| "xcrun --show-sdk-path unavailable".to_string())?;
419
+    let sdk_ver =
420
+        sdk_version().ok_or_else(|| "xcrun --show-sdk-version unavailable".to_string())?;
421
+    let work_dir = unique_temp_dir(&case.name)?;
422
+    let mut compiled: BTreeMap<String, PathBuf> = BTreeMap::new();
423
+    let mut sidecars: BTreeMap<String, PathBuf> = BTreeMap::new();
424
+    let mut artifacts: BTreeMap<String, PathBuf> = BTreeMap::new();
425
+    for input in &case.inputs {
426
+        let stem = input
427
+            .file_stem()
428
+            .and_then(|s| s.to_str())
429
+            .ok_or_else(|| format!("invalid input stem {}", input.display()))?;
430
+        match input.extension().and_then(|s| s.to_str()) {
431
+            Some("s") => {
432
+                let src = fs::read_to_string(input)
433
+                    .map_err(|e| format!("read parity input {}: {e}", input.display()))?;
434
+                let obj = work_dir.join(format!("{stem}.o"));
435
+                assemble(&src, &obj)?;
436
+                compiled.insert(format!("{stem}.o"), obj);
437
+            }
438
+            Some("c") => {
439
+                let src = fs::read_to_string(input)
440
+                    .map_err(|e| format!("read parity input {}: {e}", input.display()))?;
441
+                let obj = work_dir.join(format!("{stem}.o"));
442
+                compile_c(&src, &obj)?;
443
+                compiled.insert(format!("{stem}.o"), obj);
444
+            }
445
+            Some("o") | Some("a") | Some("tbd") => {
446
+                let copied = work_dir.join(
447
+                    input
448
+                        .file_name()
449
+                        .ok_or_else(|| format!("invalid input file name {}", input.display()))?,
450
+                );
451
+                fs::copy(input, &copied).map_err(|e| {
452
+                    format!(
453
+                        "copy parity input {} -> {}: {e}",
454
+                        input.display(),
455
+                        copied.display()
456
+                    )
457
+                })?;
458
+                compiled.insert(
459
+                    input
460
+                        .file_name()
461
+                        .and_then(|s| s.to_str())
462
+                        .ok_or_else(|| format!("invalid UTF-8 input file {}", input.display()))?
463
+                        .to_string(),
464
+                    copied,
465
+                );
466
+            }
467
+            other => {
468
+                return Err(format!(
469
+                    "unsupported parity input extension {:?} for {}",
470
+                    other,
471
+                    input.display()
472
+                ));
473
+            }
474
+        }
475
+    }
476
+    let files_dir = case.dir.join("files");
477
+    if files_dir.is_dir() {
478
+        for entry in fs::read_dir(&files_dir)
479
+            .map_err(|e| format!("read sidecar files for {}: {e}", case.name))?
480
+        {
481
+            let entry = entry.map_err(|e| format!("read sidecar entry for {}: {e}", case.name))?;
482
+            let src = entry.path();
483
+            if !src.is_file() {
484
+                continue;
485
+            }
486
+            let name = src
487
+                .file_name()
488
+                .and_then(|s| s.to_str())
489
+                .ok_or_else(|| format!("invalid sidecar file name {}", src.display()))?
490
+                .to_string();
491
+            let dst = work_dir.join(&name);
492
+            fs::copy(&src, &dst)
493
+                .map_err(|e| format!("copy sidecar {} -> {}: {e}", src.display(), dst.display()))?;
494
+            sidecars.insert(name, dst);
495
+        }
496
+    }
497
+    for artifact in &case.artifacts {
498
+        let src = case.dir.join("inputs").join(&artifact.src_name);
499
+        let src_contents = fs::read_to_string(&src)
500
+            .map_err(|e| format!("read artifact src {}: {e}", src.display()))?;
501
+        let out = work_dir.join(&artifact.out_name);
502
+        match artifact.kind {
503
+            ArtifactKind::ClangDylib => compile_dylib_c(&src_contents, &out)?,
504
+            ArtifactKind::ClangArchive => compile_archive_c(&src_contents, &out)?,
505
+            ArtifactKind::ClangReexportDylib => {
506
+                let dep_name = artifact.dep_name.as_ref().ok_or_else(|| {
507
+                    format!(
508
+                        "missing reexport dependency for artifact {}",
509
+                        artifact.out_name
510
+                    )
511
+                })?;
512
+                let dep = artifacts
513
+                    .get(dep_name)
514
+                    .ok_or_else(|| format!("unknown reexport dependency `{dep_name}`"))?;
515
+                compile_reexport_dylib_c(&src_contents, &out, dep)?;
516
+            }
517
+        }
518
+        artifacts.insert(artifact.out_name.clone(), out);
519
+    }
520
+
521
+    let suffix = if case.args.iter().any(|arg| arg == "-dylib") {
522
+        "dylib"
523
+    } else {
524
+        "out"
525
+    };
526
+    let our_path = work_dir.join(format!("ours.{suffix}"));
527
+    let their_path = work_dir.join(format!("apple.{suffix}"));
528
+
529
+    let our_args = expand_args(
530
+        &case.args, &compiled, &sidecars, &artifacts, &our_path, &sdk, &sdk_ver,
531
+    )?;
532
+    let their_args = expand_args(
533
+        &case.args,
534
+        &compiled,
535
+        &sidecars,
536
+        &artifacts,
537
+        &their_path,
538
+        &sdk,
539
+        &sdk_ver,
540
+    )?;
541
+
542
+    let our_output = Command::new(env!("CARGO_BIN_EXE_afs-ld"))
543
+        .args(&our_args)
544
+        .output()
545
+        .map_err(|e| format!("spawn afs-ld: {e}"))?;
546
+    if !our_output.status.success() {
547
+        return Err(format!(
548
+            "afs-ld failed for {}:\n{}",
549
+            case.name,
550
+            String::from_utf8_lossy(&our_output.stderr)
551
+        ));
552
+    }
553
+
554
+    let their_output = Command::new("xcrun")
555
+        .arg("ld")
556
+        .args(&their_args)
557
+        .output()
558
+        .map_err(|e| format!("spawn xcrun ld: {e}"))?;
559
+    if !their_output.status.success() {
560
+        return Err(format!(
561
+            "Apple ld failed for {}:\n{}",
562
+            case.name,
563
+            String::from_utf8_lossy(&their_output.stderr)
564
+        ));
565
+    }
566
+
567
+    let ours = fs::read(&our_path)
568
+        .map_err(|e| format!("read afs-ld output {}: {e}", our_path.display()))?;
569
+    let theirs = fs::read(&their_path)
570
+        .map_err(|e| format!("read Apple ld output {}: {e}", their_path.display()))?;
571
+
572
+    Ok(LinkOutputs {
573
+        ours,
574
+        theirs,
575
+        our_path,
576
+        their_path,
577
+    })
578
+}
579
+
580
+pub fn command_ids(bytes: &[u8]) -> Result<Vec<u32>, String> {
581
+    let header = parse_header(bytes).map_err(|e| format!("parse header: {e}"))?;
582
+    let commands = parse_commands(&header, bytes).map_err(|e| format!("parse commands: {e}"))?;
583
+    Ok(commands
584
+        .into_iter()
585
+        .map(|cmd| match cmd {
586
+            LoadCommand::Segment64(_) => LC_SEGMENT_64,
587
+            LoadCommand::Symtab(_) => LC_SYMTAB,
588
+            LoadCommand::Dysymtab(_) => LC_DYSYMTAB,
589
+            LoadCommand::BuildVersion(_) => LC_BUILD_VERSION,
590
+            LoadCommand::DyldInfoOnly(_) => LC_DYLD_INFO_ONLY,
591
+            LoadCommand::DyldChainedFixups(_) => LC_DYLD_CHAINED_FIXUPS,
592
+            LoadCommand::DyldExportsTrie(_) => LC_DYLD_EXPORTS_TRIE,
593
+            LoadCommand::Dylib(d) => d.cmd,
594
+            LoadCommand::Raw { cmd, .. } => cmd,
595
+            other => panic!("unexpected load command in command_ids helper: {other:?}"),
596
+        })
597
+        .collect())
598
+}
599
+
600
+pub fn compare_command_ids(ours: &[u8], theirs: &[u8], ignored: &[u32]) -> Result<(), String> {
601
+    let our_ids: Vec<u32> = command_ids(ours)?
602
+        .into_iter()
603
+        .filter(|cmd| !ignored.contains(cmd))
604
+        .collect();
605
+    let their_ids: Vec<u32> = command_ids(theirs)?
606
+        .into_iter()
607
+        .filter(|cmd| !ignored.contains(cmd))
608
+        .collect();
609
+    if our_ids != their_ids {
610
+        return Err(format!(
611
+            "load-command ids differ:\nours:   {our_ids:#x?}\ntheirs: {their_ids:#x?}"
612
+        ));
613
+    }
614
+    Ok(())
615
+}
616
+
617
+pub fn compare_command_details(
618
+    ours: &[u8],
619
+    theirs: &[u8],
620
+    checks: &[CommandCheck],
621
+) -> Result<(), String> {
622
+    for check in checks {
623
+        match check {
624
+            CommandCheck::BuildVersion => {
625
+                let ours = normalized_build_version(ours)?;
626
+                let theirs = normalized_build_version(theirs)?;
627
+                if ours != theirs {
628
+                    return Err(format!(
629
+                        "LC_BUILD_VERSION diverged:\nours:   {ours:#?}\ntheirs: {theirs:#?}"
630
+                    ));
631
+                }
632
+            }
633
+            CommandCheck::LoadDylibNames => {
634
+                let ours = load_dylib_names(ours)?;
635
+                let theirs = load_dylib_names(theirs)?;
636
+                if ours != theirs {
637
+                    return Err(format!(
638
+                        "LC_LOAD_DYLIB names diverged:\nours:   {ours:#?}\ntheirs: {theirs:#?}"
639
+                    ));
640
+                }
641
+            }
642
+            CommandCheck::ExportRecords => {
643
+                let ours = canonical_export_records(ours)?;
644
+                let theirs = canonical_export_records(theirs)?;
645
+                if ours != theirs {
646
+                    return Err(format!(
647
+                        "canonical export records diverged:\nours:   {ours:#?}\ntheirs: {theirs:#?}"
648
+                    ));
649
+                }
650
+            }
651
+            CommandCheck::SymbolRecordMap => {
652
+                let ours = canonical_symbol_record_map(ours)?;
653
+                let theirs = canonical_symbol_record_map(theirs)?;
654
+                if ours != theirs {
655
+                    return Err(format!(
656
+                        "canonical symbol record map diverged:\nours:   {ours:#?}\ntheirs: {theirs:#?}"
657
+                    ));
658
+                }
659
+            }
660
+            CommandCheck::IndirectSymbolIdentities => {
661
+                let ours = indirect_symbol_identities(ours)?;
662
+                let theirs = indirect_symbol_identities(theirs)?;
663
+                if ours != theirs {
664
+                    return Err(format!(
665
+                        "indirect symbol identities diverged:\nours:   {ours:#?}\ntheirs: {theirs:#?}"
666
+                    ));
667
+                }
668
+            }
669
+            CommandCheck::SymbolPartitionNames => {
670
+                let ours = symbol_partition_names(ours)?;
671
+                let theirs = symbol_partition_names(theirs)?;
672
+                if ours != theirs {
673
+                    return Err(format!(
674
+                        "symbol partition names diverged:\nours:   {ours:#?}\ntheirs: {theirs:#?}"
675
+                    ));
676
+                }
677
+            }
678
+            CommandCheck::StringTableNearParity => {
679
+                let our_len = raw_string_table(ours)?.len();
680
+                let their_len = raw_string_table(theirs)?.len();
681
+                if !string_table_within_five_percent(our_len, their_len) {
682
+                    return Err(format!(
683
+                        "string table length drifted too far from Apple ld: ours={} theirs={}",
684
+                        our_len, their_len
685
+                    ));
686
+                }
687
+            }
688
+            CommandCheck::FunctionStarts => {
689
+                let ours = decode_function_starts(ours)?;
690
+                let theirs = decode_function_starts(theirs)?;
691
+                if ours != theirs {
692
+                    return Err(format!(
693
+                        "function starts diverged:\nours:   {ours:#?}\ntheirs: {theirs:#?}"
694
+                    ));
695
+                }
696
+            }
697
+            CommandCheck::NormalizedFunctionStarts => {
698
+                let ours = normalize_function_start_offsets(&decode_function_starts(ours)?);
699
+                let theirs = normalize_function_start_offsets(&decode_function_starts(theirs)?);
700
+                if ours != theirs {
701
+                    return Err(format!(
702
+                        "normalized function starts diverged:\nours:   {ours:#?}\ntheirs: {theirs:#?}"
703
+                    ));
704
+                }
705
+            }
706
+            CommandCheck::DataInCode => {
707
+                let ours = canonical_data_in_code(ours)?;
708
+                let theirs = canonical_data_in_code(theirs)?;
709
+                if ours != theirs {
710
+                    return Err(format!(
711
+                        "canonical data-in-code records diverged:\nours:   {ours:#?}\ntheirs: {theirs:#?}"
712
+                    ));
713
+                }
714
+            }
715
+            CommandCheck::RebasedUnwindBytes => {
716
+                let ours = rebased_unwind_bytes(ours)?;
717
+                let theirs = rebased_unwind_bytes(theirs)?;
718
+                if ours != theirs {
719
+                    return Err("rebased unwind bytes diverged".to_string());
720
+                }
721
+            }
722
+            CommandCheck::DyldInfoRebase => {
723
+                let ours = dyld_info_stream(ours, DyldInfoStreamKind::Rebase)?;
724
+                let theirs = dyld_info_stream(theirs, DyldInfoStreamKind::Rebase)?;
725
+                if ours != theirs {
726
+                    return Err("rebase stream diverged".to_string());
727
+                }
728
+            }
729
+            CommandCheck::DyldInfoBind => {
730
+                let ours = dyld_info_stream(ours, DyldInfoStreamKind::Bind)?;
731
+                let theirs = dyld_info_stream(theirs, DyldInfoStreamKind::Bind)?;
732
+                if ours != theirs {
733
+                    return Err("bind stream diverged".to_string());
734
+                }
735
+            }
736
+            CommandCheck::DyldInfoWeakBind => {
737
+                let ours = dyld_info_stream(ours, DyldInfoStreamKind::WeakBind)?;
738
+                let theirs = dyld_info_stream(theirs, DyldInfoStreamKind::WeakBind)?;
739
+                if ours != theirs {
740
+                    return Err("weak-bind stream diverged".to_string());
741
+                }
742
+            }
743
+            CommandCheck::DyldInfoLazyBind => {
744
+                let ours = dyld_info_stream(ours, DyldInfoStreamKind::LazyBind)?;
745
+                let theirs = dyld_info_stream(theirs, DyldInfoStreamKind::LazyBind)?;
746
+                if ours != theirs {
747
+                    return Err("lazy-bind stream diverged".to_string());
748
+                }
749
+            }
750
+        }
751
+    }
752
+    Ok(())
753
+}
754
+
755
+pub fn ensure_absent_load_commands(
756
+    bytes: &[u8],
757
+    commands: &[u32],
758
+    side: &str,
759
+) -> Result<(), String> {
760
+    let ids = command_ids(bytes)?;
761
+    for command in commands {
762
+        if ids.contains(command) {
763
+            return Err(format!(
764
+                "{side} unexpectedly emitted {}",
765
+                load_command_name(*command)
766
+            ));
767
+        }
768
+    }
769
+    Ok(())
770
+}
771
+
772
+pub fn ensure_absent_sections(
773
+    bytes: &[u8],
774
+    sections: &[(String, String)],
775
+    side: &str,
776
+) -> Result<(), String> {
777
+    for (segname, sectname) in sections {
778
+        if output_section(bytes, segname, sectname).is_some() {
779
+            return Err(format!(
780
+                "{side} unexpectedly emitted section {segname},{sectname}"
781
+            ));
782
+        }
783
+    }
784
+    Ok(())
785
+}
786
+
787
+pub fn output_section(bytes: &[u8], segname: &str, sectname: &str) -> Option<(u64, Vec<u8>)> {
788
+    let header = parse_header(bytes).ok()?;
789
+    let commands = parse_commands(&header, bytes).ok()?;
790
+    for cmd in commands {
791
+        if let LoadCommand::Segment64(seg) = cmd {
792
+            for section in seg.sections {
793
+                if section.segname_str() == segname && section.sectname_str() == sectname {
794
+                    let data = if section.offset == 0 {
795
+                        Vec::new()
796
+                    } else {
797
+                        let start = section.offset as usize;
798
+                        let end = start + section.size as usize;
799
+                        bytes.get(start..end)?.to_vec()
800
+                    };
801
+                    return Some((section.addr, data));
802
+                }
803
+            }
804
+        }
805
+    }
806
+    None
807
+}
808
+
809
+fn output_section_header(bytes: &[u8], segname: &str, sectname: &str) -> Option<Section64Header> {
810
+    let header = parse_header(bytes).ok()?;
811
+    let commands = parse_commands(&header, bytes).ok()?;
812
+    for cmd in commands {
813
+        if let LoadCommand::Segment64(seg) = cmd {
814
+            for section in seg.sections {
815
+                if section.segname_str() == segname && section.sectname_str() == sectname {
816
+                    return Some(section);
817
+                }
818
+            }
819
+        }
820
+    }
821
+    None
822
+}
823
+
824
+fn segment_vmaddr(bytes: &[u8], segname: &str) -> Option<u64> {
825
+    let header = parse_header(bytes).ok()?;
826
+    let commands = parse_commands(&header, bytes).ok()?;
827
+    for cmd in commands {
828
+        if let LoadCommand::Segment64(seg) = cmd {
829
+            if seg.segname_str() == segname {
830
+                return Some(seg.vmaddr);
831
+            }
832
+        }
833
+    }
834
+    None
835
+}
836
+
837
+pub fn compare_sections(
838
+    ours: &[u8],
839
+    theirs: &[u8],
840
+    sections: &[(String, String)],
841
+    case_tolerances: &[CaseTolerance],
842
+) -> Result<(), String> {
843
+    for (segname, sectname) in sections {
844
+        let (_, our_bytes) = output_section(ours, segname, sectname)
845
+            .ok_or_else(|| format!("missing section {segname},{sectname} in afs-ld output"))?;
846
+        let (_, their_bytes) = output_section(theirs, segname, sectname)
847
+            .ok_or_else(|| format!("missing section {segname},{sectname} in Apple output"))?;
848
+        let diff = apply_section_tolerances(
849
+            diff_macho(&our_bytes, &their_bytes),
850
+            segname,
851
+            sectname,
852
+            case_tolerances,
853
+        );
854
+        if !diff.is_clean() {
855
+            return Err(format!(
856
+                "section bytes differ for {segname},{sectname}: {:#?}",
857
+                diff.critical
858
+            ));
859
+        }
860
+    }
861
+    Ok(())
862
+}
863
+
864
+pub fn compare_page_refs(
865
+    ours: &[u8],
866
+    theirs: &[u8],
867
+    checks: &[PageRefCheck],
868
+) -> Result<(), String> {
869
+    if checks.is_empty() {
870
+        return Ok(());
871
+    }
872
+    let our_symbols = symbol_values(ours)?;
873
+    let their_symbols = symbol_values(theirs)?;
874
+    for check in checks {
875
+        let (our_addr, our_bytes) = output_section(ours, &check.segname, &check.sectname)
876
+            .ok_or_else(|| {
877
+                format!(
878
+                    "missing section {},{} in afs-ld output",
879
+                    check.segname, check.sectname
880
+                )
881
+            })?;
882
+        let (their_addr, their_bytes) = output_section(theirs, &check.segname, &check.sectname)
883
+            .ok_or_else(|| {
884
+                format!(
885
+                    "missing section {},{} in Apple output",
886
+                    check.segname, check.sectname
887
+                )
888
+            })?;
889
+        let our_target =
890
+            decode_page_reference(&our_bytes, our_addr, check.site_offset, check.kind)?;
891
+        let their_target =
892
+            decode_page_reference(&their_bytes, their_addr, check.site_offset, check.kind)?;
893
+        let expected_ours = *our_symbols
894
+            .get(&check.symbol)
895
+            .ok_or_else(|| format!("missing symbol {} in afs-ld output", check.symbol))?;
896
+        let expected_theirs = *their_symbols
897
+            .get(&check.symbol)
898
+            .ok_or_else(|| format!("missing symbol {} in Apple output", check.symbol))?;
899
+        if our_target != expected_ours || their_target != expected_theirs {
900
+            return Err(format!(
901
+                "page ref {},{}+0x{:x} -> {} diverged: ours=0x{:x} expected=0x{:x}; theirs=0x{:x} expected=0x{:x}",
902
+                check.segname,
903
+                check.sectname,
904
+                check.site_offset,
905
+                check.symbol,
906
+                our_target,
907
+                expected_ours,
908
+                their_target,
909
+                expected_theirs,
910
+            ));
911
+        }
912
+    }
913
+    Ok(())
914
+}
915
+
916
+pub fn run_program(path: &Path, args: &[String]) -> Result<ProgramOutput, String> {
917
+    let output = Command::new(path)
918
+        .args(args)
919
+        .output()
920
+        .map_err(|e| format!("run {}: {e}", path.display()))?;
921
+    Ok(ProgramOutput {
922
+        exit_code: output.status.code(),
923
+        stdout: output.stdout,
924
+        stderr: output.stderr,
925
+    })
926
+}
927
+
928
+pub fn compare_runtime(our_path: &Path, their_path: &Path, args: &[String]) -> Result<(), String> {
929
+    let ours = run_program(our_path, args)?;
930
+    let theirs = run_program(their_path, args)?;
931
+    if ours != theirs {
932
+        return Err(format!(
933
+            "runtime differs:\nours: exit={:?} stdout={:?} stderr={:?}\ntheirs: exit={:?} stdout={:?} stderr={:?}",
934
+            ours.exit_code,
935
+            String::from_utf8_lossy(&ours.stdout),
936
+            String::from_utf8_lossy(&ours.stderr),
937
+            theirs.exit_code,
938
+            String::from_utf8_lossy(&theirs.stdout),
939
+            String::from_utf8_lossy(&theirs.stderr),
940
+        ));
941
+    }
942
+    Ok(())
943
+}
944
+
945
+/// Byte-level diff between two Mach-O images or section byte slices.
946
+///
947
+/// Sprint 27 starts tolerating a very small allowlist: UUID bytes, dylib
948
+/// timestamp fields, and code-signature command/blob bytes at matching
949
+/// offsets. Unknown diffs remain critical.
54950
 pub fn diff_macho(ours: &[u8], theirs: &[u8]) -> DiffReport {
55951
     let mut report = DiffReport::default();
56952
 
@@ -68,29 +964,1085 @@ pub fn diff_macho(ours: &[u8], theirs: &[u8]) -> DiffReport {
68964
         return report;
69965
     }
70966
 
967
+    let our_mask = tolerated_mask(ours);
968
+    let their_mask = tolerated_mask(theirs);
969
+
71970
     let mut i = 0;
72971
     while i < ours.len() {
73
-        if ours[i] != theirs[i] {
74
-            let start = i;
75
-            while i < ours.len() && ours[i] != theirs[i] {
76
-                i += 1;
972
+        if ours[i] == theirs[i] {
973
+            i += 1;
974
+            continue;
975
+        }
976
+
977
+        let tolerated_reason = match (our_mask[i], their_mask[i]) {
978
+            (Some(left), Some(right)) if left == right => Some(left),
979
+            _ => None,
980
+        };
981
+        let start = i;
982
+        i += 1;
983
+        while i < ours.len() && ours[i] != theirs[i] {
984
+            let same_category = match tolerated_reason {
985
+                Some(reason) => matches!(
986
+                    (our_mask[i], their_mask[i]),
987
+                    (Some(left), Some(right)) if left == reason && right == reason
988
+                ),
989
+                None => !matches!(
990
+                    (our_mask[i], their_mask[i]),
991
+                    (Some(left), Some(right)) if left == right
992
+                ),
993
+            };
994
+            if !same_category {
995
+                break;
77996
             }
997
+            i += 1;
998
+        }
999
+
1000
+        let len = i - start;
1001
+        if let Some(reason) = tolerated_reason {
1002
+            report.tolerated.push(DiffChunk {
1003
+                offset: start,
1004
+                len,
1005
+                reason: reason.to_string(),
1006
+                category: DiffCategory::Tolerated(reason),
1007
+            });
1008
+        } else {
781009
             report.critical.push(DiffChunk {
791010
                 offset: start,
80
-                len: i - start,
81
-                reason: format!("{} byte(s) differ starting at 0x{start:x}", i - start),
1011
+                len,
1012
+                reason: format!("{} byte(s) differ starting at 0x{start:x}", len),
821013
                 category: DiffCategory::Critical,
831014
             });
84
-        } else {
85
-            i += 1;
861015
         }
871016
     }
881017
 
891018
     report
901019
 }
911020
 
92
-/// Placeholder for the full linker-spawning contract. Sprint 18 wires this to
93
-/// real invocations of afs-ld and the system `ld` via `xcrun -f ld`.
94
-pub fn link_both(_case: &LinkCase) -> LinkOutputs {
95
-    panic!("link_both is not implemented until Sprint 18 (hello-world milestone)");
1021
+pub fn parse_case_tolerances(notes: Option<&str>) -> Result<Vec<CaseTolerance>, String> {
1022
+    let Some(notes) = notes else {
1023
+        return Ok(Vec::new());
1024
+    };
1025
+
1026
+    let mut tolerances = Vec::new();
1027
+    let mut in_block = false;
1028
+    for raw_line in notes.lines() {
1029
+        let line = raw_line.trim();
1030
+        if line.is_empty() {
1031
+            continue;
1032
+        }
1033
+        if line == "tolerated:" {
1034
+            in_block = true;
1035
+            continue;
1036
+        }
1037
+        if !in_block {
1038
+            continue;
1039
+        }
1040
+        if !line.starts_with("- region:") {
1041
+            // Stop once the simple tolerated block ends.
1042
+            if !line.starts_with('#') && !raw_line.starts_with(' ') && !raw_line.starts_with('\t') {
1043
+                break;
1044
+            }
1045
+            continue;
1046
+        }
1047
+        tolerances.push(parse_case_tolerance_line(line)?);
1048
+    }
1049
+    Ok(tolerances)
1050
+}
1051
+
1052
+pub fn apply_section_tolerances(
1053
+    mut diff: DiffReport,
1054
+    segname: &str,
1055
+    sectname: &str,
1056
+    case_tolerances: &[CaseTolerance],
1057
+) -> DiffReport {
1058
+    if diff.critical.is_empty() || case_tolerances.is_empty() {
1059
+        return diff;
1060
+    }
1061
+
1062
+    let mut remaining = Vec::new();
1063
+    for chunk in diff.critical.drain(..) {
1064
+        let tolerated = case_tolerances
1065
+            .iter()
1066
+            .find(|tol| tolerance_covers_chunk(tol, segname, sectname, chunk.offset, chunk.len));
1067
+        if let Some(tol) = tolerated {
1068
+            diff.tolerated.push(DiffChunk {
1069
+                offset: chunk.offset,
1070
+                len: chunk.len,
1071
+                reason: tol.reason.clone(),
1072
+                category: DiffCategory::Tolerated("case-note"),
1073
+            });
1074
+        } else {
1075
+            remaining.push(chunk);
1076
+        }
1077
+    }
1078
+    diff.critical = remaining;
1079
+    diff
1080
+}
1081
+
1082
+fn unique_temp_dir(case_name: &str) -> Result<PathBuf, String> {
1083
+    let stamp = SystemTime::now()
1084
+        .duration_since(UNIX_EPOCH)
1085
+        .map_err(|e| format!("clock error: {e}"))?
1086
+        .as_nanos();
1087
+    let safe_name = case_name.replace(['/', ' '], "-");
1088
+    let dir = std::env::temp_dir().join(format!(
1089
+        "afs-ld-parity-{}-{safe_name}-{stamp}",
1090
+        std::process::id()
1091
+    ));
1092
+    fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
1093
+    Ok(dir)
1094
+}
1095
+
1096
+fn parse_case_tolerance_line(line: &str) -> Result<CaseTolerance, String> {
1097
+    let rest = line
1098
+        .strip_prefix("- region:")
1099
+        .ok_or_else(|| format!("invalid tolerance line `{line}`"))?
1100
+        .trim();
1101
+    let (before_reason, reason_part) = rest
1102
+        .split_once(" reason:")
1103
+        .ok_or_else(|| format!("missing `reason:` in tolerance line `{line}`"))?;
1104
+    let (region_part, bytes_part) = before_reason
1105
+        .split_once(" bytes ")
1106
+        .ok_or_else(|| format!("missing `bytes` range in tolerance line `{line}`"))?;
1107
+    let reason = reason_part.trim().trim_matches('"').to_string();
1108
+    if reason.is_empty() {
1109
+        return Err(format!("empty tolerance reason in `{line}`"));
1110
+    }
1111
+    let (start, end_inclusive) = parse_tolerance_range(bytes_part.trim())?;
1112
+    let region_token = region_part.trim();
1113
+    let (segname, sectname) = match region_token.split_once(',') {
1114
+        Some((segname, sectname)) => (
1115
+            segname.trim().to_string(),
1116
+            Some(sectname.trim().to_string()),
1117
+        ),
1118
+        None => (region_token.to_string(), None),
1119
+    };
1120
+    if segname.is_empty() {
1121
+        return Err(format!("empty tolerance region in `{line}`"));
1122
+    }
1123
+    Ok(CaseTolerance {
1124
+        region: ToleranceRegion::SectionBytes {
1125
+            segname,
1126
+            sectname,
1127
+            start,
1128
+            end_inclusive,
1129
+        },
1130
+        reason,
1131
+    })
1132
+}
1133
+
1134
+fn parse_tolerance_range(range: &str) -> Result<(usize, usize), String> {
1135
+    let (start, end) = range
1136
+        .split_once('-')
1137
+        .ok_or_else(|| format!("invalid tolerance range `{range}`"))?;
1138
+    let start = parse_usize(start.trim())?;
1139
+    let end = parse_usize(end.trim())?;
1140
+    if end < start {
1141
+        return Err(format!("tolerance range end before start in `{range}`"));
1142
+    }
1143
+    Ok((start, end))
1144
+}
1145
+
1146
+fn parse_usize(token: &str) -> Result<usize, String> {
1147
+    if let Some(rest) = token.strip_prefix("0x") {
1148
+        usize::from_str_radix(rest, 16).map_err(|e| format!("parse usize `{token}`: {e}"))
1149
+    } else {
1150
+        token
1151
+            .parse::<usize>()
1152
+            .map_err(|e| format!("parse usize `{token}`: {e}"))
1153
+    }
1154
+}
1155
+
1156
+fn tolerance_covers_chunk(
1157
+    tolerance: &CaseTolerance,
1158
+    segname: &str,
1159
+    sectname: &str,
1160
+    offset: usize,
1161
+    len: usize,
1162
+) -> bool {
1163
+    match &tolerance.region {
1164
+        ToleranceRegion::SectionBytes {
1165
+            segname: expected_seg,
1166
+            sectname: expected_sect,
1167
+            start,
1168
+            end_inclusive,
1169
+        } => {
1170
+            if expected_seg != segname {
1171
+                return false;
1172
+            }
1173
+            if let Some(expected_sect) = expected_sect {
1174
+                if expected_sect != sectname {
1175
+                    return false;
1176
+                }
1177
+            }
1178
+            let end = offset.saturating_add(len.saturating_sub(1));
1179
+            offset >= *start && end <= *end_inclusive
1180
+        }
1181
+    }
1182
+}
1183
+
1184
+fn read_tokens(path: &Path) -> Result<Vec<String>, String> {
1185
+    let contents = fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?;
1186
+    Ok(contents
1187
+        .lines()
1188
+        .map(str::trim)
1189
+        .filter(|line| !line.is_empty() && !line.starts_with('#'))
1190
+        .map(ToOwned::to_owned)
1191
+        .collect())
1192
+}
1193
+
1194
+fn read_tokens_if_present(path: &Path) -> Result<Vec<String>, String> {
1195
+    if path.exists() {
1196
+        read_tokens(path)
1197
+    } else {
1198
+        Ok(Vec::new())
1199
+    }
1200
+}
1201
+
1202
+fn read_sections(path: &Path) -> Result<Vec<(String, String)>, String> {
1203
+    let mut sections = Vec::new();
1204
+    for line in read_tokens(path)? {
1205
+        let mut parts = line.split_whitespace();
1206
+        let segname = parts
1207
+            .next()
1208
+            .ok_or_else(|| format!("missing segment name in {}", path.display()))?;
1209
+        let sectname = parts
1210
+            .next()
1211
+            .ok_or_else(|| format!("missing section name in {}", path.display()))?;
1212
+        if parts.next().is_some() {
1213
+            return Err(format!(
1214
+                "too many fields in section spec `{line}` from {}",
1215
+                path.display()
1216
+            ));
1217
+        }
1218
+        sections.push((segname.to_string(), sectname.to_string()));
1219
+    }
1220
+    Ok(sections)
1221
+}
1222
+
1223
+fn read_sections_if_present(path: &Path) -> Result<Vec<(String, String)>, String> {
1224
+    if path.exists() {
1225
+        read_sections(path)
1226
+    } else {
1227
+        Ok(Vec::new())
1228
+    }
1229
+}
1230
+
1231
+fn read_load_command_names(path: &Path) -> Result<Vec<u32>, String> {
1232
+    if !path.exists() {
1233
+        return Ok(Vec::new());
1234
+    }
1235
+    let mut commands = Vec::new();
1236
+    for line in read_tokens(path)? {
1237
+        commands.push(parse_load_command_name(&line)?);
1238
+    }
1239
+    Ok(commands)
1240
+}
1241
+
1242
+fn read_command_checks(path: &Path) -> Result<Vec<CommandCheck>, String> {
1243
+    if !path.exists() {
1244
+        return Ok(Vec::new());
1245
+    }
1246
+    let mut checks = Vec::new();
1247
+    for line in read_tokens(path)? {
1248
+        checks.push(parse_command_check(&line)?);
1249
+    }
1250
+    Ok(checks)
1251
+}
1252
+
1253
+fn read_page_refs(path: &Path) -> Result<Vec<PageRefCheck>, String> {
1254
+    if !path.exists() {
1255
+        return Ok(Vec::new());
1256
+    }
1257
+    let mut checks = Vec::new();
1258
+    for line in read_tokens(path)? {
1259
+        let mut parts = line.split_whitespace();
1260
+        let segname = parts
1261
+            .next()
1262
+            .ok_or_else(|| format!("missing segment name in {}", path.display()))?;
1263
+        let sectname = parts
1264
+            .next()
1265
+            .ok_or_else(|| format!("missing section name in {}", path.display()))?;
1266
+        let site_offset = parts
1267
+            .next()
1268
+            .ok_or_else(|| format!("missing site offset in {}", path.display()))?;
1269
+        let kind = parts
1270
+            .next()
1271
+            .ok_or_else(|| format!("missing page-ref kind in {}", path.display()))?;
1272
+        let symbol = parts
1273
+            .next()
1274
+            .ok_or_else(|| format!("missing symbol name in {}", path.display()))?;
1275
+        if parts.next().is_some() {
1276
+            return Err(format!(
1277
+                "too many fields in page-ref spec `{line}` from {}",
1278
+                path.display()
1279
+            ));
1280
+        }
1281
+        checks.push(PageRefCheck {
1282
+            segname: segname.to_string(),
1283
+            sectname: sectname.to_string(),
1284
+            site_offset: parse_u64(site_offset)?,
1285
+            kind: parse_page_ref_kind(kind)?,
1286
+            symbol: symbol.to_string(),
1287
+        });
1288
+    }
1289
+    Ok(checks)
1290
+}
1291
+
1292
+fn read_artifacts(path: &Path) -> Result<Vec<ArtifactSpec>, String> {
1293
+    if !path.exists() {
1294
+        return Ok(Vec::new());
1295
+    }
1296
+    let mut specs = Vec::new();
1297
+    for line in read_tokens(path)? {
1298
+        let mut parts = line.split_whitespace();
1299
+        let kind = parts
1300
+            .next()
1301
+            .ok_or_else(|| format!("missing artifact kind in {}", path.display()))?;
1302
+        let src_name = parts
1303
+            .next()
1304
+            .ok_or_else(|| format!("missing artifact src in {}", path.display()))?;
1305
+        let out_name = parts
1306
+            .next()
1307
+            .ok_or_else(|| format!("missing artifact output in {}", path.display()))?;
1308
+        let dep_name = parts.next().map(str::to_string);
1309
+        if parts.next().is_some() {
1310
+            return Err(format!(
1311
+                "too many fields in artifact spec `{line}` from {}",
1312
+                path.display()
1313
+            ));
1314
+        }
1315
+        let (kind, dep_name) = match kind {
1316
+            "clang_dylib" => {
1317
+                if dep_name.is_some() {
1318
+                    return Err(format!(
1319
+                        "clang_dylib takes exactly 3 fields in {}",
1320
+                        path.display()
1321
+                    ));
1322
+                }
1323
+                (ArtifactKind::ClangDylib, None)
1324
+            }
1325
+            "clang_archive" => {
1326
+                if dep_name.is_some() {
1327
+                    return Err(format!(
1328
+                        "clang_archive takes exactly 3 fields in {}",
1329
+                        path.display()
1330
+                    ));
1331
+                }
1332
+                (ArtifactKind::ClangArchive, None)
1333
+            }
1334
+            "clang_reexport_dylib" => {
1335
+                let dep_name = dep_name.ok_or_else(|| {
1336
+                    format!(
1337
+                        "clang_reexport_dylib needs a dependency artifact in {}",
1338
+                        path.display()
1339
+                    )
1340
+                })?;
1341
+                (ArtifactKind::ClangReexportDylib, Some(dep_name))
1342
+            }
1343
+            other => return Err(format!("unknown artifact kind `{other}`")),
1344
+        };
1345
+        specs.push(ArtifactSpec {
1346
+            src_name: src_name.to_string(),
1347
+            out_name: out_name.to_string(),
1348
+            kind,
1349
+            dep_name,
1350
+        });
1351
+    }
1352
+    Ok(specs)
1353
+}
1354
+
1355
+fn parse_command_check(name: &str) -> Result<CommandCheck, String> {
1356
+    match name {
1357
+        "build_version" => Ok(CommandCheck::BuildVersion),
1358
+        "load_dylib_names" => Ok(CommandCheck::LoadDylibNames),
1359
+        "export_records" => Ok(CommandCheck::ExportRecords),
1360
+        "symbol_record_map" => Ok(CommandCheck::SymbolRecordMap),
1361
+        "indirect_symbol_identities" => Ok(CommandCheck::IndirectSymbolIdentities),
1362
+        "symbol_partition_names" => Ok(CommandCheck::SymbolPartitionNames),
1363
+        "string_table_near_parity" => Ok(CommandCheck::StringTableNearParity),
1364
+        "function_starts" => Ok(CommandCheck::FunctionStarts),
1365
+        "normalized_function_starts" => Ok(CommandCheck::NormalizedFunctionStarts),
1366
+        "data_in_code" => Ok(CommandCheck::DataInCode),
1367
+        "rebased_unwind_bytes" => Ok(CommandCheck::RebasedUnwindBytes),
1368
+        "dyld_info_rebase" => Ok(CommandCheck::DyldInfoRebase),
1369
+        "dyld_info_bind" => Ok(CommandCheck::DyldInfoBind),
1370
+        "dyld_info_weak_bind" => Ok(CommandCheck::DyldInfoWeakBind),
1371
+        "dyld_info_lazy_bind" => Ok(CommandCheck::DyldInfoLazyBind),
1372
+        other => Err(format!("unknown command check `{other}`")),
1373
+    }
1374
+}
1375
+
1376
+fn parse_page_ref_kind(kind: &str) -> Result<PageRefKind, String> {
1377
+    match kind {
1378
+        "add" => Ok(PageRefKind::Add),
1379
+        "load" => Ok(PageRefKind::Load),
1380
+        other => Err(format!("unknown page-ref kind `{other}`")),
1381
+    }
1382
+}
1383
+
1384
+fn parse_load_command_name(name: &str) -> Result<u32, String> {
1385
+    match name {
1386
+        "LC_SEGMENT_64" => Ok(LC_SEGMENT_64),
1387
+        "LC_LOAD_DYLIB" => Ok(LC_LOAD_DYLIB),
1388
+        "LC_UUID" => Ok(LC_UUID),
1389
+        "LC_CODE_SIGNATURE" => Ok(LC_CODE_SIGNATURE),
1390
+        "LC_LINKER_OPTIMIZATION_HINT" => Ok(afs_ld::macho::constants::LC_LINKER_OPTIMIZATION_HINT),
1391
+        other => Err(format!("unknown load command name `{other}`")),
1392
+    }
1393
+}
1394
+
1395
+fn load_command_name(cmd: u32) -> &'static str {
1396
+    match cmd {
1397
+        LC_SEGMENT_64 => "LC_SEGMENT_64",
1398
+        LC_LOAD_DYLIB => "LC_LOAD_DYLIB",
1399
+        LC_UUID => "LC_UUID",
1400
+        LC_CODE_SIGNATURE => "LC_CODE_SIGNATURE",
1401
+        afs_ld::macho::constants::LC_LINKER_OPTIMIZATION_HINT => "LC_LINKER_OPTIMIZATION_HINT",
1402
+        _ => "unknown load command",
1403
+    }
1404
+}
1405
+
1406
+fn parse_u64(value: &str) -> Result<u64, String> {
1407
+    if let Some(hex) = value.strip_prefix("0x") {
1408
+        u64::from_str_radix(hex, 16).map_err(|e| format!("parse hex `{value}`: {e}"))
1409
+    } else {
1410
+        value
1411
+            .parse::<u64>()
1412
+            .map_err(|e| format!("parse integer `{value}`: {e}"))
1413
+    }
1414
+}
1415
+
1416
+fn expand_args(
1417
+    args: &[String],
1418
+    compiled: &BTreeMap<String, PathBuf>,
1419
+    sidecars: &BTreeMap<String, PathBuf>,
1420
+    artifacts: &BTreeMap<String, PathBuf>,
1421
+    out: &Path,
1422
+    sdk: &str,
1423
+    sdk_ver: &str,
1424
+) -> Result<Vec<String>, String> {
1425
+    let mut expanded = Vec::with_capacity(args.len());
1426
+    for arg in args {
1427
+        if arg == "@OUT@" {
1428
+            expanded.push(out.to_string_lossy().to_string());
1429
+            continue;
1430
+        }
1431
+        if arg == "@SDK_PATH@" {
1432
+            expanded.push(sdk.to_string());
1433
+            continue;
1434
+        }
1435
+        if arg == "@SDK_VERSION@" {
1436
+            expanded.push(sdk_ver.to_string());
1437
+            continue;
1438
+        }
1439
+        if let Some(rel) = arg
1440
+            .strip_prefix("@SDK_TBD:")
1441
+            .and_then(|rest| rest.strip_suffix('@'))
1442
+        {
1443
+            expanded.push(Path::new(sdk).join(rel).to_string_lossy().to_string());
1444
+            continue;
1445
+        }
1446
+        if let Some(name) = arg
1447
+            .strip_prefix("@INPUT:")
1448
+            .and_then(|rest| rest.strip_suffix('@'))
1449
+        {
1450
+            let input = compiled
1451
+                .get(name)
1452
+                .ok_or_else(|| format!("unknown parity input placeholder `{name}`"))?;
1453
+            expanded.push(input.to_string_lossy().to_string());
1454
+            continue;
1455
+        }
1456
+        if let Some(name) = arg
1457
+            .strip_prefix("@FILE:")
1458
+            .and_then(|rest| rest.strip_suffix('@'))
1459
+        {
1460
+            let file = sidecars
1461
+                .get(name)
1462
+                .ok_or_else(|| format!("unknown parity sidecar placeholder `{name}`"))?;
1463
+            expanded.push(file.to_string_lossy().to_string());
1464
+            continue;
1465
+        }
1466
+        if let Some(name) = arg
1467
+            .strip_prefix("@ARTIFACT:")
1468
+            .and_then(|rest| rest.strip_suffix('@'))
1469
+        {
1470
+            let artifact = artifacts
1471
+                .get(name)
1472
+                .ok_or_else(|| format!("unknown parity artifact placeholder `{name}`"))?;
1473
+            expanded.push(artifact.to_string_lossy().to_string());
1474
+            continue;
1475
+        }
1476
+        expanded.push(arg.clone());
1477
+    }
1478
+    Ok(expanded)
1479
+}
1480
+
1481
+fn tolerated_mask(bytes: &[u8]) -> Vec<Option<&'static str>> {
1482
+    let mut mask = vec![None; bytes.len()];
1483
+    let Ok(header) = parse_header(bytes) else {
1484
+        return mask;
1485
+    };
1486
+    let cmd_base = 32usize;
1487
+    let Ok(cmd_limit) = cmd_base.checked_add(header.sizeofcmds as usize).ok_or(()) else {
1488
+        return mask;
1489
+    };
1490
+    if cmd_limit > bytes.len() {
1491
+        return mask;
1492
+    }
1493
+
1494
+    let mut cursor = cmd_base;
1495
+    for _ in 0..header.ncmds {
1496
+        if cursor + 8 > cmd_limit {
1497
+            break;
1498
+        }
1499
+        let cmd = u32_le(&bytes[cursor..cursor + 4]);
1500
+        let cmdsize = u32_le(&bytes[cursor + 4..cursor + 8]) as usize;
1501
+        if cmdsize < 8 || cursor + cmdsize > cmd_limit {
1502
+            break;
1503
+        }
1504
+        match cmd {
1505
+            LC_UUID => mark_range(&mut mask, cursor, cursor + cmdsize, "UUID bytes"),
1506
+            LC_CODE_SIGNATURE => {
1507
+                mark_range(
1508
+                    &mut mask,
1509
+                    cursor,
1510
+                    cursor + cmdsize,
1511
+                    "code-signature load command",
1512
+                );
1513
+                if cmdsize >= 16 {
1514
+                    let dataoff = u32_le(&bytes[cursor + 8..cursor + 12]) as usize;
1515
+                    let datasize = u32_le(&bytes[cursor + 12..cursor + 16]) as usize;
1516
+                    if let Some(end) = dataoff.checked_add(datasize) {
1517
+                        if end <= bytes.len() {
1518
+                            mark_range(&mut mask, dataoff, end, "code-signature hashes");
1519
+                        }
1520
+                    }
1521
+                }
1522
+            }
1523
+            LC_ID_DYLIB | LC_LOAD_DYLIB | LC_LOAD_WEAK_DYLIB | LC_REEXPORT_DYLIB
1524
+            | LC_LOAD_UPWARD_DYLIB => {
1525
+                if cmdsize >= 16 {
1526
+                    mark_range(&mut mask, cursor + 12, cursor + 16, "dylib timestamp");
1527
+                }
1528
+            }
1529
+            _ => {}
1530
+        }
1531
+        cursor += cmdsize;
1532
+    }
1533
+
1534
+    mask
1535
+}
1536
+
1537
+fn mark_range(mask: &mut [Option<&'static str>], start: usize, end: usize, reason: &'static str) {
1538
+    let start = start.min(mask.len());
1539
+    let end = end.min(mask.len());
1540
+    for slot in &mut mask[start..end] {
1541
+        *slot = Some(reason);
1542
+    }
1543
+}
1544
+
1545
+fn build_version_command(bytes: &[u8]) -> Result<Option<BuildVersionCmd>, String> {
1546
+    let header = parse_header(bytes).map_err(|e| e.to_string())?;
1547
+    let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
1548
+    Ok(commands.into_iter().find_map(|cmd| match cmd {
1549
+        LoadCommand::BuildVersion(cmd) => Some(cmd),
1550
+        _ => None,
1551
+    }))
1552
+}
1553
+
1554
+fn normalized_build_version(bytes: &[u8]) -> Result<Option<NormalizedBuildVersion>, String> {
1555
+    Ok(build_version_command(bytes)?.map(|cmd| {
1556
+        (
1557
+            cmd.platform,
1558
+            cmd.minos,
1559
+            cmd.sdk,
1560
+            cmd.tools.into_iter().map(|tool| tool.tool).collect(),
1561
+        )
1562
+    }))
1563
+}
1564
+
1565
+fn load_dylib_names(bytes: &[u8]) -> Result<Vec<String>, String> {
1566
+    let header = parse_header(bytes).map_err(|e| e.to_string())?;
1567
+    let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
1568
+    Ok(commands
1569
+        .into_iter()
1570
+        .filter_map(|cmd| match cmd {
1571
+            LoadCommand::Dylib(cmd) if cmd.cmd == LC_LOAD_DYLIB => Some(cmd.name),
1572
+            _ => None,
1573
+        })
1574
+        .collect())
1575
+}
1576
+
1577
+#[derive(Debug, Clone, PartialEq, Eq)]
1578
+struct CanonicalSymbolRecord {
1579
+    name: String,
1580
+    n_type: u8,
1581
+    n_sect: u8,
1582
+    n_desc: u16,
1583
+    value: u64,
1584
+}
1585
+
1586
+#[derive(Debug, Clone, PartialEq, Eq)]
1587
+enum CanonicalExportKind {
1588
+    Regular(u64),
1589
+    ThreadLocal(u64),
1590
+    Absolute(u64),
1591
+    Reexport { ordinal: u32, imported_name: String },
1592
+    StubAndResolver { stub: u64, resolver: u64 },
1593
+}
1594
+
1595
+#[derive(Debug, Clone, PartialEq, Eq)]
1596
+struct CanonicalExportRecord {
1597
+    name: String,
1598
+    flags: u64,
1599
+    kind: CanonicalExportKind,
1600
+}
1601
+
1602
+fn canonical_symbol_record_map(
1603
+    bytes: &[u8],
1604
+) -> Result<BTreeMap<String, CanonicalSymbolRecord>, String> {
1605
+    Ok(canonical_symbol_records(bytes)?
1606
+        .into_iter()
1607
+        .map(|record| (record.name.clone(), record))
1608
+        .collect())
1609
+}
1610
+
1611
+fn canonical_symbol_records(bytes: &[u8]) -> Result<Vec<CanonicalSymbolRecord>, String> {
1612
+    let (symtab, _) = symtab_and_dysymtab(bytes)?;
1613
+    let symbols =
1614
+        parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).map_err(|e| e.to_string())?;
1615
+    let strings =
1616
+        StringTable::from_file(bytes, symtab.stroff, symtab.strsize).map_err(|e| e.to_string())?;
1617
+    let section_addrs = section_addrs(bytes)?;
1618
+    Ok(symbols
1619
+        .iter()
1620
+        .map(|symbol| {
1621
+            let value = if symbol.kind() == SymKind::Sect && symbol.sect_idx() != 0 {
1622
+                let section_addr = section_addrs[symbol.sect_idx() as usize - 1];
1623
+                if symbol.value() >= section_addr {
1624
+                    symbol.value() - section_addr
1625
+                } else {
1626
+                    symbol.value()
1627
+                }
1628
+            } else {
1629
+                symbol.value()
1630
+            };
1631
+            CanonicalSymbolRecord {
1632
+                name: strings.get(symbol.strx()).unwrap().to_string(),
1633
+                n_type: symbol.raw.n_type,
1634
+                n_sect: symbol.raw.n_sect,
1635
+                n_desc: symbol.raw.n_desc,
1636
+                value,
1637
+            }
1638
+        })
1639
+        .collect())
1640
+}
1641
+
1642
+fn canonical_export_records(bytes: &[u8]) -> Result<Vec<CanonicalExportRecord>, String> {
1643
+    let dylib = DylibFile::parse("/tmp/canonical.dylib", bytes).map_err(|e| e.to_string())?;
1644
+    let symbol_values: BTreeMap<String, u64> = canonical_symbol_records(bytes)?
1645
+        .into_iter()
1646
+        .map(|record| (record.name, record.value))
1647
+        .collect();
1648
+    let mut out = dylib
1649
+        .exports
1650
+        .entries()
1651
+        .map_err(|e| e.to_string())?
1652
+        .into_iter()
1653
+        .map(|entry| {
1654
+            let kind = match entry.kind {
1655
+                ExportKind::Regular { .. } => {
1656
+                    CanonicalExportKind::Regular(*symbol_values.get(&entry.name).unwrap())
1657
+                }
1658
+                ExportKind::ThreadLocal { .. } => {
1659
+                    CanonicalExportKind::ThreadLocal(*symbol_values.get(&entry.name).unwrap())
1660
+                }
1661
+                ExportKind::Absolute { .. } => {
1662
+                    CanonicalExportKind::Absolute(*symbol_values.get(&entry.name).unwrap())
1663
+                }
1664
+                ExportKind::Reexport {
1665
+                    ordinal,
1666
+                    imported_name,
1667
+                } => CanonicalExportKind::Reexport {
1668
+                    ordinal,
1669
+                    imported_name,
1670
+                },
1671
+                ExportKind::StubAndResolver { stub, resolver } => {
1672
+                    CanonicalExportKind::StubAndResolver { stub, resolver }
1673
+                }
1674
+            };
1675
+            CanonicalExportRecord {
1676
+                name: entry.name,
1677
+                flags: entry.flags,
1678
+                kind,
1679
+            }
1680
+        })
1681
+        .collect::<Vec<_>>();
1682
+    out.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
1683
+    Ok(out)
1684
+}
1685
+
1686
+fn symbol_partition_names(bytes: &[u8]) -> Result<(Vec<String>, Vec<String>, Vec<String>), String> {
1687
+    let (symtab, dysymtab) = symtab_and_dysymtab(bytes)?;
1688
+    let symbols =
1689
+        parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).map_err(|e| e.to_string())?;
1690
+    let strings =
1691
+        StringTable::from_file(bytes, symtab.stroff, symtab.strsize).map_err(|e| e.to_string())?;
1692
+    let names_for = |start: u32, count: u32| -> Vec<String> {
1693
+        symbols[start as usize..(start + count) as usize]
1694
+            .iter()
1695
+            .map(|symbol| strings.get(symbol.strx()).unwrap().to_string())
1696
+            .collect()
1697
+    };
1698
+    Ok((
1699
+        names_for(dysymtab.ilocalsym, dysymtab.nlocalsym),
1700
+        names_for(dysymtab.iextdefsym, dysymtab.nextdefsym),
1701
+        names_for(dysymtab.iundefsym, dysymtab.nundefsym),
1702
+    ))
1703
+}
1704
+
1705
+fn raw_string_table(bytes: &[u8]) -> Result<Vec<u8>, String> {
1706
+    let (symtab, _) = symtab_and_dysymtab(bytes)?;
1707
+    let start = symtab.stroff as usize;
1708
+    let end = start + symtab.strsize as usize;
1709
+    Ok(bytes[start..end].to_vec())
1710
+}
1711
+
1712
+pub fn string_table_within_five_percent(ours: usize, theirs: usize) -> bool {
1713
+    let delta = ours.abs_diff(theirs);
1714
+    delta * 20 <= theirs
1715
+}
1716
+
1717
+fn indirect_symbol_table(bytes: &[u8]) -> Result<Vec<u32>, String> {
1718
+    let (_, dysymtab) = symtab_and_dysymtab(bytes)?;
1719
+    if dysymtab.nindirectsyms == 0 {
1720
+        return Ok(Vec::new());
1721
+    }
1722
+    let start = dysymtab.indirectsymoff as usize;
1723
+    let end = start + dysymtab.nindirectsyms as usize * 4;
1724
+    Ok(bytes[start..end]
1725
+        .chunks_exact(4)
1726
+        .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
1727
+        .collect())
1728
+}
1729
+
1730
+fn indirect_symbol_identities(bytes: &[u8]) -> Result<Vec<String>, String> {
1731
+    let (symtab, _) = symtab_and_dysymtab(bytes)?;
1732
+    let symbols =
1733
+        parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).map_err(|e| e.to_string())?;
1734
+    let strings =
1735
+        StringTable::from_file(bytes, symtab.stroff, symtab.strsize).map_err(|e| e.to_string())?;
1736
+    Ok(indirect_symbol_table(bytes)?
1737
+        .into_iter()
1738
+        .map(|index| {
1739
+            if index & INDIRECT_SYMBOL_LOCAL != 0 {
1740
+                if index & INDIRECT_SYMBOL_ABS != 0 {
1741
+                    "<LOCAL|ABS>".to_string()
1742
+                } else {
1743
+                    "<LOCAL>".to_string()
1744
+                }
1745
+            } else if index & INDIRECT_SYMBOL_ABS != 0 {
1746
+                "<ABS>".to_string()
1747
+            } else {
1748
+                let symbol = &symbols[index as usize];
1749
+                strings.get(symbol.strx()).unwrap().to_string()
1750
+            }
1751
+        })
1752
+        .collect())
1753
+}
1754
+
1755
+fn raw_linkedit_data_cmd(bytes: &[u8], expected_cmd: u32) -> Result<(u32, u32), String> {
1756
+    let header = parse_header(bytes).map_err(|e| e.to_string())?;
1757
+    let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
1758
+    for cmd in commands {
1759
+        match cmd {
1760
+            LoadCommand::Raw { cmd, data, .. } if cmd == expected_cmd => {
1761
+                return Ok((u32_le(&data[0..4]), u32_le(&data[4..8])));
1762
+            }
1763
+            LoadCommand::LinkerOptimizationHint(linkedit)
1764
+                if expected_cmd == afs_ld::macho::constants::LC_LINKER_OPTIMIZATION_HINT =>
1765
+            {
1766
+                return Ok((linkedit.dataoff, linkedit.datasize));
1767
+            }
1768
+            _ => {}
1769
+        }
1770
+    }
1771
+    Err(format!("missing raw linkedit command 0x{expected_cmd:x}"))
1772
+}
1773
+
1774
+fn linkedit_payload(bytes: &[u8], cmd: u32) -> Result<Vec<u8>, String> {
1775
+    let (dataoff, datasize) = raw_linkedit_data_cmd(bytes, cmd)?;
1776
+    if datasize == 0 {
1777
+        return Ok(Vec::new());
1778
+    }
1779
+    Ok(bytes[dataoff as usize..(dataoff + datasize) as usize].to_vec())
1780
+}
1781
+
1782
+fn decode_function_starts(bytes: &[u8]) -> Result<Vec<u64>, String> {
1783
+    let payload = linkedit_payload(bytes, LC_FUNCTION_STARTS)?;
1784
+    let mut offsets = Vec::new();
1785
+    let mut cursor = 0usize;
1786
+    let mut current = 0u64;
1787
+    while cursor < payload.len() {
1788
+        let (delta, used) = read_uleb(&payload[cursor..]).map_err(|e| e.to_string())?;
1789
+        cursor += used;
1790
+        if delta == 0 {
1791
+            break;
1792
+        }
1793
+        current += delta;
1794
+        offsets.push(current);
1795
+    }
1796
+    Ok(offsets)
1797
+}
1798
+
1799
+fn normalize_function_start_offsets(starts: &[u64]) -> Vec<u64> {
1800
+    let Some(&base) = starts.first() else {
1801
+        return Vec::new();
1802
+    };
1803
+    starts.iter().map(|offset| offset - base).collect()
1804
+}
1805
+
1806
+#[derive(Debug, Clone, PartialEq, Eq)]
1807
+struct DataInCodeRecord {
1808
+    offset: u32,
1809
+    length: u16,
1810
+    kind: u16,
1811
+}
1812
+
1813
+fn decode_data_in_code(bytes: &[u8]) -> Result<Vec<DataInCodeRecord>, String> {
1814
+    let payload = linkedit_payload(bytes, LC_DATA_IN_CODE)?;
1815
+    Ok(payload
1816
+        .chunks_exact(8)
1817
+        .map(|chunk| DataInCodeRecord {
1818
+            offset: u32::from_le_bytes(chunk[0..4].try_into().unwrap()),
1819
+            length: u16::from_le_bytes(chunk[4..6].try_into().unwrap()),
1820
+            kind: u16::from_le_bytes(chunk[6..8].try_into().unwrap()),
1821
+        })
1822
+        .collect())
1823
+}
1824
+
1825
+fn canonical_data_in_code(bytes: &[u8]) -> Result<Vec<DataInCodeRecord>, String> {
1826
+    let text = output_section_header(bytes, "__TEXT", "__text")
1827
+        .ok_or_else(|| "missing __TEXT,__text section".to_string())?;
1828
+    Ok(decode_data_in_code(bytes)?
1829
+        .into_iter()
1830
+        .map(|record| DataInCodeRecord {
1831
+            offset: record.offset - text.offset,
1832
+            length: record.length,
1833
+            kind: record.kind,
1834
+        })
1835
+        .collect())
1836
+}
1837
+
1838
+fn rebased_unwind_bytes(bytes: &[u8]) -> Result<Vec<u8>, String> {
1839
+    let header_base = segment_vmaddr(bytes, "__TEXT").unwrap_or(0);
1840
+    let text_base = output_section(bytes, "__TEXT", "__text")
1841
+        .ok_or_else(|| "missing __TEXT,__text section".to_string())?
1842
+        .0
1843
+        - header_base;
1844
+    let got_range = output_section(bytes, "__DATA_CONST", "__got")
1845
+        .map(|(addr, data)| (addr - header_base, addr - header_base + data.len() as u64));
1846
+    let lsda_base =
1847
+        output_section(bytes, "__TEXT", "__gcc_except_tab").map(|(addr, _)| addr - header_base);
1848
+    let (_, unwind) = output_section(bytes, "__TEXT", "__unwind_info")
1849
+        .ok_or_else(|| "missing __TEXT,__unwind_info section".to_string())?;
1850
+    let mut out = unwind;
1851
+    if out.len() < 28 {
1852
+        return Ok(out);
1853
+    }
1854
+
1855
+    let personalities_offset = u32_le(&out[12..16]) as usize;
1856
+    let personalities_count = u32_le(&out[16..20]) as usize;
1857
+    let indices_offset = u32_le(&out[20..24]) as usize;
1858
+    let indices_count = u32_le(&out[24..28]) as usize;
1859
+
1860
+    for idx in 0..personalities_count {
1861
+        let off = personalities_offset + idx * 4;
1862
+        let value = u32_le(&out[off..off + 4]) as u64;
1863
+        let rebased = if let Some((got_start, got_end)) = got_range {
1864
+            if got_start <= value && value < got_end {
1865
+                value - got_start
1866
+            } else if value >= text_base {
1867
+                value - text_base
1868
+            } else {
1869
+                value
1870
+            }
1871
+        } else if value >= text_base {
1872
+            value - text_base
1873
+        } else {
1874
+            value
1875
+        };
1876
+        out[off..off + 4].copy_from_slice(&(rebased as u32).to_le_bytes());
1877
+    }
1878
+
1879
+    let mut lsda_offsets = Vec::with_capacity(indices_count);
1880
+    for idx in 0..indices_count {
1881
+        let entry_off = indices_offset + idx * 12;
1882
+        let function_offset = u32_le(&out[entry_off..entry_off + 4]) as u64;
1883
+        let rebased = function_offset.saturating_sub(text_base);
1884
+        out[entry_off..entry_off + 4].copy_from_slice(&(rebased as u32).to_le_bytes());
1885
+        lsda_offsets.push(u32_le(&out[entry_off + 8..entry_off + 12]) as usize);
1886
+    }
1887
+
1888
+    if let (Some(lsda_base), Some(&start), Some(&end)) =
1889
+        (lsda_base, lsda_offsets.first(), lsda_offsets.last())
1890
+    {
1891
+        let mut entry_off = start;
1892
+        while entry_off < end {
1893
+            let function_offset = u32_le(&out[entry_off..entry_off + 4]) as u64;
1894
+            let lsda_offset = u32_le(&out[entry_off + 4..entry_off + 8]) as u64;
1895
+            out[entry_off..entry_off + 4]
1896
+                .copy_from_slice(&(function_offset.saturating_sub(text_base) as u32).to_le_bytes());
1897
+            out[entry_off + 4..entry_off + 8]
1898
+                .copy_from_slice(&(lsda_offset.saturating_sub(lsda_base) as u32).to_le_bytes());
1899
+            entry_off += 8;
1900
+        }
1901
+    }
1902
+
1903
+    let _ = decode_unwind_info(&out).map_err(|e| format!("decode unwind info: {e}"))?;
1904
+    Ok(out)
1905
+}
1906
+
1907
+fn symtab_and_dysymtab(
1908
+    bytes: &[u8],
1909
+) -> Result<
1910
+    (
1911
+        afs_ld::macho::reader::SymtabCmd,
1912
+        afs_ld::macho::reader::DysymtabCmd,
1913
+    ),
1914
+    String,
1915
+> {
1916
+    let header = parse_header(bytes).map_err(|e| e.to_string())?;
1917
+    let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
1918
+    let mut symtab = None;
1919
+    let mut dysymtab = None;
1920
+    for cmd in commands {
1921
+        match cmd {
1922
+            LoadCommand::Symtab(cmd) => symtab = Some(cmd),
1923
+            LoadCommand::Dysymtab(cmd) => dysymtab = Some(cmd),
1924
+            _ => {}
1925
+        }
1926
+    }
1927
+    Ok((
1928
+        symtab.ok_or_else(|| "missing LC_SYMTAB".to_string())?,
1929
+        dysymtab.ok_or_else(|| "missing LC_DYSYMTAB".to_string())?,
1930
+    ))
1931
+}
1932
+
1933
+fn section_addrs(bytes: &[u8]) -> Result<Vec<u64>, String> {
1934
+    let header = parse_header(bytes).map_err(|e| e.to_string())?;
1935
+    let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
1936
+    let mut out = Vec::new();
1937
+    for cmd in commands {
1938
+        if let LoadCommand::Segment64(seg) = cmd {
1939
+            for section in seg.sections {
1940
+                out.push(section.addr);
1941
+            }
1942
+        }
1943
+    }
1944
+    Ok(out)
1945
+}
1946
+
1947
+#[derive(Clone, Copy)]
1948
+enum DyldInfoStreamKind {
1949
+    Rebase,
1950
+    Bind,
1951
+    WeakBind,
1952
+    LazyBind,
1953
+}
1954
+
1955
+fn dyld_info_command(bytes: &[u8]) -> Result<DyldInfoCmd, String> {
1956
+    let header = parse_header(bytes).map_err(|e| e.to_string())?;
1957
+    let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
1958
+    commands
1959
+        .into_iter()
1960
+        .find_map(|cmd| match cmd {
1961
+            LoadCommand::DyldInfoOnly(cmd) => Some(cmd),
1962
+            _ => None,
1963
+        })
1964
+        .ok_or_else(|| "missing LC_DYLD_INFO_ONLY".to_string())
1965
+}
1966
+
1967
+fn dyld_info_stream(bytes: &[u8], kind: DyldInfoStreamKind) -> Result<Vec<u8>, String> {
1968
+    let dyld_info = dyld_info_command(bytes)?;
1969
+    let (off, size) = match kind {
1970
+        DyldInfoStreamKind::Rebase => (dyld_info.rebase_off, dyld_info.rebase_size),
1971
+        DyldInfoStreamKind::Bind => (dyld_info.bind_off, dyld_info.bind_size),
1972
+        DyldInfoStreamKind::WeakBind => (dyld_info.weak_bind_off, dyld_info.weak_bind_size),
1973
+        DyldInfoStreamKind::LazyBind => (dyld_info.lazy_bind_off, dyld_info.lazy_bind_size),
1974
+    };
1975
+    if size == 0 {
1976
+        return Ok(Vec::new());
1977
+    }
1978
+    let start = off as usize;
1979
+    let end = start + size as usize;
1980
+    bytes
1981
+        .get(start..end)
1982
+        .map(|slice| slice.to_vec())
1983
+        .ok_or_else(|| "dyld-info stream out of bounds".to_string())
1984
+}
1985
+
1986
+fn symbol_values(bytes: &[u8]) -> Result<BTreeMap<String, u64>, String> {
1987
+    let header = parse_header(bytes).map_err(|e| e.to_string())?;
1988
+    let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
1989
+    let symtab = commands
1990
+        .iter()
1991
+        .find_map(|cmd| match cmd {
1992
+            LoadCommand::Symtab(cmd) => Some(*cmd),
1993
+            _ => None,
1994
+        })
1995
+        .ok_or_else(|| "missing LC_SYMTAB".to_string())?;
1996
+    let symbols =
1997
+        parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).map_err(|e| e.to_string())?;
1998
+    let strings =
1999
+        StringTable::from_file(bytes, symtab.stroff, symtab.strsize).map_err(|e| e.to_string())?;
2000
+    let mut out = BTreeMap::new();
2001
+    for symbol in symbols {
2002
+        let Ok(name) = strings.get(symbol.strx()) else {
2003
+            continue;
2004
+        };
2005
+        out.insert(name.to_string(), symbol.value());
2006
+    }
2007
+    Ok(out)
2008
+}
2009
+
2010
+fn decode_page_reference(
2011
+    bytes: &[u8],
2012
+    section_addr: u64,
2013
+    site_offset: u64,
2014
+    kind: PageRefKind,
2015
+) -> Result<u64, String> {
2016
+    let start = site_offset as usize;
2017
+    let adrp = read_insn(bytes, start)?;
2018
+    let second = read_insn(bytes, start + 4)?;
2019
+    let place = section_addr + site_offset;
2020
+    let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
2021
+    let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
2022
+    let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
2023
+    let adrp_base = ((place as i64) & !0xfff) + (adrp_pages << 12);
2024
+    let low = match kind {
2025
+        PageRefKind::Add => ((second >> 10) & 0xfff) as u64,
2026
+        PageRefKind::Load => {
2027
+            let shift = ((second >> 30) & 0b11) as u64;
2028
+            (((second >> 10) & 0xfff) as u64) << shift
2029
+        }
2030
+    };
2031
+    Ok((adrp_base as u64) + low)
2032
+}
2033
+
2034
+fn read_insn(bytes: &[u8], start: usize) -> Result<u32, String> {
2035
+    let end = start + 4;
2036
+    let slice = bytes
2037
+        .get(start..end)
2038
+        .ok_or_else(|| format!("instruction read OOB at 0x{start:x}"))?;
2039
+    Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
2040
+}
2041
+
2042
+fn sign_extend_21(value: i64) -> i64 {
2043
+    if value & (1 << 20) != 0 {
2044
+        value | !0x1f_ffff
2045
+    } else {
2046
+        value
2047
+    }
962048
 }
tests/diff_harness_tolerates_known_linkedit.rsadded
96 lines changed — click to load
@@ -0,0 +1,96 @@
1
+//! Tolerated-diff proof points for the parity harness.
2
+
3
+mod common;
4
+
5
+use common::harness::diff_macho;
6
+
7
+const MH_MAGIC_64: u32 = 0xFEEDFACF;
8
+const CPU_TYPE_ARM64: u32 = 0x0100_000C;
9
+const MH_EXECUTE: u32 = 2;
10
+const LC_ID_DYLIB: u32 = 0x0D;
11
+const LC_UUID: u32 = 0x1B;
12
+const LC_CODE_SIGNATURE: u32 = 0x1D;
13
+
14
+#[test]
15
+fn differing_uuid_bytes_are_tolerated() {
16
+    let ours = synth_uuid_image([0x11; 16]);
17
+    let theirs = synth_uuid_image([0x22; 16]);
18
+    let report = diff_macho(&ours, &theirs);
19
+    assert!(
20
+        report.is_clean(),
21
+        "UUID-only diff should be tolerated: {report:#?}"
22
+    );
23
+    assert_eq!(report.tolerated.len(), 1);
24
+}
25
+
26
+#[test]
27
+fn differing_code_signature_blob_bytes_are_tolerated() {
28
+    let ours = synth_code_signature_image([0xAA; 8]);
29
+    let theirs = synth_code_signature_image([0xBB; 8]);
30
+    let report = diff_macho(&ours, &theirs);
31
+    assert!(
32
+        report.is_clean(),
33
+        "code-signature-only diff should be tolerated: {report:#?}"
34
+    );
35
+    assert_eq!(report.tolerated.len(), 1);
36
+}
37
+
38
+#[test]
39
+fn differing_dylib_timestamps_are_tolerated() {
40
+    let ours = synth_dylib_image(2);
41
+    let theirs = synth_dylib_image(7);
42
+    let report = diff_macho(&ours, &theirs);
43
+    assert!(
44
+        report.is_clean(),
45
+        "dylib timestamp-only diff should be tolerated: {report:#?}"
46
+    );
47
+    assert_eq!(report.tolerated.len(), 1);
48
+}
49
+
50
+fn synth_uuid_image(uuid: [u8; 16]) -> Vec<u8> {
51
+    let mut out = Vec::new();
52
+    push_header(&mut out, 1, 24);
53
+    out.extend_from_slice(&LC_UUID.to_le_bytes());
54
+    out.extend_from_slice(&24u32.to_le_bytes());
55
+    out.extend_from_slice(&uuid);
56
+    out
57
+}
58
+
59
+fn synth_code_signature_image(blob: [u8; 8]) -> Vec<u8> {
60
+    let mut out = Vec::new();
61
+    let dataoff = 0x40u32;
62
+    let datasize = blob.len() as u32;
63
+    push_header(&mut out, 1, 16);
64
+    out.extend_from_slice(&LC_CODE_SIGNATURE.to_le_bytes());
65
+    out.extend_from_slice(&16u32.to_le_bytes());
66
+    out.extend_from_slice(&dataoff.to_le_bytes());
67
+    out.extend_from_slice(&datasize.to_le_bytes());
68
+    out.resize(dataoff as usize, 0);
69
+    out.extend_from_slice(&blob);
70
+    out
71
+}
72
+
73
+fn synth_dylib_image(timestamp: u32) -> Vec<u8> {
74
+    let mut out = Vec::new();
75
+    let cmdsize = 32u32;
76
+    push_header(&mut out, 1, cmdsize);
77
+    out.extend_from_slice(&LC_ID_DYLIB.to_le_bytes());
78
+    out.extend_from_slice(&cmdsize.to_le_bytes());
79
+    out.extend_from_slice(&24u32.to_le_bytes());
80
+    out.extend_from_slice(&timestamp.to_le_bytes());
81
+    out.extend_from_slice(&0u32.to_le_bytes());
82
+    out.extend_from_slice(&0u32.to_le_bytes());
83
+    out.extend_from_slice(b"x\0\0\0\0\0\0\0");
84
+    out
85
+}
86
+
87
+fn push_header(out: &mut Vec<u8>, ncmds: u32, sizeofcmds: u32) {
88
+    out.extend_from_slice(&MH_MAGIC_64.to_le_bytes());
89
+    out.extend_from_slice(&CPU_TYPE_ARM64.to_le_bytes());
90
+    out.extend_from_slice(&0u32.to_le_bytes());
91
+    out.extend_from_slice(&MH_EXECUTE.to_le_bytes());
92
+    out.extend_from_slice(&ncmds.to_le_bytes());
93
+    out.extend_from_slice(&sizeofcmds.to_le_bytes());
94
+    out.extend_from_slice(&0u32.to_le_bytes());
95
+    out.extend_from_slice(&0u32.to_le_bytes());
96
+}
tests/linker_run.rsmodified
8520 lines changed — click to load
@@ -18,7 +18,7 @@ use afs_ld::macho::constants::{
1818
     BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM, BIND_OPCODE_SET_TYPE_IMM,
1919
     BIND_SYMBOL_FLAGS_WEAK_IMPORT, DICE_KIND_JUMP_TABLE32, INDIRECT_SYMBOL_ABS,
2020
     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,
21
+    LC_FUNCTION_STARTS, LC_LINKER_OPTIMIZATION_HINT, LC_SEGMENT_64, LC_SYMTAB, N_PEXT,
2222
     REBASE_IMMEDIATE_MASK, REBASE_OPCODE_ADD_ADDR_IMM_SCALED, REBASE_OPCODE_ADD_ADDR_ULEB,
2323
     REBASE_OPCODE_DONE, REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB, REBASE_OPCODE_DO_REBASE_IMM_TIMES,
2424
     REBASE_OPCODE_DO_REBASE_ULEB_TIMES, REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB,
@@ -31,7 +31,7 @@ use afs_ld::macho::reader::{parse_commands, parse_header, u32_le, LoadCommand, S
3131
 use afs_ld::string_table::StringTable;
3232
 use afs_ld::symbol::{parse_nlist_table, SymKind};
3333
 use afs_ld::synth::unwind::decode_unwind_info;
34
-use afs_ld::{LinkError, LinkOptions, Linker, OutputKind};
34
+use afs_ld::{FrameworkSpec, LinkError, LinkOptions, Linker, OutputKind};
3535
 use common::harness::diff_macho;
3636
 
3737
 fn have_xcrun() -> bool {
@@ -220,6 +220,36 @@ fn output_section(bytes: &[u8], segname: &str, sectname: &str) -> Option<(u64, V
220220
     None
221221
 }
222222
 
223
+fn output_sections(bytes: &[u8], segname: &str, sectname: &str) -> Vec<(u64, Vec<u8>)> {
224
+    let Ok(header) = parse_header(bytes) else {
225
+        return Vec::new();
226
+    };
227
+    let Ok(commands) = parse_commands(&header, bytes) else {
228
+        return Vec::new();
229
+    };
230
+    let mut matches = Vec::new();
231
+    for cmd in commands {
232
+        if let LoadCommand::Segment64(seg) = cmd {
233
+            for section in seg.sections {
234
+                if section.segname_str() == segname && section.sectname_str() == sectname {
235
+                    let data = if section.offset == 0 {
236
+                        Vec::new()
237
+                    } else {
238
+                        let start = section.offset as usize;
239
+                        let end = start + section.size as usize;
240
+                        let Some(bytes) = bytes.get(start..end) else {
241
+                            continue;
242
+                        };
243
+                        bytes.to_vec()
244
+                    };
245
+                    matches.push((section.addr, data));
246
+                }
247
+            }
248
+        }
249
+    }
250
+    matches
251
+}
252
+
223253
 fn output_section_header(bytes: &[u8], segname: &str, sectname: &str) -> Option<Section64Header> {
224254
     let header = parse_header(bytes).ok()?;
225255
     let commands = parse_commands(&header, bytes).ok()?;
@@ -372,6 +402,13 @@ fn canonical_symbol_records(bytes: &[u8]) -> Vec<CanonicalSymbolRecord> {
372402
         .collect()
373403
 }
374404
 
405
+fn canonical_symbol_record_map(bytes: &[u8]) -> HashMap<String, CanonicalSymbolRecord> {
406
+    canonical_symbol_records(bytes)
407
+        .into_iter()
408
+        .map(|record| (record.name.clone(), record))
409
+        .collect()
410
+}
411
+
375412
 #[derive(Debug, Clone, PartialEq, Eq)]
376413
 enum CanonicalExportKind {
377414
     Regular(u64),
@@ -509,10 +546,16 @@ fn raw_linkedit_data_cmd(bytes: &[u8], expected_cmd: u32) -> (u32, u32) {
509546
     let header = parse_header(bytes).unwrap();
510547
     let commands = parse_commands(&header, bytes).unwrap();
511548
     for cmd in commands {
512
-        if let LoadCommand::Raw { cmd, data, .. } = cmd {
513
-            if cmd == expected_cmd {
549
+        match cmd {
550
+            LoadCommand::Raw { cmd, data, .. } if cmd == expected_cmd => {
514551
                 return (u32_le(&data[0..4]), u32_le(&data[4..8]));
515552
             }
553
+            LoadCommand::LinkerOptimizationHint(linkedit)
554
+                if expected_cmd == LC_LINKER_OPTIMIZATION_HINT =>
555
+            {
556
+                return (linkedit.dataoff, linkedit.datasize);
557
+            }
558
+            _ => {}
516559
         }
517560
     }
518561
     panic!("missing raw linkedit command 0x{expected_cmd:x}");
@@ -765,6 +808,18 @@ fn canonical_data_in_code(bytes: &[u8]) -> Vec<DataInCodeRecord> {
765808
         .collect()
766809
 }
767810
 
811
+fn has_loh_command(bytes: &[u8]) -> bool {
812
+    let header = parse_header(bytes).unwrap();
813
+    parse_commands(&header, bytes)
814
+        .unwrap()
815
+        .into_iter()
816
+        .any(|cmd| match cmd {
817
+            LoadCommand::LinkerOptimizationHint(_) => true,
818
+            LoadCommand::Raw { cmd, .. } => cmd == LC_LINKER_OPTIMIZATION_HINT,
819
+            _ => false,
820
+        })
821
+}
822
+
768823
 fn assert_strtab_within_five_percent(ours: &[u8], apple: &[u8]) {
769824
     let delta = ours.len().abs_diff(apple.len());
770825
     assert!(
@@ -1789,6 +1844,21 @@ fn read_insn(bytes: &[u8], start: usize) -> Result<u32, String> {
17891844
     Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
17901845
 }
17911846
 
1847
+fn is_adrp(insn: u32) -> bool {
1848
+    (insn & 0x9f00_0000) == 0x9000_0000
1849
+}
1850
+
1851
+fn is_add_imm_64(insn: u32) -> bool {
1852
+    (insn & 0xffc0_0000) == 0x9100_0000
1853
+}
1854
+
1855
+fn is_ldr_literal(insn: u32) -> bool {
1856
+    matches!(
1857
+        insn & 0xff00_0000,
1858
+        0x1800_0000 | 0x5800_0000 | 0x1c00_0000 | 0x5c00_0000 | 0x9c00_0000
1859
+    )
1860
+}
1861
+
17921862
 fn sign_extend_26(value: i64) -> i64 {
17931863
     if value & (1 << 25) != 0 {
17941864
         value | !0x03ff_ffff
@@ -1911,6 +1981,304 @@ fn linker_run_emits_non_empty_executable_from_real_object() {
19111981
     let _ = fs::remove_file(apple_out);
19121982
 }
19131983
 
1984
+#[test]
1985
+fn linker_run_loh_executable_surfaces_match_apple_ld() {
1986
+    if !have_xcrun() || !have_xcrun_tool("ld") {
1987
+        eprintln!("skipping: xcrun as/ld unavailable");
1988
+        return;
1989
+    }
1990
+    let Some(sdk) = sdk_path() else {
1991
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
1992
+        return;
1993
+    };
1994
+    let Some(sdk_ver) = sdk_version() else {
1995
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
1996
+        return;
1997
+    };
1998
+
1999
+    let cases = [
2000
+        (
2001
+            "loh-apple-adrp-add-exec",
2002
+            r#"
2003
+                .section __TEXT,__text,regular,pure_instructions
2004
+                .globl _main
2005
+                .globl _target
2006
+                _main:
2007
+                Lloh0:
2008
+                    adrp x0, _target@PAGE
2009
+                Lloh1:
2010
+                    add x0, x0, _target@PAGEOFF
2011
+                    mov w0, #0
2012
+                    ret
2013
+                _target:
2014
+                    ret
2015
+                .loh AdrpAdd Lloh0, Lloh1
2016
+                .subsections_via_symbols
2017
+            "#,
2018
+        ),
2019
+        (
2020
+            "loh-apple-adrp-ldr-exec",
2021
+            r#"
2022
+                .section __TEXT,__text,regular,pure_instructions
2023
+                .globl _main
2024
+                .globl _target
2025
+                _main:
2026
+                Lloh0:
2027
+                    adrp x0, _target@PAGE
2028
+                Lloh1:
2029
+                    ldr x1, [x0, _target@PAGEOFF]
2030
+                    mov w0, #0
2031
+                    ret
2032
+                    .p2align 3
2033
+                _target:
2034
+                    .quad 0x1122334455667788
2035
+                .loh AdrpLdr Lloh0, Lloh1
2036
+                .subsections_via_symbols
2037
+            "#,
2038
+        ),
2039
+        (
2040
+            "loh-apple-adrp-ldr-got-ldr-exec",
2041
+            r#"
2042
+                .section __TEXT,__text,regular,pure_instructions
2043
+                .globl _main
2044
+                .globl _value
2045
+                _main:
2046
+                Lloh0:
2047
+                    adrp x8, _value@GOTPAGE
2048
+                Lloh1:
2049
+                    ldr x8, [x8, _value@GOTPAGEOFF]
2050
+                Lloh2:
2051
+                    ldr w0, [x8]
2052
+                    ret
2053
+
2054
+                .section __DATA,__data
2055
+                .p2align 2
2056
+                _value:
2057
+                    .long 7
2058
+                .loh AdrpLdrGotLdr Lloh0, Lloh1, Lloh2
2059
+                .subsections_via_symbols
2060
+            "#,
2061
+        ),
2062
+    ];
2063
+
2064
+    for (name, src) in cases {
2065
+        let obj = scratch(&format!("{name}.o"));
2066
+        let our_out = scratch(&format!("{name}-ours.out"));
2067
+        let apple_out = scratch(&format!("{name}-apple.out"));
2068
+        if let Err(e) = assemble(src, &obj) {
2069
+            eprintln!("skipping: assemble failed: {e}");
2070
+            let _ = fs::remove_file(obj);
2071
+            let _ = fs::remove_file(our_out);
2072
+            let _ = fs::remove_file(apple_out);
2073
+            return;
2074
+        }
2075
+
2076
+        let opts = LinkOptions {
2077
+            inputs: vec![obj.clone()],
2078
+            output: Some(our_out.clone()),
2079
+            kind: OutputKind::Executable,
2080
+            ..LinkOptions::default()
2081
+        };
2082
+        Linker::run(&opts).unwrap();
2083
+        apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
2084
+
2085
+        let our_bytes = fs::read(&our_out).unwrap();
2086
+        let apple_bytes = fs::read(&apple_out).unwrap();
2087
+        let (our_text_addr, our_text) = output_section(&our_bytes, "__TEXT", "__text").unwrap();
2088
+        let (apple_text_addr, apple_text) =
2089
+            output_section(&apple_bytes, "__TEXT", "__text").unwrap();
2090
+        assert_eq!(
2091
+            our_text.len(),
2092
+            apple_text.len(),
2093
+            "{name} text length drifted from Apple ld"
2094
+        );
2095
+        assert!(
2096
+            !has_loh_command(&our_bytes),
2097
+            "{name} should omit LC_LINKER_OPTIMIZATION_HINT"
2098
+        );
2099
+        assert!(
2100
+            !has_loh_command(&apple_bytes),
2101
+            "{name} Apple peer unexpectedly emitted LC_LINKER_OPTIMIZATION_HINT"
2102
+        );
2103
+        match name {
2104
+            "loh-apple-adrp-add-exec" => {
2105
+                let our_target = symbol_values(&our_bytes)["_target"];
2106
+                let apple_target = symbol_values(&apple_bytes)["_target"];
2107
+                let our_first = read_insn(&our_text, 0).unwrap();
2108
+                let our_second = read_insn(&our_text, 4).unwrap();
2109
+                let apple_first = read_insn(&apple_text, 0).unwrap();
2110
+                let apple_second = read_insn(&apple_text, 4).unwrap();
2111
+                assert!(is_adrp(our_first), "{name} should keep ADRP");
2112
+                assert!(is_add_imm_64(our_second), "{name} should keep ADD");
2113
+                assert!(is_adrp(apple_first), "{name} Apple peer should keep ADRP");
2114
+                assert!(
2115
+                    is_add_imm_64(apple_second),
2116
+                    "{name} Apple peer should keep ADD"
2117
+                );
2118
+                assert_eq!(
2119
+                    decode_page_reference(&our_text, our_text_addr, 0, &PageRefKind::Add).unwrap(),
2120
+                    our_target
2121
+                );
2122
+                assert_eq!(
2123
+                    decode_page_reference(&apple_text, apple_text_addr, 0, &PageRefKind::Add)
2124
+                        .unwrap(),
2125
+                    apple_target
2126
+                );
2127
+            }
2128
+            "loh-apple-adrp-ldr-exec" => {
2129
+                let our_target = symbol_values(&our_bytes)["_target"];
2130
+                let apple_target = symbol_values(&apple_bytes)["_target"];
2131
+                let our_first = read_insn(&our_text, 0).unwrap();
2132
+                let our_second = read_insn(&our_text, 4).unwrap();
2133
+                let apple_first = read_insn(&apple_text, 0).unwrap();
2134
+                let apple_second = read_insn(&apple_text, 4).unwrap();
2135
+                assert!(is_adrp(our_first), "{name} should keep ADRP");
2136
+                assert!(
2137
+                    !is_ldr_literal(our_second),
2138
+                    "{name} should keep pageoff LDR"
2139
+                );
2140
+                assert!(is_adrp(apple_first), "{name} Apple peer should keep ADRP");
2141
+                assert!(
2142
+                    !is_ldr_literal(apple_second),
2143
+                    "{name} Apple peer should keep pageoff LDR"
2144
+                );
2145
+                assert_eq!(
2146
+                    decode_page_reference(&our_text, our_text_addr, 0, &PageRefKind::Load).unwrap(),
2147
+                    our_target
2148
+                );
2149
+                assert_eq!(
2150
+                    decode_page_reference(&apple_text, apple_text_addr, 0, &PageRefKind::Load)
2151
+                        .unwrap(),
2152
+                    apple_target
2153
+                );
2154
+            }
2155
+            "loh-apple-adrp-ldr-got-ldr-exec" => {
2156
+                let our_target = symbol_values(&our_bytes)["_value"];
2157
+                let apple_target = symbol_values(&apple_bytes)["_value"];
2158
+                let our_first = read_insn(&our_text, 0).unwrap();
2159
+                let our_second = read_insn(&our_text, 4).unwrap();
2160
+                let our_third = read_insn(&our_text, 8).unwrap();
2161
+                let apple_first = read_insn(&apple_text, 0).unwrap();
2162
+                let apple_second = read_insn(&apple_text, 4).unwrap();
2163
+                let apple_third = read_insn(&apple_text, 8).unwrap();
2164
+                assert!(is_adrp(our_first), "{name} should keep ADRP");
2165
+                assert!(
2166
+                    is_add_imm_64(our_second),
2167
+                    "{name} should keep GOT-resolved ADD"
2168
+                );
2169
+                assert!(is_adrp(apple_first), "{name} Apple peer should keep ADRP");
2170
+                assert!(
2171
+                    is_add_imm_64(apple_second),
2172
+                    "{name} Apple peer should keep GOT-resolved ADD"
2173
+                );
2174
+                assert_eq!(our_third, apple_third, "{name} final load drifted");
2175
+                assert_eq!(
2176
+                    decode_page_reference(&our_text, our_text_addr, 0, &PageRefKind::Add).unwrap(),
2177
+                    our_target
2178
+                );
2179
+                assert_eq!(
2180
+                    decode_page_reference(&apple_text, apple_text_addr, 0, &PageRefKind::Add)
2181
+                        .unwrap(),
2182
+                    apple_target
2183
+                );
2184
+            }
2185
+            _ => unreachable!("unexpected LOH parity case"),
2186
+        }
2187
+
2188
+        let _ = fs::remove_file(obj);
2189
+        let _ = fs::remove_file(our_out);
2190
+        let _ = fs::remove_file(apple_out);
2191
+    }
2192
+}
2193
+
2194
+#[test]
2195
+fn linker_run_loh_dylib_surfaces_match_apple_ld() {
2196
+    if !have_xcrun() || !have_xcrun_tool("ld") {
2197
+        eprintln!("skipping: xcrun as/ld unavailable");
2198
+        return;
2199
+    }
2200
+    let Some(sdk) = sdk_path() else {
2201
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
2202
+        return;
2203
+    };
2204
+    let Some(sdk_ver) = sdk_version() else {
2205
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
2206
+        return;
2207
+    };
2208
+
2209
+    let obj = scratch("loh-apple-dylib.o");
2210
+    let our_out = scratch("loh-apple-ours.dylib");
2211
+    let apple_out = scratch("loh-apple-apple.dylib");
2212
+    let install_name = "@rpath/liblohprobe.dylib";
2213
+    let src = r#"
2214
+        .section __TEXT,__text,regular,pure_instructions
2215
+        .globl _loh_probe
2216
+        .globl _target
2217
+        _loh_probe:
2218
+        Lloh0:
2219
+            adrp x0, _target@PAGE
2220
+        Lloh1:
2221
+            add x0, x0, _target@PAGEOFF
2222
+            ret
2223
+        _target:
2224
+            ret
2225
+        .loh AdrpAdd Lloh0, Lloh1
2226
+        .subsections_via_symbols
2227
+    "#;
2228
+    if let Err(e) = assemble(src, &obj) {
2229
+        eprintln!("skipping: assemble failed: {e}");
2230
+        return;
2231
+    }
2232
+
2233
+    let opts = LinkOptions {
2234
+        inputs: vec![obj.clone()],
2235
+        output: Some(our_out.clone()),
2236
+        kind: OutputKind::Dylib,
2237
+        install_name: Some(install_name.into()),
2238
+        ..LinkOptions::default()
2239
+    };
2240
+    Linker::run(&opts).unwrap();
2241
+    apple_link_dylib_classic(&obj, &apple_out, install_name, &sdk, &sdk_ver).unwrap();
2242
+
2243
+    let our_bytes = fs::read(&our_out).unwrap();
2244
+    let apple_bytes = fs::read(&apple_out).unwrap();
2245
+    let (our_text_addr, our_text) = output_section(&our_bytes, "__TEXT", "__text").unwrap();
2246
+    let (apple_text_addr, apple_text) = output_section(&apple_bytes, "__TEXT", "__text").unwrap();
2247
+    let our_target = symbol_values(&our_bytes)["_target"];
2248
+    let apple_target = symbol_values(&apple_bytes)["_target"];
2249
+    let our_first = read_insn(&our_text, 0).unwrap();
2250
+    let our_second = read_insn(&our_text, 4).unwrap();
2251
+    let apple_first = read_insn(&apple_text, 0).unwrap();
2252
+    let apple_second = read_insn(&apple_text, 4).unwrap();
2253
+    assert!(is_adrp(our_first), "dylib LOH should keep ADRP");
2254
+    assert!(is_add_imm_64(our_second), "dylib LOH should keep ADD");
2255
+    assert!(is_adrp(apple_first), "Apple dylib LOH should keep ADRP");
2256
+    assert!(
2257
+        is_add_imm_64(apple_second),
2258
+        "Apple dylib LOH should keep ADD"
2259
+    );
2260
+    assert_eq!(
2261
+        decode_page_reference(&our_text, our_text_addr, 0, &PageRefKind::Add).unwrap(),
2262
+        our_target
2263
+    );
2264
+    assert_eq!(
2265
+        decode_page_reference(&apple_text, apple_text_addr, 0, &PageRefKind::Add).unwrap(),
2266
+        apple_target
2267
+    );
2268
+    assert!(
2269
+        !has_loh_command(&our_bytes),
2270
+        "dylib output should omit LC_LINKER_OPTIMIZATION_HINT"
2271
+    );
2272
+    assert!(
2273
+        !has_loh_command(&apple_bytes),
2274
+        "Apple dylib peer unexpectedly emitted LC_LINKER_OPTIMIZATION_HINT"
2275
+    );
2276
+
2277
+    let _ = fs::remove_file(obj);
2278
+    let _ = fs::remove_file(our_out);
2279
+    let _ = fs::remove_file(apple_out);
2280
+}
2281
+
19142282
 #[test]
19152283
 fn linker_run_emits_minimal_dylib_from_real_object() {
19162284
     if !have_xcrun() {
@@ -1971,58 +2339,376 @@ fn linker_run_emits_minimal_dylib_from_real_object() {
19712339
 }
19722340
 
19732341
 #[test]
1974
-fn dylib_export_surfaces_match_apple_ld() {
1975
-    if !have_xcrun() || !have_xcrun_tool("ld") {
1976
-        eprintln!("skipping: xcrun as/ld unavailable");
2342
+fn linker_run_uses_dylib_identity_flags() {
2343
+    if !have_xcrun() {
2344
+        eprintln!("skipping: xcrun as unavailable");
19772345
         return;
19782346
     }
1979
-    let Some(sdk) = sdk_path() else {
1980
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
1981
-        return;
1982
-    };
1983
-    let Some(sdk_ver) = sdk_version() else {
1984
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
2347
+
2348
+    let obj = scratch("libmeta.o");
2349
+    let out = scratch("libmeta.dylib");
2350
+    let src = r#"
2351
+        .section __TEXT,__text,regular,pure_instructions
2352
+        .globl _exported
2353
+        _exported:
2354
+            ret
2355
+        .subsections_via_symbols
2356
+    "#;
2357
+    if let Err(e) = assemble(src, &obj) {
2358
+        eprintln!("skipping: assemble failed: {e}");
19852359
         return;
1986
-    };
2360
+    }
19872361
 
1988
-    let case = ExportParityCase {
1989
-        name: "export-parity",
1990
-        src: r#"
1991
-            .section __TEXT,__text,regular,pure_instructions
1992
-            .globl _exported
1993
-            _exported:
1994
-                ret
1995
-            .subsections_via_symbols
1996
-        "#,
2362
+    let opts = LinkOptions {
2363
+        inputs: vec![obj.clone()],
2364
+        output: Some(out.clone()),
2365
+        kind: OutputKind::Dylib,
2366
+        install_name: Some("@rpath/libmeta_custom.dylib".into()),
2367
+        current_version: Some((2 << 16) | (3 << 8) | 4),
2368
+        compatibility_version: Some((1 << 16) | (5 << 8)),
2369
+        rpaths: vec!["@loader_path/../lib".into()],
2370
+        ..LinkOptions::default()
19972371
     };
1998
-    assert_dylib_export_case_matches_apple_ld(&case, &sdk, &sdk_ver).unwrap();
2372
+    Linker::run(&opts).unwrap();
2373
+
2374
+    let bytes = fs::read(&out).unwrap();
2375
+    let header = parse_header(&bytes).unwrap();
2376
+    let commands = parse_commands(&header, &bytes).unwrap();
2377
+    let id_dylib = commands
2378
+        .iter()
2379
+        .find_map(|cmd| match cmd {
2380
+            LoadCommand::Dylib(cmd) if cmd.cmd == afs_ld::macho::constants::LC_ID_DYLIB => {
2381
+                Some(cmd.clone())
2382
+            }
2383
+            _ => None,
2384
+        })
2385
+        .expect("missing LC_ID_DYLIB");
2386
+    assert_eq!(id_dylib.name, "@rpath/libmeta_custom.dylib");
2387
+    assert_eq!(id_dylib.current_version, (2 << 16) | (3 << 8) | 4);
2388
+    assert_eq!(id_dylib.compatibility_version, (1 << 16) | (5 << 8));
2389
+    assert!(commands
2390
+        .iter()
2391
+        .any(|cmd| matches!(cmd, LoadCommand::Rpath(r) if r.path == "@loader_path/../lib")));
2392
+
2393
+    let _ = fs::remove_file(obj);
2394
+    let _ = fs::remove_file(out);
19992395
 }
20002396
 
20012397
 #[test]
2002
-fn dylib_export_surfaces_match_apple_ld_with_shared_prefixes() {
2003
-    if !have_xcrun() || !have_xcrun_tool("ld") {
2004
-        eprintln!("skipping: xcrun as/ld unavailable");
2398
+fn linker_run_honors_exported_symbol_filters_like_ld() {
2399
+    if !have_xcrun() {
2400
+        eprintln!("skipping: xcrun as unavailable");
20052401
         return;
20062402
     }
2007
-    let Some(sdk) = sdk_path() else {
2008
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
2009
-        return;
2010
-    };
2011
-    let Some(sdk_ver) = sdk_version() else {
2012
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
2403
+
2404
+    let obj = scratch("export-filter.o");
2405
+    let our_out = scratch("export-filter-ours.dylib");
2406
+    let apple_out = scratch("export-filter-apple.dylib");
2407
+    let list_path = scratch("export-filter-exports.txt");
2408
+    let src = r#"
2409
+        .section __TEXT,__text,regular,pure_instructions
2410
+        .globl _alpha
2411
+        .globl _beta
2412
+        .globl _gamma
2413
+        _alpha:
2414
+            ret
2415
+        _beta:
2416
+            ret
2417
+        _gamma:
2418
+            ret
2419
+        .subsections_via_symbols
2420
+    "#;
2421
+    if let Err(e) = assemble(src, &obj) {
2422
+        eprintln!("skipping: assemble failed: {e}");
20132423
         return;
2424
+    }
2425
+    fs::write(&list_path, "_bet?\n").unwrap();
2426
+
2427
+    let opts = LinkOptions {
2428
+        inputs: vec![obj.clone()],
2429
+        output: Some(our_out.clone()),
2430
+        kind: OutputKind::Dylib,
2431
+        exported_symbols: vec!["_alpha".into()],
2432
+        exported_symbols_lists: vec![list_path.clone()],
2433
+        ..LinkOptions::default()
20142434
     };
2435
+    Linker::run(&opts).unwrap();
20152436
 
2016
-    let case = ExportParityCase {
2017
-        name: "export-prefix-parity",
2018
-        src: r#"
2019
-            .section __TEXT,__text,regular,pure_instructions
2020
-            .globl _alpha
2021
-            _alpha:
2022
-                ret
2023
-            .globl _alphabet
2024
-            _alphabet:
2025
-                ret
2437
+    let apple = Command::new("xcrun")
2438
+        .args(["clang", "-arch", "arm64", "-dynamiclib"])
2439
+        .arg(&obj)
2440
+        .arg("-o")
2441
+        .arg(&apple_out)
2442
+        .arg("-Wl,-exported_symbol,_alpha")
2443
+        .arg(format!(
2444
+            "-Wl,-exported_symbols_list,{}",
2445
+            list_path.display()
2446
+        ))
2447
+        .output()
2448
+        .unwrap();
2449
+    assert!(
2450
+        apple.status.success(),
2451
+        "xcrun ld failed: {}",
2452
+        String::from_utf8_lossy(&apple.stderr)
2453
+    );
2454
+
2455
+    let our_bytes = fs::read(&our_out).unwrap();
2456
+    let apple_bytes = fs::read(&apple_out).unwrap();
2457
+    assert_eq!(
2458
+        canonical_export_records(&our_bytes),
2459
+        canonical_export_records(&apple_bytes)
2460
+    );
2461
+    assert_eq!(
2462
+        dyld_info_export_names(&our_bytes).unwrap(),
2463
+        vec!["_alpha".to_string(), "_beta".to_string()]
2464
+    );
2465
+    assert_eq!(
2466
+        canonical_symbol_record_map(&our_bytes),
2467
+        canonical_symbol_record_map(&apple_bytes)
2468
+    );
2469
+
2470
+    let our_symbols = canonical_symbol_record_map(&our_bytes);
2471
+    let gamma = our_symbols.get("_gamma").expect("missing _gamma");
2472
+    assert_ne!(
2473
+        gamma.n_type & N_PEXT,
2474
+        0,
2475
+        "expected _gamma to be private extern"
2476
+    );
2477
+
2478
+    let _ = fs::remove_file(obj);
2479
+    let _ = fs::remove_file(our_out);
2480
+    let _ = fs::remove_file(apple_out);
2481
+    let _ = fs::remove_file(list_path);
2482
+}
2483
+
2484
+#[test]
2485
+fn linker_run_honors_unexported_symbol_filters_like_ld() {
2486
+    if !have_xcrun() {
2487
+        eprintln!("skipping: xcrun as unavailable");
2488
+        return;
2489
+    }
2490
+
2491
+    let obj = scratch("unexport-filter.o");
2492
+    let our_out = scratch("unexport-filter-ours.dylib");
2493
+    let apple_out = scratch("unexport-filter-apple.dylib");
2494
+    let list_path = scratch("unexport-filter-hidden.txt");
2495
+    let src = r#"
2496
+        .section __TEXT,__text,regular,pure_instructions
2497
+        .globl _alpha
2498
+        .globl _beta
2499
+        .globl _gamma
2500
+        _alpha:
2501
+            ret
2502
+        _beta:
2503
+            ret
2504
+        _gamma:
2505
+            ret
2506
+        .subsections_via_symbols
2507
+    "#;
2508
+    if let Err(e) = assemble(src, &obj) {
2509
+        eprintln!("skipping: assemble failed: {e}");
2510
+        return;
2511
+    }
2512
+    fs::write(&list_path, "_bet?\n").unwrap();
2513
+
2514
+    let opts = LinkOptions {
2515
+        inputs: vec![obj.clone()],
2516
+        output: Some(our_out.clone()),
2517
+        kind: OutputKind::Dylib,
2518
+        unexported_symbols: vec!["_gamma".into()],
2519
+        unexported_symbols_lists: vec![list_path.clone()],
2520
+        ..LinkOptions::default()
2521
+    };
2522
+    Linker::run(&opts).unwrap();
2523
+
2524
+    let apple = Command::new("xcrun")
2525
+        .args(["clang", "-arch", "arm64", "-dynamiclib"])
2526
+        .arg(&obj)
2527
+        .arg("-o")
2528
+        .arg(&apple_out)
2529
+        .arg("-Wl,-unexported_symbol,_gamma")
2530
+        .arg(format!(
2531
+            "-Wl,-unexported_symbols_list,{}",
2532
+            list_path.display()
2533
+        ))
2534
+        .output()
2535
+        .unwrap();
2536
+    assert!(
2537
+        apple.status.success(),
2538
+        "xcrun ld failed: {}",
2539
+        String::from_utf8_lossy(&apple.stderr)
2540
+    );
2541
+
2542
+    let our_bytes = fs::read(&our_out).unwrap();
2543
+    let apple_bytes = fs::read(&apple_out).unwrap();
2544
+    assert_eq!(
2545
+        canonical_export_records(&our_bytes),
2546
+        canonical_export_records(&apple_bytes)
2547
+    );
2548
+    assert_eq!(
2549
+        dyld_info_export_names(&our_bytes).unwrap(),
2550
+        vec!["_alpha".to_string()]
2551
+    );
2552
+    assert_eq!(
2553
+        canonical_symbol_record_map(&our_bytes),
2554
+        canonical_symbol_record_map(&apple_bytes)
2555
+    );
2556
+
2557
+    let our_symbols = canonical_symbol_record_map(&our_bytes);
2558
+    for name in ["_beta", "_gamma"] {
2559
+        let record = our_symbols
2560
+            .get(name)
2561
+            .unwrap_or_else(|| panic!("missing {name}"));
2562
+        assert_ne!(
2563
+            record.n_type & N_PEXT,
2564
+            0,
2565
+            "expected {name} to be private extern"
2566
+        );
2567
+    }
2568
+
2569
+    let _ = fs::remove_file(obj);
2570
+    let _ = fs::remove_file(our_out);
2571
+    let _ = fs::remove_file(apple_out);
2572
+    let _ = fs::remove_file(list_path);
2573
+}
2574
+
2575
+#[test]
2576
+fn linker_run_loads_minimal_dylib_via_dlopen() {
2577
+    if !have_xcrun() || !have_tool("codesign") {
2578
+        eprintln!("skipping: xcrun clang/as or codesign unavailable");
2579
+        return;
2580
+    }
2581
+
2582
+    let obj = scratch("libfoo_add.o");
2583
+    let out = scratch("libfoo_add.dylib");
2584
+    let caller_src = scratch("libfoo_add-caller.c");
2585
+    let caller = scratch("libfoo_add-caller.out");
2586
+    let src = r#"
2587
+        .section __TEXT,__text,regular,pure_instructions
2588
+        .globl _foo_add
2589
+        _foo_add:
2590
+            add w0, w0, w1
2591
+            ret
2592
+        .subsections_via_symbols
2593
+    "#;
2594
+    if let Err(e) = assemble(src, &obj) {
2595
+        eprintln!("skipping: assemble failed: {e}");
2596
+        return;
2597
+    }
2598
+
2599
+    let opts = LinkOptions {
2600
+        inputs: vec![obj.clone()],
2601
+        output: Some(out.clone()),
2602
+        kind: OutputKind::Dylib,
2603
+        ..LinkOptions::default()
2604
+    };
2605
+    Linker::run(&opts).unwrap();
2606
+
2607
+    let verify = Command::new("codesign")
2608
+        .arg("-v")
2609
+        .arg(&out)
2610
+        .output()
2611
+        .unwrap();
2612
+    assert!(
2613
+        verify.status.success(),
2614
+        "codesign verify failed: {}",
2615
+        String::from_utf8_lossy(&verify.stderr)
2616
+    );
2617
+
2618
+    fs::write(
2619
+        &caller_src,
2620
+        r#"
2621
+            #include <dlfcn.h>
2622
+            typedef int (*foo_add_fn)(int, int);
2623
+            int main(int argc, char **argv) {
2624
+                if (argc != 2) return 10;
2625
+                void *handle = dlopen(argv[1], RTLD_NOW);
2626
+                if (!handle) return 11;
2627
+                foo_add_fn fn = (foo_add_fn)dlsym(handle, "foo_add");
2628
+                if (!fn) return 12;
2629
+                int value = fn(2, 3);
2630
+                dlclose(handle);
2631
+                return value == 5 ? 0 : 1;
2632
+            }
2633
+        "#,
2634
+    )
2635
+    .unwrap();
2636
+
2637
+    let output = Command::new("xcrun")
2638
+        .args(["--sdk", "macosx", "clang", "-arch", "arm64"])
2639
+        .arg(&caller_src)
2640
+        .arg("-o")
2641
+        .arg(&caller)
2642
+        .output()
2643
+        .unwrap();
2644
+    assert!(
2645
+        output.status.success(),
2646
+        "xcrun clang caller failed: {}",
2647
+        String::from_utf8_lossy(&output.stderr)
2648
+    );
2649
+
2650
+    let status = Command::new(&caller).arg(&out).status().unwrap();
2651
+    assert_eq!(status.code(), Some(0), "expected dlopen caller to exit 0");
2652
+
2653
+    let _ = fs::remove_file(obj);
2654
+    let _ = fs::remove_file(out);
2655
+    let _ = fs::remove_file(caller_src);
2656
+    let _ = fs::remove_file(caller);
2657
+}
2658
+
2659
+#[test]
2660
+fn dylib_export_surfaces_match_apple_ld() {
2661
+    if !have_xcrun() || !have_xcrun_tool("ld") {
2662
+        eprintln!("skipping: xcrun as/ld unavailable");
2663
+        return;
2664
+    }
2665
+    let Some(sdk) = sdk_path() else {
2666
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
2667
+        return;
2668
+    };
2669
+    let Some(sdk_ver) = sdk_version() else {
2670
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
2671
+        return;
2672
+    };
2673
+
2674
+    let case = ExportParityCase {
2675
+        name: "export-parity",
2676
+        src: r#"
2677
+            .section __TEXT,__text,regular,pure_instructions
2678
+            .globl _exported
2679
+            _exported:
2680
+                ret
2681
+            .subsections_via_symbols
2682
+        "#,
2683
+    };
2684
+    assert_dylib_export_case_matches_apple_ld(&case, &sdk, &sdk_ver).unwrap();
2685
+}
2686
+
2687
+#[test]
2688
+fn dylib_export_surfaces_match_apple_ld_with_shared_prefixes() {
2689
+    if !have_xcrun() || !have_xcrun_tool("ld") {
2690
+        eprintln!("skipping: xcrun as/ld unavailable");
2691
+        return;
2692
+    }
2693
+    let Some(sdk) = sdk_path() else {
2694
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
2695
+        return;
2696
+    };
2697
+    let Some(sdk_ver) = sdk_version() else {
2698
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
2699
+        return;
2700
+    };
2701
+
2702
+    let case = ExportParityCase {
2703
+        name: "export-prefix-parity",
2704
+        src: r#"
2705
+            .section __TEXT,__text,regular,pure_instructions
2706
+            .globl _alpha
2707
+            _alpha:
2708
+                ret
2709
+            .globl _alphabet
2710
+            _alphabet:
2711
+                ret
20262712
             .globl _alphanumeric
20272713
             _alphanumeric:
20282714
                 ret
@@ -2199,6 +2885,57 @@ fn linker_run_reports_unresolved_symbol() {
21992885
     let _ = fs::remove_file(obj);
22002886
 }
22012887
 
2888
+#[test]
2889
+fn linker_run_promotes_unresolved_symbol_to_dynamic_lookup() {
2890
+    if !have_xcrun() {
2891
+        eprintln!("skipping: xcrun as unavailable");
2892
+        return;
2893
+    }
2894
+
2895
+    let obj = scratch("missing-dynamic.o");
2896
+    let out = scratch("missing-dynamic.out");
2897
+    let src = r#"
2898
+        .section __TEXT,__text,regular,pure_instructions
2899
+        .globl _main
2900
+        _main:
2901
+            mov w0, #0
2902
+            ret
2903
+        .section __DATA,__data
2904
+        .p2align 3
2905
+        .globl _missing_slot
2906
+        _missing_slot:
2907
+            .quad _missing
2908
+        .subsections_via_symbols
2909
+    "#;
2910
+    if let Err(e) = assemble(src, &obj) {
2911
+        eprintln!("skipping: assemble failed: {e}");
2912
+        return;
2913
+    }
2914
+
2915
+    let opts = LinkOptions {
2916
+        inputs: vec![obj.clone()],
2917
+        output: Some(out.clone()),
2918
+        kind: OutputKind::Executable,
2919
+        undefined_treatment: afs_ld::resolve::UndefinedTreatment::DynamicLookup,
2920
+        ..LinkOptions::default()
2921
+    };
2922
+    Linker::run(&opts).unwrap();
2923
+
2924
+    let bytes = fs::read(&out).unwrap();
2925
+    let bind_records = decode_bind_records(&bytes, false).unwrap();
2926
+    assert!(
2927
+        bind_records
2928
+            .iter()
2929
+            .any(|record| record.symbol == "_missing" && record.ordinal == 0xFFFE),
2930
+        "expected flat-lookup bind for _missing, got {bind_records:?}"
2931
+    );
2932
+    let (_, _, undefs) = symbol_partition_names(&bytes);
2933
+    assert_eq!(undefs, vec!["_missing".to_string()]);
2934
+
2935
+    let _ = fs::remove_file(obj);
2936
+    let _ = fs::remove_file(out);
2937
+}
2938
+
22022939
 #[test]
22032940
 fn linker_run_reports_duplicate_from_fetched_archive_member() {
22042941
     if !have_xcrun() {
@@ -2348,13 +3085,13 @@ fn fetched_archive_member_undefined_reports_member_referrer() {
23483085
 }
23493086
 
23503087
 #[test]
2351
-fn linker_run_carries_tbd_inputs_into_load_commands() {
2352
-    if !have_xcrun() {
2353
-        eprintln!("skipping: xcrun unavailable");
3088
+fn linker_run_all_load_pulls_entry_from_archive() {
3089
+    if !have_xcrun() || !have_tool("codesign") {
3090
+        eprintln!("skipping: xcrun as or codesign unavailable");
23543091
         return;
23553092
     }
23563093
     let Some(sdk) = sdk_path() else {
2357
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
3094
+        eprintln!("skipping: no macOS SDK path");
23583095
         return;
23593096
     };
23603097
     let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
@@ -2363,102 +3100,160 @@ fn linker_run_carries_tbd_inputs_into_load_commands() {
23633100
         return;
23643101
     }
23653102
 
2366
-    let obj = scratch("tbd-main.o");
2367
-    let out = scratch("tbd-a.out");
3103
+    let member_obj = scratch("all-load-main.o");
3104
+    let archive = scratch("all-load-main.a");
3105
+    let out = scratch("all-load-main.out");
23683106
     let src = r#"
23693107
         .section __TEXT,__text,regular,pure_instructions
23703108
         .globl _main
23713109
         _main:
2372
-            mov x0, #0
3110
+            mov w0, #7
23733111
             ret
23743112
         .subsections_via_symbols
23753113
     "#;
2376
-    if let Err(e) = assemble(src, &obj) {
3114
+    if let Err(e) = assemble(src, &member_obj) {
23773115
         eprintln!("skipping: assemble failed: {e}");
23783116
         return;
23793117
     }
3118
+    let ar = Command::new("ar")
3119
+        .arg("rcs")
3120
+        .arg(&archive)
3121
+        .arg(&member_obj)
3122
+        .output()
3123
+        .unwrap();
3124
+    if !ar.status.success() {
3125
+        eprintln!(
3126
+            "skipping: ar failed: {}",
3127
+            String::from_utf8_lossy(&ar.stderr)
3128
+        );
3129
+        return;
3130
+    }
23803131
 
23813132
     let opts = LinkOptions {
2382
-        inputs: vec![obj.clone(), tbd.clone()],
3133
+        inputs: vec![archive.clone(), tbd],
23833134
         output: Some(out.clone()),
23843135
         kind: OutputKind::Executable,
3136
+        all_load: true,
23853137
         ..LinkOptions::default()
23863138
     };
23873139
     Linker::run(&opts).unwrap();
23883140
 
2389
-    let bytes = fs::read(&out).unwrap();
2390
-    let header = parse_header(&bytes).unwrap();
2391
-    let commands = parse_commands(&header, &bytes).unwrap();
2392
-    assert!(
2393
-        commands.iter().any(|cmd| matches!(
2394
-            cmd,
2395
-            LoadCommand::Dylib(d) if d.cmd == afs_ld::macho::constants::LC_LOAD_DYLIB
2396
-        )),
2397
-        "expected at least one LC_LOAD_DYLIB in output"
3141
+    let verify = Command::new("codesign")
3142
+        .arg("-v")
3143
+        .arg(&out)
3144
+        .output()
3145
+        .unwrap();
3146
+    assert!(
3147
+        verify.status.success(),
3148
+        "codesign verify failed: {}",
3149
+        String::from_utf8_lossy(&verify.stderr)
3150
+    );
3151
+    let status = Command::new(&out).status().unwrap();
3152
+    assert_eq!(
3153
+        status.code(),
3154
+        Some(7),
3155
+        "expected all-load executable to exit 7"
23983156
     );
23993157
 
2400
-    let _ = fs::remove_file(obj);
3158
+    let _ = fs::remove_file(member_obj);
3159
+    let _ = fs::remove_file(archive);
24013160
     let _ = fs::remove_file(out);
24023161
 }
24033162
 
24043163
 #[test]
2405
-fn linker_run_handles_non_standard_segment_without_panicking() {
2406
-    if !have_xcrun() {
2407
-        eprintln!("skipping: xcrun as unavailable");
3164
+fn linker_run_force_load_pulls_entry_from_archive() {
3165
+    if !have_xcrun() || !have_tool("codesign") {
3166
+        eprintln!("skipping: xcrun as or codesign unavailable");
3167
+        return;
3168
+    }
3169
+    let Some(sdk) = sdk_path() else {
3170
+        eprintln!("skipping: no macOS SDK path");
3171
+        return;
3172
+    };
3173
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
3174
+    if !tbd.exists() {
3175
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
24083176
         return;
24093177
     }
24103178
 
2411
-    let obj = scratch("custom-segment.o");
2412
-    let out = scratch("custom-segment.out");
3179
+    let member_obj = scratch("force-load-main.o");
3180
+    let archive = scratch("force-load-main.a");
3181
+    let out = scratch("force-load-main.out");
24133182
     let src = r#"
2414
-        .section __FOO,__bar
2415
-        .globl _custom
2416
-        _custom:
2417
-            .quad 1
3183
+        .section __TEXT,__text,regular,pure_instructions
3184
+        .globl _main
3185
+        _main:
3186
+            mov w0, #9
3187
+            ret
24183188
         .subsections_via_symbols
24193189
     "#;
2420
-    if let Err(e) = assemble(src, &obj) {
3190
+    if let Err(e) = assemble(src, &member_obj) {
24213191
         eprintln!("skipping: assemble failed: {e}");
24223192
         return;
24233193
     }
3194
+    let ar = Command::new("ar")
3195
+        .arg("rcs")
3196
+        .arg(&archive)
3197
+        .arg(&member_obj)
3198
+        .output()
3199
+        .unwrap();
3200
+    if !ar.status.success() {
3201
+        eprintln!(
3202
+            "skipping: ar failed: {}",
3203
+            String::from_utf8_lossy(&ar.stderr)
3204
+        );
3205
+        return;
3206
+    }
24243207
 
24253208
     let opts = LinkOptions {
2426
-        inputs: vec![obj.clone()],
3209
+        inputs: vec![archive.clone(), tbd],
24273210
         output: Some(out.clone()),
24283211
         kind: OutputKind::Executable,
3212
+        force_load_archives: vec![archive.clone()],
24293213
         ..LinkOptions::default()
24303214
     };
24313215
     Linker::run(&opts).unwrap();
24323216
 
2433
-    let bytes = fs::read(&out).unwrap();
2434
-    let header = parse_header(&bytes).unwrap();
2435
-    let commands = parse_commands(&header, &bytes).unwrap();
2436
-    assert!(commands.iter().any(|cmd| match cmd {
2437
-        LoadCommand::Segment64(seg) => seg.segname_str() == "__FOO",
2438
-        _ => false,
2439
-    }));
3217
+    let verify = Command::new("codesign")
3218
+        .arg("-v")
3219
+        .arg(&out)
3220
+        .output()
3221
+        .unwrap();
3222
+    assert!(
3223
+        verify.status.success(),
3224
+        "codesign verify failed: {}",
3225
+        String::from_utf8_lossy(&verify.stderr)
3226
+    );
3227
+    let status = Command::new(&out).status().unwrap();
3228
+    assert_eq!(
3229
+        status.code(),
3230
+        Some(9),
3231
+        "expected force-load executable to exit 9"
3232
+    );
24403233
 
2441
-    let _ = fs::remove_file(obj);
3234
+    let _ = fs::remove_file(member_obj);
3235
+    let _ = fs::remove_file(archive);
24423236
     let _ = fs::remove_file(out);
24433237
 }
24443238
 
24453239
 #[test]
2446
-fn linker_run_uses_requested_entry_symbol() {
2447
-    if !have_xcrun() {
2448
-        eprintln!("skipping: xcrun as unavailable");
3240
+fn linker_run_resolves_lsystem_via_syslibroot() {
3241
+    if !have_xcrun() || !have_tool("codesign") {
3242
+        eprintln!("skipping: xcrun as or codesign unavailable");
24493243
         return;
24503244
     }
3245
+    let Some(sdk) = sdk_path() else {
3246
+        eprintln!("skipping: no macOS SDK path");
3247
+        return;
3248
+    };
24513249
 
2452
-    let obj = scratch("entry.o");
2453
-    let out = scratch("entry.out");
3250
+    let obj = scratch("lsystem-main.o");
3251
+    let out = scratch("lsystem-main.out");
24543252
     let src = r#"
24553253
         .section __TEXT,__text,regular,pure_instructions
24563254
         .globl _main
24573255
         _main:
2458
-            ret
2459
-        .globl _alt
2460
-        _alt:
2461
-            mov x0, #1
3256
+            mov w0, #0
24623257
             ret
24633258
         .subsections_via_symbols
24643259
     "#;
@@ -2469,41 +3264,37 @@ fn linker_run_uses_requested_entry_symbol() {
24693264
 
24703265
     let opts = LinkOptions {
24713266
         inputs: vec![obj.clone()],
3267
+        library_names: vec!["System".into()],
3268
+        syslibroot: Some(PathBuf::from(&sdk)),
24723269
         output: Some(out.clone()),
2473
-        entry: Some("_alt".into()),
24743270
         kind: OutputKind::Executable,
24753271
         ..LinkOptions::default()
24763272
     };
24773273
     Linker::run(&opts).unwrap();
24783274
 
24793275
     let bytes = fs::read(&out).unwrap();
2480
-    let header = parse_header(&bytes).unwrap();
2481
-    let commands = parse_commands(&header, &bytes).unwrap();
2482
-    let mut text_offset = None;
2483
-    let mut main_entryoff = None;
2484
-    for cmd in commands {
2485
-        match cmd {
2486
-            LoadCommand::Segment64(seg) => {
2487
-                for section in seg.sections {
2488
-                    if section.sectname_str() == "__text" {
2489
-                        text_offset = Some(section.offset as u64);
2490
-                    }
2491
-                }
2492
-            }
2493
-            LoadCommand::Raw { cmd, data, .. } if cmd == afs_ld::macho::constants::LC_MAIN => {
2494
-                let mut buf = [0u8; 8];
2495
-                buf.copy_from_slice(&data[0..8]);
2496
-                main_entryoff = Some(u64::from_le_bytes(buf));
2497
-            }
2498
-            _ => {}
2499
-        }
2500
-    }
2501
-
2502
-    let text_offset = text_offset.expect("text section offset");
2503
-    let main_entryoff = main_entryoff.expect("LC_MAIN entryoff");
3276
+    let dylibs = load_dylib_names(&bytes).unwrap();
25043277
     assert!(
2505
-        main_entryoff > text_offset,
2506
-        "expected custom entry to land after start of __text: text={text_offset}, entry={main_entryoff}"
3278
+        dylibs
3279
+            .iter()
3280
+            .any(|name| name == "/usr/lib/libSystem.B.dylib"),
3281
+        "expected libSystem load command, got {dylibs:?}"
3282
+    );
3283
+    let verify = Command::new("codesign")
3284
+        .arg("-v")
3285
+        .arg(&out)
3286
+        .output()
3287
+        .unwrap();
3288
+    assert!(
3289
+        verify.status.success(),
3290
+        "codesign verify failed: {}",
3291
+        String::from_utf8_lossy(&verify.stderr)
3292
+    );
3293
+    let status = Command::new(&out).status().unwrap();
3294
+    assert_eq!(
3295
+        status.code(),
3296
+        Some(0),
3297
+        "expected executable linked via -lSystem to exit 0"
25073298
     );
25083299
 
25093300
     let _ = fs::remove_file(obj);
@@ -2511,20 +3302,27 @@ fn linker_run_uses_requested_entry_symbol() {
25113302
 }
25123303
 
25133304
 #[test]
2514
-fn linker_run_defaults_entry_to_main_symbol() {
3305
+fn linker_run_resolves_framework_via_syslibroot() {
25153306
     if !have_xcrun() {
2516
-        eprintln!("skipping: xcrun as unavailable");
3307
+        eprintln!("skipping: xcrun unavailable");
3308
+        return;
3309
+    }
3310
+    let Some(sdk) = sdk_path() else {
3311
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
3312
+        return;
3313
+    };
3314
+    let metal = PathBuf::from(format!(
3315
+        "{sdk}/System/Library/Frameworks/Metal.framework/Metal.tbd"
3316
+    ));
3317
+    if !metal.exists() {
3318
+        eprintln!("skipping: no Metal.tbd at {}", metal.display());
25173319
         return;
25183320
     }
25193321
 
2520
-    let obj = scratch("default-entry.o");
2521
-    let out = scratch("default-entry.out");
3322
+    let obj = scratch("framework-main.o");
3323
+    let out = scratch("framework-main.out");
25223324
     let src = r#"
25233325
         .section __TEXT,__text,regular,pure_instructions
2524
-        .globl _helper
2525
-        _helper:
2526
-            mov w0, #7
2527
-            ret
25283326
         .globl _main
25293327
         _main:
25303328
             mov w0, #0
@@ -2538,53 +3336,57 @@ fn linker_run_defaults_entry_to_main_symbol() {
25383336
 
25393337
     let opts = LinkOptions {
25403338
         inputs: vec![obj.clone()],
3339
+        frameworks: vec![FrameworkSpec {
3340
+            name: "Metal".into(),
3341
+            weak: false,
3342
+        }],
3343
+        syslibroot: Some(PathBuf::from(&sdk)),
25413344
         output: Some(out.clone()),
25423345
         kind: OutputKind::Executable,
25433346
         ..LinkOptions::default()
25443347
     };
25453348
     Linker::run(&opts).unwrap();
25463349
 
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
-    );
3350
+    let bytes = fs::read(&out).unwrap();
3351
+    let header = parse_header(&bytes).unwrap();
3352
+    let commands = parse_commands(&header, &bytes).unwrap();
3353
+    assert!(commands.iter().any(|cmd| matches!(
3354
+        cmd,
3355
+        LoadCommand::Dylib(d)
3356
+            if d.cmd == afs_ld::macho::constants::LC_LOAD_DYLIB
3357
+                && d.name.contains("Metal.framework")
3358
+    )));
25533359
 
25543360
     let _ = fs::remove_file(obj);
25553361
     let _ = fs::remove_file(out);
25563362
 }
25573363
 
25583364
 #[test]
2559
-fn linker_run_applies_core_arm64_relocations() {
3365
+fn linker_run_resolves_weak_framework_via_syslibroot() {
25603366
     if !have_xcrun() {
2561
-        eprintln!("skipping: xcrun as unavailable");
3367
+        eprintln!("skipping: xcrun unavailable");
3368
+        return;
3369
+    }
3370
+    let Some(sdk) = sdk_path() else {
3371
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
3372
+        return;
3373
+    };
3374
+    let metal = PathBuf::from(format!(
3375
+        "{sdk}/System/Library/Frameworks/Metal.framework/Metal.tbd"
3376
+    ));
3377
+    if !metal.exists() {
3378
+        eprintln!("skipping: no Metal.tbd at {}", metal.display());
25623379
         return;
25633380
     }
25643381
 
2565
-    let obj = scratch("relocs.o");
2566
-    let out = scratch("relocs.out");
3382
+    let obj = scratch("weak-framework-main.o");
3383
+    let out = scratch("weak-framework-main.out");
25673384
     let src = r#"
25683385
         .section __TEXT,__text,regular,pure_instructions
25693386
         .globl _main
2570
-        .globl _helper
25713387
         _main:
2572
-            adrp x0, _target@PAGE
2573
-            add x0, x0, _target@PAGEOFF
2574
-            bl _helper
2575
-            ret
2576
-        _helper:
3388
+            mov w0, #0
25773389
             ret
2578
-
2579
-        .section __DATA,__data
2580
-        .p2align 3
2581
-        _target:
2582
-            .quad _helper
2583
-
2584
-        .section __TEXT,__const
2585
-        .p2align 3
2586
-        _delta:
2587
-            .quad _helper - _main
25883390
         .subsections_via_symbols
25893391
     "#;
25903392
     if let Err(e) = assemble(src, &obj) {
@@ -2594,6 +3396,11 @@ fn linker_run_applies_core_arm64_relocations() {
25943396
 
25953397
     let opts = LinkOptions {
25963398
         inputs: vec![obj.clone()],
3399
+        frameworks: vec![FrameworkSpec {
3400
+            name: "Metal".into(),
3401
+            weak: true,
3402
+        }],
3403
+        syslibroot: Some(PathBuf::from(&sdk)),
25973404
         output: Some(out.clone()),
25983405
         kind: OutputKind::Executable,
25993406
         ..LinkOptions::default()
@@ -2601,74 +3408,34 @@ fn linker_run_applies_core_arm64_relocations() {
26013408
     Linker::run(&opts).unwrap();
26023409
 
26033410
     let bytes = fs::read(&out).unwrap();
2604
-    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").expect("text section");
2605
-    let (data_addr, data) = output_section(&bytes, "__DATA", "__data").expect("data section");
2606
-    let (_, cdata) = output_section(&bytes, "__TEXT", "__const").expect("const section");
2607
-
2608
-    let adrp = u32::from_le_bytes(text[0..4].try_into().unwrap());
2609
-    let add = u32::from_le_bytes(text[4..8].try_into().unwrap());
2610
-    let branch = u32::from_le_bytes(text[8..12].try_into().unwrap());
2611
-    let data_ptr = u64::from_le_bytes(data[0..8].try_into().unwrap());
2612
-    let delta = u64::from_le_bytes(cdata[0..8].try_into().unwrap());
2613
-
2614
-    let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
2615
-    let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
2616
-    let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
2617
-    let adrp_base = ((text_addr as i64) & !0xfff) + (adrp_pages << 12);
2618
-    let add_imm = ((add >> 10) & 0xfff) as u64;
2619
-    let reconstructed_target = (adrp_base as u64) + add_imm;
2620
-
2621
-    assert_eq!(
2622
-        reconstructed_target, data_addr,
2623
-        "ADRP+ADD should resolve _target"
2624
-    );
2625
-    assert_eq!(
2626
-        branch & 0x03ff_ffff,
2627
-        0x2,
2628
-        "BL should branch forward 8 bytes"
2629
-    );
2630
-    assert_eq!(
2631
-        data_ptr,
2632
-        text_addr + 16,
2633
-        ".quad _helper should point at helper"
2634
-    );
2635
-    assert_eq!(delta, 16, "_helper - _main should fold through SUBTRACTOR");
3411
+    let header = parse_header(&bytes).unwrap();
3412
+    let commands = parse_commands(&header, &bytes).unwrap();
3413
+    assert!(commands.iter().any(|cmd| matches!(
3414
+        cmd,
3415
+        LoadCommand::Dylib(d)
3416
+            if d.cmd == afs_ld::macho::constants::LC_LOAD_WEAK_DYLIB
3417
+                && d.name.contains("Metal.framework")
3418
+    )));
26363419
 
26373420
     let _ = fs::remove_file(obj);
26383421
     let _ = fs::remove_file(out);
26393422
 }
26403423
 
2641
-fn sign_extend_21(value: i64) -> i64 {
2642
-    if value & (1 << 20) != 0 {
2643
-        value | !0x1f_ffff
2644
-    } else {
2645
-        value
2646
-    }
2647
-}
2648
-
26493424
 #[test]
2650
-fn linker_run_applies_scaled_pageoff12_for_ldr_x() {
3425
+fn linker_run_uses_platform_version_for_build_command() {
26513426
     if !have_xcrun() {
26523427
         eprintln!("skipping: xcrun as unavailable");
26533428
         return;
26543429
     }
26553430
 
2656
-    let obj = scratch("scaled-ldr.o");
2657
-    let out = scratch("scaled-ldr.out");
3431
+    let obj = scratch("platform-version.o");
3432
+    let out = scratch("platform-version.out");
26583433
     let src = r#"
26593434
         .section __TEXT,__text,regular,pure_instructions
26603435
         .globl _main
26613436
         _main:
2662
-            adrp x0, _target@PAGE
2663
-            ldr x1, [x0, _target@PAGEOFF]
3437
+            mov w0, #0
26643438
             ret
2665
-
2666
-        .section __DATA,__data
2667
-        .space 0x3f8
2668
-        .p2align 3
2669
-        .globl _target
2670
-        _target:
2671
-            .quad 0x1122334455667788
26723439
         .subsections_via_symbols
26733440
     "#;
26743441
     if let Err(e) = assemble(src, &obj) {
@@ -2680,397 +3447,2674 @@ fn linker_run_applies_scaled_pageoff12_for_ldr_x() {
26803447
         inputs: vec![obj.clone()],
26813448
         output: Some(out.clone()),
26823449
         kind: OutputKind::Executable,
3450
+        platform_version: Some(afs_ld::PlatformVersion {
3451
+            minos: (13 << 16) | (2 << 8) | 1,
3452
+            sdk: (14 << 16) | (5 << 8),
3453
+        }),
26833454
         ..LinkOptions::default()
26843455
     };
26853456
     Linker::run(&opts).unwrap();
26863457
 
26873458
     let bytes = fs::read(&out).unwrap();
2688
-    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").expect("text section");
2689
-    let (data_addr, data) = output_section(&bytes, "__DATA", "__data").expect("data section");
3459
+    let header = parse_header(&bytes).unwrap();
3460
+    let commands = parse_commands(&header, &bytes).unwrap();
3461
+    let build = commands
3462
+        .into_iter()
3463
+        .find_map(|cmd| match cmd {
3464
+            LoadCommand::BuildVersion(cmd) => Some(cmd),
3465
+            _ => None,
3466
+        })
3467
+        .expect("missing LC_BUILD_VERSION");
3468
+    assert_eq!(build.minos, (13 << 16) | (2 << 8) | 1);
3469
+    assert_eq!(build.sdk, (14 << 16) | (5 << 8));
26903470
 
2691
-    let adrp = u32::from_le_bytes(text[0..4].try_into().unwrap());
2692
-    let ldr = u32::from_le_bytes(text[4..8].try_into().unwrap());
2693
-    let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
2694
-    let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
2695
-    let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
2696
-    let adrp_base = ((text_addr as i64) & !0xfff) + (adrp_pages << 12);
2697
-    let ldr_shift = ((ldr >> 30) & 0b11) as u64;
2698
-    let ldr_imm = ((ldr >> 10) & 0xfff) as u64;
2699
-    let reconstructed_target = (adrp_base as u64) + (ldr_imm << ldr_shift);
3471
+    let _ = fs::remove_file(obj);
3472
+    let _ = fs::remove_file(out);
3473
+}
27003474
 
2701
-    assert_eq!(ldr_shift, 3, "expected 64-bit LDR scale");
2702
-    assert_eq!(ldr_imm, 0x7f, "scaled imm12 should store 0x3f8 >> 3");
2703
-    assert_eq!(reconstructed_target, data_addr + 0x3f8);
2704
-    assert_eq!(
2705
-        u64::from_le_bytes(data[0x3f8..0x400].try_into().unwrap()),
2706
-        0x1122334455667788
2707
-    );
3475
+#[test]
3476
+fn linker_run_emits_rpath_command() {
3477
+    if !have_xcrun() {
3478
+        eprintln!("skipping: xcrun as unavailable");
3479
+        return;
3480
+    }
3481
+
3482
+    let obj = scratch("rpath-main.o");
3483
+    let out = scratch("rpath-main.out");
3484
+    let src = r#"
3485
+        .section __TEXT,__text,regular,pure_instructions
3486
+        .globl _main
3487
+        _main:
3488
+            mov w0, #0
3489
+            ret
3490
+        .subsections_via_symbols
3491
+    "#;
3492
+    if let Err(e) = assemble(src, &obj) {
3493
+        eprintln!("skipping: assemble failed: {e}");
3494
+        return;
3495
+    }
3496
+
3497
+    let opts = LinkOptions {
3498
+        inputs: vec![obj.clone()],
3499
+        output: Some(out.clone()),
3500
+        kind: OutputKind::Executable,
3501
+        rpaths: vec!["@loader_path/../Frameworks".into()],
3502
+        ..LinkOptions::default()
3503
+    };
3504
+    Linker::run(&opts).unwrap();
3505
+
3506
+    let bytes = fs::read(&out).unwrap();
3507
+    let header = parse_header(&bytes).unwrap();
3508
+    let commands = parse_commands(&header, &bytes).unwrap();
3509
+    let rpaths: Vec<String> = commands
3510
+        .into_iter()
3511
+        .filter_map(|cmd| match cmd {
3512
+            LoadCommand::Rpath(cmd) => Some(cmd.path),
3513
+            _ => None,
3514
+        })
3515
+        .collect();
3516
+    assert_eq!(rpaths, vec!["@loader_path/../Frameworks".to_string()]);
27083517
 
27093518
     let _ = fs::remove_file(obj);
27103519
     let _ = fs::remove_file(out);
27113520
 }
27123521
 
27133522
 #[test]
2714
-fn relocated_sections_match_apple_ld_across_fixture_matrix() {
2715
-    if !have_xcrun() || !have_xcrun_tool("ld") {
2716
-        eprintln!("skipping: xcrun as/ld unavailable");
3523
+fn linker_run_emits_map_file() {
3524
+    if !have_xcrun() {
3525
+        eprintln!("skipping: xcrun as unavailable");
27173526
         return;
27183527
     }
2719
-    let Some(sdk) = sdk_path() else {
2720
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
3528
+
3529
+    let obj = scratch("map-main.o");
3530
+    let out = scratch("map-main.out");
3531
+    let map = scratch("map-main.map");
3532
+    let src = r#"
3533
+        .section __TEXT,__text,regular,pure_instructions
3534
+        .globl _main
3535
+        _main:
3536
+            mov w0, #0
3537
+            ret
3538
+        .subsections_via_symbols
3539
+    "#;
3540
+    if let Err(e) = assemble(src, &obj) {
3541
+        eprintln!("skipping: assemble failed: {e}");
27213542
         return;
3543
+    }
3544
+
3545
+    let opts = LinkOptions {
3546
+        inputs: vec![obj.clone()],
3547
+        output: Some(out.clone()),
3548
+        map: Some(map.clone()),
3549
+        kind: OutputKind::Executable,
3550
+        ..LinkOptions::default()
27223551
     };
2723
-    let Some(sdk_ver) = sdk_version() else {
2724
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
3552
+    Linker::run(&opts).unwrap();
3553
+
3554
+    let map_text = fs::read_to_string(&map).unwrap();
3555
+    assert!(map_text.contains("# Path:"));
3556
+    assert!(map_text.contains("# Object files:"));
3557
+    assert!(map_text.contains("linker synthesized"));
3558
+    assert!(map_text.contains(&obj.display().to_string()));
3559
+    assert!(map_text.contains("# Sections:"));
3560
+    assert!(map_text.contains("__TEXT"));
3561
+    assert!(map_text.contains("__text"));
3562
+    assert!(map_text.contains("# Symbols:"));
3563
+    assert!(map_text.contains("_main"));
3564
+    assert!(map_text.contains("# Dead stripped:"));
3565
+
3566
+    let _ = fs::remove_file(obj);
3567
+    let _ = fs::remove_file(out);
3568
+    let _ = fs::remove_file(map);
3569
+}
3570
+
3571
+#[test]
3572
+fn linker_run_map_lists_dead_stripped_symbols() {
3573
+    if !have_xcrun() {
3574
+        eprintln!("skipping: xcrun as unavailable");
3575
+        return;
3576
+    }
3577
+
3578
+    let main_obj = scratch("map-dead-main.o");
3579
+    let helper_obj = scratch("map-dead-helper.o");
3580
+    let unused_obj = scratch("map-dead-unused.o");
3581
+    let out = scratch("map-dead.out");
3582
+    let map = scratch("map-dead.map");
3583
+    let main_src = r#"
3584
+        .section __TEXT,__text,regular,pure_instructions
3585
+        .globl _main
3586
+        _main:
3587
+            bl _helper
3588
+            mov w0, #0
3589
+            ret
3590
+        .subsections_via_symbols
3591
+    "#;
3592
+    let helper_src = r#"
3593
+        .section __TEXT,__text,regular,pure_instructions
3594
+        .globl _helper
3595
+        _helper:
3596
+            ret
3597
+        .subsections_via_symbols
3598
+    "#;
3599
+    let unused_src = r#"
3600
+        .section __TEXT,__text,regular,pure_instructions
3601
+        .globl _unused
3602
+        _unused:
3603
+            ret
3604
+        .subsections_via_symbols
3605
+    "#;
3606
+    if let Err(e) = assemble(main_src, &main_obj) {
3607
+        eprintln!("skipping: assemble failed: {e}");
27253608
         return;
3609
+    }
3610
+    if let Err(e) = assemble(helper_src, &helper_obj) {
3611
+        eprintln!("skipping: assemble failed: {e}");
3612
+        let _ = fs::remove_file(main_obj);
3613
+        return;
3614
+    }
3615
+    if let Err(e) = assemble(unused_src, &unused_obj) {
3616
+        eprintln!("skipping: assemble failed: {e}");
3617
+        let _ = fs::remove_file(main_obj);
3618
+        let _ = fs::remove_file(helper_obj);
3619
+        return;
3620
+    }
3621
+
3622
+    let opts = LinkOptions {
3623
+        inputs: vec![main_obj.clone(), helper_obj.clone(), unused_obj.clone()],
3624
+        output: Some(out.clone()),
3625
+        map: Some(map.clone()),
3626
+        dead_strip: true,
3627
+        kind: OutputKind::Executable,
3628
+        ..LinkOptions::default()
27263629
     };
2727
-    const TEXT: SectionCase = SectionCase {
2728
-        segname: "__TEXT",
2729
-        sectname: "__text",
3630
+    Linker::run(&opts).unwrap();
3631
+
3632
+    let map_text = fs::read_to_string(&map).unwrap();
3633
+    let dead_stripped_idx = map_text.find("# Dead stripped:").unwrap();
3634
+    let dead_stripped = &map_text[dead_stripped_idx..];
3635
+    assert!(dead_stripped.contains("_unused"));
3636
+    assert!(!dead_stripped.contains("_helper"));
3637
+
3638
+    let _ = fs::remove_file(main_obj);
3639
+    let _ = fs::remove_file(helper_obj);
3640
+    let _ = fs::remove_file(unused_obj);
3641
+    let _ = fs::remove_file(out);
3642
+    let _ = fs::remove_file(map);
3643
+}
3644
+
3645
+#[test]
3646
+fn linker_run_map_lists_folded_symbols_under_icf_safe() {
3647
+    if !have_xcrun() {
3648
+        eprintln!("skipping: xcrun as unavailable");
3649
+        return;
3650
+    }
3651
+
3652
+    let obj = scratch("map-icf-folded.o");
3653
+    let out = scratch("map-icf-folded.out");
3654
+    let map = scratch("map-icf-folded.map");
3655
+    let src = r#"
3656
+        .section __TEXT,__text,regular,pure_instructions
3657
+        .globl _main
3658
+        _main:
3659
+            bl _helper1
3660
+            bl _helper2
3661
+            mov w0, #0
3662
+            ret
3663
+
3664
+        .private_extern _helper1
3665
+        _helper1:
3666
+            mov w0, #7
3667
+            ret
3668
+
3669
+        .private_extern _helper2
3670
+        _helper2:
3671
+            mov w0, #7
3672
+            ret
3673
+        .subsections_via_symbols
3674
+    "#;
3675
+    if let Err(e) = assemble(src, &obj) {
3676
+        eprintln!("skipping: assemble failed: {e}");
3677
+        return;
3678
+    }
3679
+
3680
+    let opts = LinkOptions {
3681
+        inputs: vec![obj.clone()],
3682
+        output: Some(out.clone()),
3683
+        map: Some(map.clone()),
3684
+        icf_mode: afs_ld::IcfMode::Safe,
3685
+        kind: OutputKind::Executable,
3686
+        ..LinkOptions::default()
27303687
     };
2731
-    const CONST: SectionCase = SectionCase {
2732
-        segname: "__TEXT",
2733
-        sectname: "__const",
3688
+    Linker::run(&opts).unwrap();
3689
+
3690
+    let map_text = fs::read_to_string(&map).unwrap();
3691
+    let folded_idx = map_text.find("# Folded symbols:").unwrap();
3692
+    let folded = &map_text[folded_idx..];
3693
+    assert!(folded.contains("_helper2 folded to _helper1"));
3694
+
3695
+    let _ = fs::remove_file(obj);
3696
+    let _ = fs::remove_file(out);
3697
+    let _ = fs::remove_file(map);
3698
+}
3699
+
3700
+#[test]
3701
+fn linker_run_carries_tbd_inputs_into_load_commands() {
3702
+    if !have_xcrun() {
3703
+        eprintln!("skipping: xcrun unavailable");
3704
+        return;
3705
+    }
3706
+    let Some(sdk) = sdk_path() else {
3707
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
3708
+        return;
27343709
     };
3710
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
3711
+    if !tbd.exists() {
3712
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
3713
+        return;
3714
+    }
27353715
 
2736
-    let cases = [
2737
-        ParityCase {
2738
-            name: "branch-forward",
2739
-            src: r#"
2740
-                .section __TEXT,__text,regular,pure_instructions
2741
-                .globl _main
2742
-                _main:
2743
-                    bl _helper
2744
-                    ret
2745
-                _helper:
2746
-                    ret
2747
-                .subsections_via_symbols
2748
-            "#,
2749
-            check: ParityCheck::ExactSections(&[TEXT]),
2750
-        },
2751
-        ParityCase {
2752
-            name: "branch-backward",
2753
-            src: r#"
2754
-                .section __TEXT,__text,regular,pure_instructions
2755
-                .globl _helper
2756
-                _helper:
2757
-                    ret
2758
-                .globl _main
2759
-                _main:
2760
-                    bl _helper
2761
-                    ret
2762
-                .subsections_via_symbols
2763
-            "#,
2764
-            check: ParityCheck::ExactSections(&[TEXT]),
2765
-        },
2766
-        ParityCase {
2767
-            name: "adrp-add-intra-text-forward",
2768
-            src: r#"
2769
-                .section __TEXT,__text,regular,pure_instructions
2770
-                .globl _main
2771
-                _main:
2772
-                    adrp x0, _target@PAGE
2773
-                    add x0, x0, _target@PAGEOFF
2774
-                    ret
2775
-                .space 0x4ff4
2776
-                _target:
2777
-                    .quad 0
2778
-                .subsections_via_symbols
2779
-            "#,
2780
-            check: ParityCheck::PageRef {
2781
-                section: TEXT,
2782
-                site_offset: 0,
2783
-                target_offset: 0x5000,
2784
-                kind: PageRefKind::Add,
2785
-            },
2786
-        },
2787
-        ParityCase {
2788
-            name: "adrp-add-intra-text-backward",
2789
-            src: r#"
2790
-                .section __TEXT,__text,regular,pure_instructions
2791
-                _target:
2792
-                    .quad 0x55
2793
-                .space 0x4ff8
2794
-                .globl _main
2795
-                _main:
3716
+    let obj = scratch("tbd-main.o");
3717
+    let out = scratch("tbd-a.out");
3718
+    let src = r#"
3719
+        .section __TEXT,__text,regular,pure_instructions
3720
+        .globl _main
3721
+        _main:
3722
+            mov x0, #0
3723
+            ret
3724
+        .subsections_via_symbols
3725
+    "#;
3726
+    if let Err(e) = assemble(src, &obj) {
3727
+        eprintln!("skipping: assemble failed: {e}");
3728
+        return;
3729
+    }
3730
+
3731
+    let opts = LinkOptions {
3732
+        inputs: vec![obj.clone(), tbd.clone()],
3733
+        output: Some(out.clone()),
3734
+        kind: OutputKind::Executable,
3735
+        ..LinkOptions::default()
3736
+    };
3737
+    Linker::run(&opts).unwrap();
3738
+
3739
+    let bytes = fs::read(&out).unwrap();
3740
+    let header = parse_header(&bytes).unwrap();
3741
+    let commands = parse_commands(&header, &bytes).unwrap();
3742
+    assert!(
3743
+        commands.iter().any(|cmd| matches!(
3744
+            cmd,
3745
+            LoadCommand::Dylib(d) if d.cmd == afs_ld::macho::constants::LC_LOAD_DYLIB
3746
+        )),
3747
+        "expected at least one LC_LOAD_DYLIB in output"
3748
+    );
3749
+
3750
+    let _ = fs::remove_file(obj);
3751
+    let _ = fs::remove_file(out);
3752
+}
3753
+
3754
+#[test]
3755
+fn linker_run_handles_non_standard_segment_without_panicking() {
3756
+    if !have_xcrun() {
3757
+        eprintln!("skipping: xcrun as unavailable");
3758
+        return;
3759
+    }
3760
+
3761
+    let obj = scratch("custom-segment.o");
3762
+    let out = scratch("custom-segment.out");
3763
+    let src = r#"
3764
+        .section __FOO,__bar
3765
+        .globl _custom
3766
+        _custom:
3767
+            .quad 1
3768
+        .subsections_via_symbols
3769
+    "#;
3770
+    if let Err(e) = assemble(src, &obj) {
3771
+        eprintln!("skipping: assemble failed: {e}");
3772
+        return;
3773
+    }
3774
+
3775
+    let opts = LinkOptions {
3776
+        inputs: vec![obj.clone()],
3777
+        output: Some(out.clone()),
3778
+        kind: OutputKind::Executable,
3779
+        ..LinkOptions::default()
3780
+    };
3781
+    Linker::run(&opts).unwrap();
3782
+
3783
+    let bytes = fs::read(&out).unwrap();
3784
+    let header = parse_header(&bytes).unwrap();
3785
+    let commands = parse_commands(&header, &bytes).unwrap();
3786
+    assert!(commands.iter().any(|cmd| match cmd {
3787
+        LoadCommand::Segment64(seg) => seg.segname_str() == "__FOO",
3788
+        _ => false,
3789
+    }));
3790
+
3791
+    let _ = fs::remove_file(obj);
3792
+    let _ = fs::remove_file(out);
3793
+}
3794
+
3795
+#[test]
3796
+fn linker_run_uses_requested_entry_symbol() {
3797
+    if !have_xcrun() {
3798
+        eprintln!("skipping: xcrun as unavailable");
3799
+        return;
3800
+    }
3801
+
3802
+    let obj = scratch("entry.o");
3803
+    let out = scratch("entry.out");
3804
+    let src = r#"
3805
+        .section __TEXT,__text,regular,pure_instructions
3806
+        .globl _main
3807
+        _main:
3808
+            ret
3809
+        .globl _alt
3810
+        _alt:
3811
+            mov x0, #1
3812
+            ret
3813
+        .subsections_via_symbols
3814
+    "#;
3815
+    if let Err(e) = assemble(src, &obj) {
3816
+        eprintln!("skipping: assemble failed: {e}");
3817
+        return;
3818
+    }
3819
+
3820
+    let opts = LinkOptions {
3821
+        inputs: vec![obj.clone()],
3822
+        output: Some(out.clone()),
3823
+        entry: Some("_alt".into()),
3824
+        kind: OutputKind::Executable,
3825
+        ..LinkOptions::default()
3826
+    };
3827
+    Linker::run(&opts).unwrap();
3828
+
3829
+    let bytes = fs::read(&out).unwrap();
3830
+    let header = parse_header(&bytes).unwrap();
3831
+    let commands = parse_commands(&header, &bytes).unwrap();
3832
+    let mut text_offset = None;
3833
+    let mut main_entryoff = None;
3834
+    for cmd in commands {
3835
+        match cmd {
3836
+            LoadCommand::Segment64(seg) => {
3837
+                for section in seg.sections {
3838
+                    if section.sectname_str() == "__text" {
3839
+                        text_offset = Some(section.offset as u64);
3840
+                    }
3841
+                }
3842
+            }
3843
+            LoadCommand::Raw { cmd, data, .. } if cmd == afs_ld::macho::constants::LC_MAIN => {
3844
+                let mut buf = [0u8; 8];
3845
+                buf.copy_from_slice(&data[0..8]);
3846
+                main_entryoff = Some(u64::from_le_bytes(buf));
3847
+            }
3848
+            _ => {}
3849
+        }
3850
+    }
3851
+
3852
+    let text_offset = text_offset.expect("text section offset");
3853
+    let main_entryoff = main_entryoff.expect("LC_MAIN entryoff");
3854
+    assert!(
3855
+        main_entryoff > text_offset,
3856
+        "expected custom entry to land after start of __text: text={text_offset}, entry={main_entryoff}"
3857
+    );
3858
+
3859
+    let _ = fs::remove_file(obj);
3860
+    let _ = fs::remove_file(out);
3861
+}
3862
+
3863
+#[test]
3864
+fn linker_run_defaults_entry_to_main_symbol() {
3865
+    if !have_xcrun() {
3866
+        eprintln!("skipping: xcrun as unavailable");
3867
+        return;
3868
+    }
3869
+
3870
+    let obj = scratch("default-entry.o");
3871
+    let out = scratch("default-entry.out");
3872
+    let src = r#"
3873
+        .section __TEXT,__text,regular,pure_instructions
3874
+        .globl _helper
3875
+        _helper:
3876
+            mov w0, #7
3877
+            ret
3878
+        .globl _main
3879
+        _main:
3880
+            mov w0, #0
3881
+            ret
3882
+        .subsections_via_symbols
3883
+    "#;
3884
+    if let Err(e) = assemble(src, &obj) {
3885
+        eprintln!("skipping: assemble failed: {e}");
3886
+        return;
3887
+    }
3888
+
3889
+    let opts = LinkOptions {
3890
+        inputs: vec![obj.clone()],
3891
+        output: Some(out.clone()),
3892
+        kind: OutputKind::Executable,
3893
+        ..LinkOptions::default()
3894
+    };
3895
+    Linker::run(&opts).unwrap();
3896
+
3897
+    let status = Command::new(&out).status().unwrap();
3898
+    assert_eq!(
3899
+        status.code(),
3900
+        Some(0),
3901
+        "default executable entry should prefer _main over the first text atom"
3902
+    );
3903
+
3904
+    let _ = fs::remove_file(obj);
3905
+    let _ = fs::remove_file(out);
3906
+}
3907
+
3908
+#[test]
3909
+fn linker_run_applies_core_arm64_relocations() {
3910
+    if !have_xcrun() {
3911
+        eprintln!("skipping: xcrun as unavailable");
3912
+        return;
3913
+    }
3914
+
3915
+    let obj = scratch("relocs.o");
3916
+    let out = scratch("relocs.out");
3917
+    let src = r#"
3918
+        .section __TEXT,__text,regular,pure_instructions
3919
+        .globl _main
3920
+        .globl _helper
3921
+        _main:
3922
+            adrp x0, _target@PAGE
3923
+            add x0, x0, _target@PAGEOFF
3924
+            bl _helper
3925
+            ret
3926
+        _helper:
3927
+            ret
3928
+
3929
+        .section __DATA,__data
3930
+        .p2align 3
3931
+        _target:
3932
+            .quad _helper
3933
+
3934
+        .section __TEXT,__const
3935
+        .p2align 3
3936
+        _delta:
3937
+            .quad _helper - _main
3938
+        .subsections_via_symbols
3939
+    "#;
3940
+    if let Err(e) = assemble(src, &obj) {
3941
+        eprintln!("skipping: assemble failed: {e}");
3942
+        return;
3943
+    }
3944
+
3945
+    let opts = LinkOptions {
3946
+        inputs: vec![obj.clone()],
3947
+        output: Some(out.clone()),
3948
+        kind: OutputKind::Executable,
3949
+        ..LinkOptions::default()
3950
+    };
3951
+    Linker::run(&opts).unwrap();
3952
+
3953
+    let bytes = fs::read(&out).unwrap();
3954
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").expect("text section");
3955
+    let (data_addr, data) = output_section(&bytes, "__DATA", "__data").expect("data section");
3956
+    let (_, cdata) = output_section(&bytes, "__TEXT", "__const").expect("const section");
3957
+
3958
+    let adrp = u32::from_le_bytes(text[0..4].try_into().unwrap());
3959
+    let add = u32::from_le_bytes(text[4..8].try_into().unwrap());
3960
+    let branch = u32::from_le_bytes(text[8..12].try_into().unwrap());
3961
+    let data_ptr = u64::from_le_bytes(data[0..8].try_into().unwrap());
3962
+    let delta = u64::from_le_bytes(cdata[0..8].try_into().unwrap());
3963
+
3964
+    let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
3965
+    let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
3966
+    let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
3967
+    let adrp_base = ((text_addr as i64) & !0xfff) + (adrp_pages << 12);
3968
+    let add_imm = ((add >> 10) & 0xfff) as u64;
3969
+    let reconstructed_target = (adrp_base as u64) + add_imm;
3970
+
3971
+    assert_eq!(
3972
+        reconstructed_target, data_addr,
3973
+        "ADRP+ADD should resolve _target"
3974
+    );
3975
+    assert_eq!(
3976
+        branch & 0x03ff_ffff,
3977
+        0x2,
3978
+        "BL should branch forward 8 bytes"
3979
+    );
3980
+    assert_eq!(
3981
+        data_ptr,
3982
+        text_addr + 16,
3983
+        ".quad _helper should point at helper"
3984
+    );
3985
+    assert_eq!(delta, 16, "_helper - _main should fold through SUBTRACTOR");
3986
+
3987
+    let _ = fs::remove_file(obj);
3988
+    let _ = fs::remove_file(out);
3989
+}
3990
+
3991
+fn sign_extend_21(value: i64) -> i64 {
3992
+    if value & (1 << 20) != 0 {
3993
+        value | !0x1f_ffff
3994
+    } else {
3995
+        value
3996
+    }
3997
+}
3998
+
3999
+#[test]
4000
+fn linker_run_applies_scaled_pageoff12_for_ldr_x() {
4001
+    if !have_xcrun() {
4002
+        eprintln!("skipping: xcrun as unavailable");
4003
+        return;
4004
+    }
4005
+
4006
+    let obj = scratch("scaled-ldr.o");
4007
+    let out = scratch("scaled-ldr.out");
4008
+    let src = r#"
4009
+        .section __TEXT,__text,regular,pure_instructions
4010
+        .globl _main
4011
+        _main:
4012
+            adrp x0, _target@PAGE
4013
+            ldr x1, [x0, _target@PAGEOFF]
4014
+            ret
4015
+
4016
+        .section __DATA,__data
4017
+        .space 0x3f8
4018
+        .p2align 3
4019
+        .globl _target
4020
+        _target:
4021
+            .quad 0x1122334455667788
4022
+        .subsections_via_symbols
4023
+    "#;
4024
+    if let Err(e) = assemble(src, &obj) {
4025
+        eprintln!("skipping: assemble failed: {e}");
4026
+        return;
4027
+    }
4028
+
4029
+    let opts = LinkOptions {
4030
+        inputs: vec![obj.clone()],
4031
+        output: Some(out.clone()),
4032
+        kind: OutputKind::Executable,
4033
+        ..LinkOptions::default()
4034
+    };
4035
+    Linker::run(&opts).unwrap();
4036
+
4037
+    let bytes = fs::read(&out).unwrap();
4038
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").expect("text section");
4039
+    let (data_addr, data) = output_section(&bytes, "__DATA", "__data").expect("data section");
4040
+
4041
+    let adrp = u32::from_le_bytes(text[0..4].try_into().unwrap());
4042
+    let ldr = u32::from_le_bytes(text[4..8].try_into().unwrap());
4043
+    let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
4044
+    let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
4045
+    let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
4046
+    let adrp_base = ((text_addr as i64) & !0xfff) + (adrp_pages << 12);
4047
+    let ldr_shift = ((ldr >> 30) & 0b11) as u64;
4048
+    let ldr_imm = ((ldr >> 10) & 0xfff) as u64;
4049
+    let reconstructed_target = (adrp_base as u64) + (ldr_imm << ldr_shift);
4050
+
4051
+    assert_eq!(ldr_shift, 3, "expected 64-bit LDR scale");
4052
+    assert_eq!(ldr_imm, 0x7f, "scaled imm12 should store 0x3f8 >> 3");
4053
+    assert_eq!(reconstructed_target, data_addr + 0x3f8);
4054
+    assert_eq!(
4055
+        u64::from_le_bytes(data[0x3f8..0x400].try_into().unwrap()),
4056
+        0x1122334455667788
4057
+    );
4058
+
4059
+    let _ = fs::remove_file(obj);
4060
+    let _ = fs::remove_file(out);
4061
+}
4062
+
4063
+#[test]
4064
+fn relocated_sections_match_apple_ld_across_fixture_matrix() {
4065
+    if !have_xcrun() || !have_xcrun_tool("ld") {
4066
+        eprintln!("skipping: xcrun as/ld unavailable");
4067
+        return;
4068
+    }
4069
+    let Some(sdk) = sdk_path() else {
4070
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4071
+        return;
4072
+    };
4073
+    let Some(sdk_ver) = sdk_version() else {
4074
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
4075
+        return;
4076
+    };
4077
+    const TEXT: SectionCase = SectionCase {
4078
+        segname: "__TEXT",
4079
+        sectname: "__text",
4080
+    };
4081
+    const CONST: SectionCase = SectionCase {
4082
+        segname: "__TEXT",
4083
+        sectname: "__const",
4084
+    };
4085
+
4086
+    let cases = [
4087
+        ParityCase {
4088
+            name: "branch-forward",
4089
+            src: r#"
4090
+                .section __TEXT,__text,regular,pure_instructions
4091
+                .globl _main
4092
+                _main:
4093
+                    bl _helper
4094
+                    ret
4095
+                _helper:
4096
+                    ret
4097
+                .subsections_via_symbols
4098
+            "#,
4099
+            check: ParityCheck::ExactSections(&[TEXT]),
4100
+        },
4101
+        ParityCase {
4102
+            name: "branch-backward",
4103
+            src: r#"
4104
+                .section __TEXT,__text,regular,pure_instructions
4105
+                .globl _helper
4106
+                _helper:
4107
+                    ret
4108
+                .globl _main
4109
+                _main:
4110
+                    bl _helper
4111
+                    ret
4112
+                .subsections_via_symbols
4113
+            "#,
4114
+            check: ParityCheck::ExactSections(&[TEXT]),
4115
+        },
4116
+        ParityCase {
4117
+            name: "adrp-add-intra-text-forward",
4118
+            src: r#"
4119
+                .section __TEXT,__text,regular,pure_instructions
4120
+                .globl _main
4121
+                _main:
4122
+                    adrp x0, _target@PAGE
4123
+                    add x0, x0, _target@PAGEOFF
4124
+                    ret
4125
+                .space 0x4ff4
4126
+                _target:
4127
+                    .quad 0
4128
+                .subsections_via_symbols
4129
+            "#,
4130
+            check: ParityCheck::PageRef {
4131
+                section: TEXT,
4132
+                site_offset: 0,
4133
+                target_offset: 0x5000,
4134
+                kind: PageRefKind::Add,
4135
+            },
4136
+        },
4137
+        ParityCase {
4138
+            name: "adrp-add-intra-text-backward",
4139
+            src: r#"
4140
+                .section __TEXT,__text,regular,pure_instructions
4141
+                _target:
4142
+                    .quad 0x55
4143
+                .space 0x4ff8
4144
+                .globl _main
4145
+                _main:
4146
+                    adrp x0, _target@PAGE
4147
+                    add x0, x0, _target@PAGEOFF
4148
+                    ret
4149
+                .subsections_via_symbols
4150
+            "#,
4151
+            check: ParityCheck::PageRef {
4152
+                section: TEXT,
4153
+                site_offset: 0x5000,
4154
+                target_offset: 0,
4155
+                kind: PageRefKind::Add,
4156
+            },
4157
+        },
4158
+        ParityCase {
4159
+            name: "adrp-ldr-x-intra-text",
4160
+            src: r#"
4161
+                .section __TEXT,__text,regular,pure_instructions
4162
+                .globl _main
4163
+                _main:
4164
+                    adrp x0, _target@PAGE
4165
+                    ldr x1, [x0, _target@PAGEOFF]
4166
+                    ret
4167
+                .space 0x3f4
4168
+                _target:
4169
+                    .quad 0x1122334455667788
4170
+                .subsections_via_symbols
4171
+            "#,
4172
+            check: ParityCheck::PageRef {
4173
+                section: TEXT,
4174
+                site_offset: 0,
4175
+                target_offset: 0x400,
4176
+                kind: PageRefKind::Load,
4177
+            },
4178
+        },
4179
+        ParityCase {
4180
+            name: "adrp-ldr-w-intra-text",
4181
+            src: r#"
4182
+                .section __TEXT,__text,regular,pure_instructions
4183
+                .globl _main
4184
+                _main:
4185
+                    adrp x0, _target@PAGE
4186
+                    ldr w1, [x0, _target@PAGEOFF]
4187
+                    ret
4188
+                .space 0x2f4
4189
+                _target:
4190
+                    .long 0x11223344
4191
+                .subsections_via_symbols
4192
+            "#,
4193
+            check: ParityCheck::PageRef {
4194
+                section: TEXT,
4195
+                site_offset: 0,
4196
+                target_offset: 0x300,
4197
+                kind: PageRefKind::Load,
4198
+            },
4199
+        },
4200
+        ParityCase {
4201
+            name: "adrp-ldrh-intra-text",
4202
+            src: r#"
4203
+                .section __TEXT,__text,regular,pure_instructions
4204
+                .globl _main
4205
+                _main:
4206
+                    adrp x0, _target@PAGE
4207
+                    ldrh w1, [x0, _target@PAGEOFF]
4208
+                    ret
4209
+                .space 0x1f4
4210
+                _target:
4211
+                    .hword 0x3344
4212
+                .subsections_via_symbols
4213
+            "#,
4214
+            check: ParityCheck::PageRef {
4215
+                section: TEXT,
4216
+                site_offset: 0,
4217
+                target_offset: 0x200,
4218
+                kind: PageRefKind::Load,
4219
+            },
4220
+        },
4221
+        ParityCase {
4222
+            name: "adrp-ldrb-intra-text",
4223
+            src: r#"
4224
+                .section __TEXT,__text,regular,pure_instructions
4225
+                .globl _main
4226
+                _main:
4227
+                    adrp x0, _target@PAGE
4228
+                    ldrb w1, [x0, _target@PAGEOFF]
4229
+                    ret
4230
+                .space 0xf4
4231
+                _target:
4232
+                    .byte 0x44
4233
+                .subsections_via_symbols
4234
+            "#,
4235
+            check: ParityCheck::PageRef {
4236
+                section: TEXT,
4237
+                site_offset: 0,
4238
+                target_offset: 0x100,
4239
+                kind: PageRefKind::Load,
4240
+            },
4241
+        },
4242
+        ParityCase {
4243
+            name: "mixed-branch-adrp-text",
4244
+            src: r#"
4245
+                .section __TEXT,__text,regular,pure_instructions
4246
+                .globl _main
4247
+                .globl _helper
4248
+                _main:
27964249
                     adrp x0, _target@PAGE
27974250
                     add x0, x0, _target@PAGEOFF
4251
+                    bl _helper
4252
+                    ret
4253
+                _helper:
4254
+                    ret
4255
+                .space 0xff0
4256
+                _target:
4257
+                    .quad 0x99
4258
+                .subsections_via_symbols
4259
+            "#,
4260
+            check: ParityCheck::PageRef {
4261
+                section: TEXT,
4262
+                site_offset: 0,
4263
+                target_offset: 0x1004,
4264
+                kind: PageRefKind::Add,
4265
+            },
4266
+        },
4267
+        ParityCase {
4268
+            name: "subtractor-positive",
4269
+            src: r#"
4270
+                .section __TEXT,__text,regular,pure_instructions
4271
+                .globl _helper
4272
+                _helper:
4273
+                    ret
4274
+                .globl _main
4275
+                _main:
4276
+                    bl _helper
4277
+                    ret
4278
+                .section __TEXT,__const
4279
+                .p2align 3
4280
+                _delta:
4281
+                    .quad _helper - _main
4282
+                .subsections_via_symbols
4283
+            "#,
4284
+            check: ParityCheck::ExactSections(&[CONST]),
4285
+        },
4286
+        ParityCase {
4287
+            name: "subtractor-negative",
4288
+            src: r#"
4289
+                .section __TEXT,__text,regular,pure_instructions
4290
+                .globl _helper
4291
+                _helper:
4292
+                    ret
4293
+                .globl _main
4294
+                _main:
4295
+                    ret
4296
+                .section __TEXT,__const
4297
+                .p2align 3
4298
+                _delta:
4299
+                    .quad _main - _helper
4300
+                .subsections_via_symbols
4301
+            "#,
4302
+            check: ParityCheck::ExactSections(&[CONST]),
4303
+        },
4304
+        ParityCase {
4305
+            name: "branch-and-subtractor",
4306
+            src: r#"
4307
+                .section __TEXT,__text,regular,pure_instructions
4308
+                .globl _helper
4309
+                _helper:
4310
+                    ret
4311
+                .globl _main
4312
+                _main:
4313
+                    bl _helper
4314
+                    ret
4315
+                .section __TEXT,__const
4316
+                .p2align 3
4317
+                _delta:
4318
+                    .quad _main - _helper
4319
+                .subsections_via_symbols
4320
+            "#,
4321
+            check: ParityCheck::ExactSections(&[TEXT, CONST]),
4322
+        },
4323
+    ];
4324
+
4325
+    let mut failures = Vec::new();
4326
+    for case in &cases {
4327
+        if let Err(err) = assert_case_matches_apple_ld(case, &sdk, &sdk_ver) {
4328
+            failures.push(err);
4329
+        }
4330
+    }
4331
+
4332
+    assert!(
4333
+        failures.is_empty(),
4334
+        "Apple ld parity failures ({} cases):\n{}",
4335
+        failures.len(),
4336
+        failures.join("\n\n")
4337
+    );
4338
+}
4339
+
4340
+#[test]
4341
+fn linker_run_thunks_none_rejects_out_of_range_branch26() {
4342
+    if !have_xcrun() {
4343
+        eprintln!("skipping: xcrun as unavailable");
4344
+        return;
4345
+    }
4346
+
4347
+    let obj = scratch("branch26-range.o");
4348
+    let out = scratch("branch26-range.out");
4349
+    let src = r#"
4350
+        .section __TEXT,__text,regular,pure_instructions
4351
+        .globl _main
4352
+        _main:
4353
+            stp x29, x30, [sp, #-16]!
4354
+            mov x29, sp
4355
+            bl _helper
4356
+            ldp x29, x30, [sp], #16
4357
+            ret
4358
+
4359
+        .zerofill __DATA,__bss,_gap,0x9000000,0
4360
+
4361
+        .section __FAR,__text,regular,pure_instructions
4362
+        .globl _helper
4363
+        _helper:
4364
+            ret
4365
+        .subsections_via_symbols
4366
+    "#;
4367
+    if let Err(e) = assemble(src, &obj) {
4368
+        eprintln!("skipping: assemble failed: {e}");
4369
+        return;
4370
+    }
4371
+
4372
+    let opts = LinkOptions {
4373
+        inputs: vec![obj.clone()],
4374
+        output: Some(out),
4375
+        kind: OutputKind::Executable,
4376
+        thunks: afs_ld::ThunkMode::None,
4377
+        ..LinkOptions::default()
4378
+    };
4379
+    let err = Linker::run(&opts).unwrap_err();
4380
+    match err {
4381
+        LinkError::Reloc(err) => {
4382
+            let msg = err.to_string();
4383
+            assert!(msg.contains("Branch26"), "{msg}");
4384
+            assert!(msg.contains("out of BRANCH26 range"), "{msg}");
4385
+            assert!(msg.contains("_helper"), "{msg}");
4386
+        }
4387
+        other => panic!("expected Reloc error, got {other:?}"),
4388
+    }
4389
+
4390
+    let _ = fs::remove_file(obj);
4391
+}
4392
+
4393
+#[test]
4394
+fn linker_run_inserts_thunk_for_out_of_range_branch26() {
4395
+    if !have_xcrun() || !have_tool("codesign") {
4396
+        eprintln!("skipping: xcrun or codesign unavailable");
4397
+        return;
4398
+    }
4399
+
4400
+    let obj = scratch("branch26-thunk.o");
4401
+    let out = scratch("branch26-thunk.out");
4402
+    let src = r#"
4403
+        .section __TEXT,__text,regular,pure_instructions
4404
+        .globl _main
4405
+        _main:
4406
+            stp x29, x30, [sp, #-16]!
4407
+            mov x29, sp
4408
+            bl _helper
4409
+            ldp x29, x30, [sp], #16
4410
+            ret
4411
+
4412
+        .zerofill __DATA,__bss,_gap,0x9000000,0
4413
+
4414
+        .section __FAR,__text,regular,pure_instructions
4415
+        .globl _helper
4416
+        _helper:
4417
+            mov w0, #0
4418
+            ret
4419
+        .subsections_via_symbols
4420
+    "#;
4421
+    if let Err(e) = assemble(src, &obj) {
4422
+        eprintln!("skipping: assemble failed: {e}");
4423
+        return;
4424
+    }
4425
+
4426
+    let opts = LinkOptions {
4427
+        inputs: vec![obj.clone()],
4428
+        output: Some(out.clone()),
4429
+        kind: OutputKind::Executable,
4430
+        ..LinkOptions::default()
4431
+    };
4432
+    Linker::run(&opts).unwrap();
4433
+
4434
+    let bytes = fs::read(&out).unwrap();
4435
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
4436
+    let (thunks_addr, thunks) = output_section(&bytes, "__TEXT", "__thunks").unwrap();
4437
+    assert_eq!(thunks.len(), 12, "expected one synthetic thunk");
4438
+    assert_eq!(
4439
+        decode_branch_target(&text, text_addr, 8).unwrap(),
4440
+        thunks_addr,
4441
+        "expected _main BL to target __thunks"
4442
+    );
4443
+    assert!(is_adrp(read_insn(&thunks, 0).unwrap()));
4444
+    assert!(is_add_imm_64(read_insn(&thunks, 4).unwrap()));
4445
+    assert_eq!(read_insn(&thunks, 8).unwrap(), 0xd61f_0200);
4446
+
4447
+    let verify = Command::new("codesign")
4448
+        .arg("-v")
4449
+        .arg(&out)
4450
+        .output()
4451
+        .unwrap();
4452
+    assert!(
4453
+        verify.status.success(),
4454
+        "codesign verify failed: {}",
4455
+        String::from_utf8_lossy(&verify.stderr)
4456
+    );
4457
+    let status = Command::new(&out).status().unwrap();
4458
+    assert_eq!(
4459
+        status.code(),
4460
+        Some(0),
4461
+        "expected thunked executable to exit 0"
4462
+    );
4463
+
4464
+    let _ = fs::remove_file(obj);
4465
+    let _ = fs::remove_file(out);
4466
+}
4467
+
4468
+#[test]
4469
+fn linker_run_safe_thunks_do_not_grow_small_programs() {
4470
+    if !have_xcrun() {
4471
+        eprintln!("skipping: xcrun as unavailable");
4472
+        return;
4473
+    }
4474
+
4475
+    let obj = scratch("branch26-small.o");
4476
+    let out = scratch("branch26-small.out");
4477
+    let src = r#"
4478
+        .section __TEXT,__text,regular,pure_instructions
4479
+        .globl _main
4480
+        _main:
4481
+            bl _helper
4482
+            ret
4483
+
4484
+        _helper:
4485
+            mov w0, #0
4486
+            ret
4487
+        .subsections_via_symbols
4488
+    "#;
4489
+    if let Err(e) = assemble(src, &obj) {
4490
+        eprintln!("skipping: assemble failed: {e}");
4491
+        return;
4492
+    }
4493
+
4494
+    let opts = LinkOptions {
4495
+        inputs: vec![obj.clone()],
4496
+        output: Some(out.clone()),
4497
+        kind: OutputKind::Executable,
4498
+        ..LinkOptions::default()
4499
+    };
4500
+    Linker::run(&opts).unwrap();
4501
+
4502
+    assert!(
4503
+        output_section(&fs::read(&out).unwrap(), "__TEXT", "__thunks").is_none(),
4504
+        "small in-range program should not gain __thunks"
4505
+    );
4506
+
4507
+    let _ = fs::remove_file(obj);
4508
+    let _ = fs::remove_file(out);
4509
+}
4510
+
4511
+#[test]
4512
+fn linker_run_thunks_all_forces_shared_thunk_for_in_range_calls() {
4513
+    if !have_xcrun() || !have_tool("codesign") {
4514
+        eprintln!("skipping: xcrun or codesign unavailable");
4515
+        return;
4516
+    }
4517
+
4518
+    let obj = scratch("branch26-thunks-all.o");
4519
+    let out = scratch("branch26-thunks-all.out");
4520
+    let src = r#"
4521
+        .section __TEXT,__text,regular,pure_instructions
4522
+        .globl _main
4523
+        _main:
4524
+            stp x29, x30, [sp, #-16]!
4525
+            mov x29, sp
4526
+            bl _helper
4527
+            bl _helper
4528
+            ldp x29, x30, [sp], #16
4529
+            ret
4530
+
4531
+        _helper:
4532
+            mov w0, #0
4533
+            ret
4534
+        .subsections_via_symbols
4535
+    "#;
4536
+    if let Err(e) = assemble(src, &obj) {
4537
+        eprintln!("skipping: assemble failed: {e}");
4538
+        return;
4539
+    }
4540
+
4541
+    let opts = LinkOptions {
4542
+        inputs: vec![obj.clone()],
4543
+        output: Some(out.clone()),
4544
+        kind: OutputKind::Executable,
4545
+        thunks: afs_ld::ThunkMode::All,
4546
+        ..LinkOptions::default()
4547
+    };
4548
+    Linker::run(&opts).unwrap();
4549
+
4550
+    let bytes = fs::read(&out).unwrap();
4551
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
4552
+    let (thunks_addr, thunks) = output_section(&bytes, "__TEXT", "__thunks").unwrap();
4553
+    assert_eq!(
4554
+        thunks.len(),
4555
+        12,
4556
+        "expected both in-range calls to share one forced thunk"
4557
+    );
4558
+    assert_eq!(
4559
+        decode_branch_target(&text, text_addr, 8).unwrap(),
4560
+        thunks_addr,
4561
+        "expected first BL to route through __thunks under -thunks=all"
4562
+    );
4563
+    assert_eq!(
4564
+        decode_branch_target(&text, text_addr, 12).unwrap(),
4565
+        thunks_addr,
4566
+        "expected second BL to share the same thunk target"
4567
+    );
4568
+
4569
+    let verify = Command::new("codesign")
4570
+        .arg("-v")
4571
+        .arg(&out)
4572
+        .output()
4573
+        .unwrap();
4574
+    assert!(
4575
+        verify.status.success(),
4576
+        "codesign verify failed: {}",
4577
+        String::from_utf8_lossy(&verify.stderr)
4578
+    );
4579
+    let status = Command::new(&out).status().unwrap();
4580
+    assert_eq!(
4581
+        status.code(),
4582
+        Some(0),
4583
+        "expected -thunks=all executable to exit 0"
4584
+    );
4585
+
4586
+    let _ = fs::remove_file(obj);
4587
+    let _ = fs::remove_file(out);
4588
+}
4589
+
4590
+#[test]
4591
+fn linker_run_places_thunks_in_caller_segment() {
4592
+    if !have_xcrun() || !have_tool("codesign") {
4593
+        eprintln!("skipping: xcrun or codesign unavailable");
4594
+        return;
4595
+    }
4596
+
4597
+    let obj = scratch("branch26-custom-segment-thunk.o");
4598
+    let out = scratch("branch26-custom-segment-thunk.out");
4599
+    let src = r#"
4600
+        .section __TEXT,__text,regular,pure_instructions
4601
+        .globl _helper
4602
+        _helper:
4603
+            mov w0, #0
4604
+            ret
4605
+
4606
+        .zerofill __DATA,__bss,_gap,0x9000000,0
4607
+
4608
+        .section __FARCALL,__text,regular,pure_instructions
4609
+        .globl _main
4610
+        _main:
4611
+            stp x29, x30, [sp, #-16]!
4612
+            mov x29, sp
4613
+            bl _helper
4614
+            ldp x29, x30, [sp], #16
4615
+            ret
4616
+        .subsections_via_symbols
4617
+    "#;
4618
+    if let Err(e) = assemble(src, &obj) {
4619
+        eprintln!("skipping: assemble failed: {e}");
4620
+        return;
4621
+    }
4622
+
4623
+    let opts = LinkOptions {
4624
+        inputs: vec![obj.clone()],
4625
+        output: Some(out.clone()),
4626
+        kind: OutputKind::Executable,
4627
+        ..LinkOptions::default()
4628
+    };
4629
+    Linker::run(&opts).unwrap();
4630
+
4631
+    let bytes = fs::read(&out).unwrap();
4632
+    assert!(
4633
+        output_section(&bytes, "__TEXT", "__thunks").is_none(),
4634
+        "expected no __TEXT thunk section for custom-segment caller"
4635
+    );
4636
+    let (text_addr, text) = output_section(&bytes, "__FARCALL", "__text").unwrap();
4637
+    let (thunks_addr, thunks) = output_section(&bytes, "__FARCALL", "__thunks").unwrap();
4638
+    assert_eq!(thunks.len(), 12, "expected one custom-segment thunk");
4639
+    assert_eq!(
4640
+        decode_branch_target(&text, text_addr, 8).unwrap(),
4641
+        thunks_addr,
4642
+        "expected custom-segment BL to target custom-segment thunk"
4643
+    );
4644
+
4645
+    let verify = Command::new("codesign")
4646
+        .arg("-v")
4647
+        .arg(&out)
4648
+        .output()
4649
+        .unwrap();
4650
+    assert!(
4651
+        verify.status.success(),
4652
+        "codesign verify failed: {}",
4653
+        String::from_utf8_lossy(&verify.stderr)
4654
+    );
4655
+
4656
+    let _ = fs::remove_file(obj);
4657
+    let _ = fs::remove_file(out);
4658
+}
4659
+
4660
+#[test]
4661
+fn linker_run_replans_thunks_until_layout_converges() {
4662
+    if !have_xcrun() {
4663
+        eprintln!("skipping: xcrun as unavailable");
4664
+        return;
4665
+    }
4666
+
4667
+    let obj = scratch("branch26-thunk-fixed-point.o");
4668
+    let out = scratch("branch26-thunk-fixed-point.out");
4669
+    let src = r#"
4670
+        .section __TEXT,__text,regular,pure_instructions
4671
+        .globl _main
4672
+        _main:
4673
+            bl _overflow
4674
+            bl _borderline
4675
+            mov w0, #0
4676
+            ret
4677
+
4678
+        .zerofill __TEXT,__apad,_gap,0x7ffffec,2
4679
+
4680
+        .section __TEXT,__late,regular,pure_instructions
4681
+        .globl _borderline
4682
+        _borderline:
4683
+            ret
4684
+
4685
+        .globl _overflow
4686
+        _overflow:
4687
+            ret
4688
+        .subsections_via_symbols
4689
+    "#;
4690
+    if let Err(e) = assemble(src, &obj) {
4691
+        eprintln!("skipping: assemble failed: {e}");
4692
+        return;
4693
+    }
4694
+
4695
+    let opts = LinkOptions {
4696
+        inputs: vec![obj.clone()],
4697
+        output: Some(out.clone()),
4698
+        kind: OutputKind::Executable,
4699
+        ..LinkOptions::default()
4700
+    };
4701
+    Linker::run(&opts).unwrap();
4702
+
4703
+    let bytes = fs::read(&out).unwrap();
4704
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
4705
+    let (thunks_addr, thunks) = output_section(&bytes, "__TEXT", "__thunks").unwrap();
4706
+    assert_eq!(
4707
+        thunks.len(),
4708
+        24,
4709
+        "expected two thunks after fixed-point replanning"
4710
+    );
4711
+    let mut actual_targets = [
4712
+        decode_branch_target(&text, text_addr, 0).unwrap(),
4713
+        decode_branch_target(&text, text_addr, 4).unwrap(),
4714
+    ];
4715
+    actual_targets.sort_unstable();
4716
+    assert_eq!(
4717
+        actual_targets,
4718
+        [thunks_addr, thunks_addr + 12],
4719
+        "expected both branches to redirect through the two thunk slots"
4720
+    );
4721
+
4722
+    let _ = fs::remove_file(obj);
4723
+    let _ = fs::remove_file(out);
4724
+}
4725
+
4726
+#[test]
4727
+fn linker_run_uses_multiple_thunk_islands_within_text_segment() {
4728
+    if !have_xcrun() || !have_tool("codesign") {
4729
+        eprintln!("skipping: xcrun or codesign unavailable");
4730
+        return;
4731
+    }
4732
+
4733
+    let obj = scratch("branch26-multi-island.o");
4734
+    let out = scratch("branch26-multi-island.out");
4735
+    let src = r#"
4736
+        .section __TEXT,__text,regular,pure_instructions
4737
+        .globl _main
4738
+        _main:
4739
+            bl _midcaller
4740
+            mov w0, #0
4741
+            ret
4742
+
4743
+        .zerofill __TEXT,__apad1,_gap1,0x9000000,2
4744
+
4745
+        .section __TEXT,__bmid,regular,pure_instructions
4746
+        .globl _midcaller
4747
+        _midcaller:
4748
+            stp x29, x30, [sp, #-16]!
4749
+            mov x29, sp
4750
+            bl _helper
4751
+            ldp x29, x30, [sp], #16
4752
+            ret
4753
+
4754
+        .zerofill __TEXT,__cpad2,_gap2,0x9000000,2
4755
+
4756
+        .section __TEXT,__dlate,regular,pure_instructions
4757
+        .globl _helper
4758
+        _helper:
4759
+            ret
4760
+        .subsections_via_symbols
4761
+    "#;
4762
+    if let Err(e) = assemble(src, &obj) {
4763
+        eprintln!("skipping: assemble failed: {e}");
4764
+        return;
4765
+    }
4766
+
4767
+    let opts = LinkOptions {
4768
+        inputs: vec![obj.clone()],
4769
+        output: Some(out.clone()),
4770
+        kind: OutputKind::Executable,
4771
+        ..LinkOptions::default()
4772
+    };
4773
+    Linker::run(&opts).unwrap();
4774
+
4775
+    let bytes = fs::read(&out).unwrap();
4776
+    let thunk_sections = output_sections(&bytes, "__TEXT", "__thunks");
4777
+    assert_eq!(
4778
+        thunk_sections.len(),
4779
+        2,
4780
+        "expected one thunk island after __text and one after __mid"
4781
+    );
4782
+    assert!(
4783
+        thunk_sections.iter().all(|(_, bytes)| bytes.len() == 12),
4784
+        "expected one thunk per island"
4785
+    );
4786
+
4787
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
4788
+    let (mid_addr, mid) = output_section(&bytes, "__TEXT", "__bmid").unwrap();
4789
+    let mut actual_targets = [
4790
+        decode_branch_target(&text, text_addr, 0).unwrap(),
4791
+        decode_branch_target(&mid, mid_addr, 8).unwrap(),
4792
+    ];
4793
+    actual_targets.sort_unstable();
4794
+    let mut expected_targets = [thunk_sections[0].0, thunk_sections[1].0];
4795
+    expected_targets.sort_unstable();
4796
+    assert_eq!(
4797
+        actual_targets, expected_targets,
4798
+        "expected the two call sites to route through the two thunk islands"
4799
+    );
4800
+
4801
+    let verify = Command::new("codesign")
4802
+        .arg("-v")
4803
+        .arg(&out)
4804
+        .output()
4805
+        .unwrap();
4806
+    assert!(
4807
+        verify.status.success(),
4808
+        "codesign verify failed: {}",
4809
+        String::from_utf8_lossy(&verify.stderr)
4810
+    );
4811
+
4812
+    let _ = fs::remove_file(obj);
4813
+    let _ = fs::remove_file(out);
4814
+}
4815
+
4816
+#[test]
4817
+fn linker_run_routes_dylib_imports_through_synthetic_sections() {
4818
+    if !have_xcrun() {
4819
+        eprintln!("skipping: xcrun unavailable");
4820
+        return;
4821
+    }
4822
+    let Some(sdk) = sdk_path() else {
4823
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4824
+        return;
4825
+    };
4826
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4827
+    if !tbd.exists() {
4828
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4829
+        return;
4830
+    }
4831
+
4832
+    let obj = scratch("import-reloc.o");
4833
+    let out = scratch("import-reloc.out");
4834
+    let src = r#"
4835
+        .section __TEXT,__text,regular,pure_instructions
4836
+        .globl _main
4837
+        _main:
4838
+            adrp x0, _write@GOTPAGE
4839
+            ldr x0, [x0, _write@GOTPAGEOFF]
4840
+            bl _write
4841
+            ret
4842
+        .subsections_via_symbols
4843
+    "#;
4844
+    if let Err(e) = assemble(src, &obj) {
4845
+        eprintln!("skipping: assemble failed: {e}");
4846
+        return;
4847
+    }
4848
+
4849
+    let opts = LinkOptions {
4850
+        inputs: vec![obj.clone(), tbd.clone()],
4851
+        output: Some(out.clone()),
4852
+        kind: OutputKind::Executable,
4853
+        ..LinkOptions::default()
4854
+    };
4855
+    Linker::run(&opts).unwrap();
4856
+
4857
+    let bytes = fs::read(&out).unwrap();
4858
+    let header = parse_header(&bytes).unwrap();
4859
+    let commands = parse_commands(&header, &bytes).unwrap();
4860
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
4861
+    let (stubs_addr, stubs) = output_section(&bytes, "__TEXT", "__stubs").unwrap();
4862
+    let (helper_addr, helper) = output_section(&bytes, "__TEXT", "__stub_helper").unwrap();
4863
+    let (got_addr, got) = output_section(&bytes, "__DATA_CONST", "__got").unwrap();
4864
+    let (lazy_addr, lazy) = output_section(&bytes, "__DATA", "__la_symbol_ptr").unwrap();
4865
+    let (dyld_private_addr, _) = output_section(&bytes, "__DATA", "__data").unwrap();
4866
+    let stubs_hdr = output_section_header(&bytes, "__TEXT", "__stubs").unwrap();
4867
+    let got_hdr = output_section_header(&bytes, "__DATA_CONST", "__got").unwrap();
4868
+    let lazy_hdr = output_section_header(&bytes, "__DATA", "__la_symbol_ptr").unwrap();
4869
+
4870
+    let symtab = commands
4871
+        .iter()
4872
+        .find_map(|cmd| match cmd {
4873
+            LoadCommand::Symtab(cmd) => Some(*cmd),
4874
+            _ => None,
4875
+        })
4876
+        .unwrap();
4877
+    let dysymtab = commands
4878
+        .iter()
4879
+        .find_map(|cmd| match cmd {
4880
+            LoadCommand::Dysymtab(cmd) => Some(*cmd),
4881
+            _ => None,
4882
+        })
4883
+        .unwrap();
4884
+    let dyld_info = commands
4885
+        .iter()
4886
+        .find_map(|cmd| match cmd {
4887
+            LoadCommand::DyldInfoOnly(cmd) => Some(*cmd),
4888
+            _ => None,
4889
+        })
4890
+        .unwrap();
4891
+    let libsystem_load = commands
4892
+        .iter()
4893
+        .find_map(|cmd| match cmd {
4894
+            LoadCommand::Dylib(cmd)
4895
+                if cmd.cmd == afs_ld::macho::constants::LC_LOAD_DYLIB
4896
+                    && cmd.name == "/usr/lib/libSystem.B.dylib" =>
4897
+            {
4898
+                Some(cmd.clone())
4899
+            }
4900
+            _ => None,
4901
+        })
4902
+        .unwrap();
4903
+    let symbols = parse_nlist_table(&bytes, symtab.symoff, symtab.nsyms).unwrap();
4904
+    let strings = StringTable::from_file(&bytes, symtab.stroff, symtab.strsize).unwrap();
4905
+    let symbol_names: Vec<&str> = symbols
4906
+        .iter()
4907
+        .map(|symbol| strings.get(symbol.strx()).unwrap())
4908
+        .collect();
4909
+
4910
+    assert_eq!(got.len(), 16);
4911
+    assert_eq!(stubs.len(), 12);
4912
+    assert_eq!(helper.len(), 36);
4913
+    assert_eq!(lazy.len(), 8);
4914
+    assert_eq!(symtab.nsyms, 5);
4915
+    assert_eq!(dysymtab.nlocalsym, 1);
4916
+    assert_eq!(dysymtab.nextdefsym, 2);
4917
+    assert_eq!(dysymtab.nundefsym, 2);
4918
+    assert_eq!(dysymtab.nindirectsyms, 4);
4919
+    assert_eq!(stubs_hdr.reserved1, 0);
4920
+    assert_eq!(got_hdr.reserved1, 1);
4921
+    assert_eq!(lazy_hdr.reserved1, 3);
4922
+    assert_eq!(stubs_hdr.reserved2, 12);
4923
+    assert!(libsystem_load.current_version >= (1 << 16));
4924
+    assert_eq!(libsystem_load.compatibility_version, 1 << 16);
4925
+    assert!(dyld_info.rebase_size > 0);
4926
+    assert!(dyld_info.bind_size > 0);
4927
+    assert!(dyld_info.lazy_bind_size > 0);
4928
+    assert_eq!(
4929
+        decode_page_reference(&text, text_addr, 0, &PageRefKind::Load).unwrap(),
4930
+        got_addr
4931
+    );
4932
+    assert_eq!(
4933
+        decode_branch_target(&text, text_addr, 8).unwrap(),
4934
+        stubs_addr
4935
+    );
4936
+    assert_eq!(
4937
+        decode_page_reference(&stubs, stubs_addr, 0, &PageRefKind::Load).unwrap(),
4938
+        lazy_addr
4939
+    );
4940
+    assert_eq!(read_insn(&stubs, 8).unwrap(), 0xd61f0200);
4941
+    assert_eq!(
4942
+        u64::from_le_bytes(lazy[0..8].try_into().unwrap()),
4943
+        helper_addr + 24
4944
+    );
4945
+    assert_eq!(
4946
+        decode_page_reference(&helper, helper_addr, 0, &PageRefKind::Add).unwrap(),
4947
+        dyld_private_addr
4948
+    );
4949
+    assert_eq!(
4950
+        decode_page_reference(&helper, helper_addr, 12, &PageRefKind::Load).unwrap(),
4951
+        got_addr + 8
4952
+    );
4953
+    assert_eq!(read_insn(&helper, 20).unwrap(), 0xd61f0200);
4954
+    assert_eq!(read_insn(&helper, 24).unwrap(), 0x1800_0050);
4955
+    assert_eq!(
4956
+        decode_branch_target(&helper, helper_addr, 28).unwrap(),
4957
+        helper_addr
4958
+    );
4959
+    assert_eq!(u32::from_le_bytes(helper[32..36].try_into().unwrap()), 0);
4960
+    let (locals, extdefs, undefs) = symbol_partition_names(&bytes);
4961
+    assert_eq!(locals, vec!["__dyld_private".to_string()]);
4962
+    assert_eq!(
4963
+        extdefs,
4964
+        vec!["__mh_execute_header".to_string(), "_main".to_string()]
4965
+    );
4966
+    assert_eq!(
4967
+        undefs,
4968
+        vec!["_write".to_string(), "dyld_stub_binder".to_string()]
4969
+    );
4970
+    assert!(symbol_names.contains(&"__dyld_private"));
4971
+    assert!(symbols[dysymtab.iundefsym as usize..]
4972
+        .iter()
4973
+        .all(|symbol| symbol.kind() == SymKind::Undef));
4974
+    assert!(symbols[dysymtab.iundefsym as usize..]
4975
+        .iter()
4976
+        .all(|symbol| symbol.library_ordinal().unwrap() > 0));
4977
+    assert!(symbol_names.contains(&"_write"));
4978
+    assert!(symbol_names.contains(&"dyld_stub_binder"));
4979
+
4980
+    let _ = fs::remove_file(out);
4981
+    let _ = fs::remove_file(obj);
4982
+}
4983
+
4984
+#[test]
4985
+fn synthetic_import_surfaces_match_apple_ld_classic_lazy_model() {
4986
+    if !have_xcrun() || !have_xcrun_tool("ld") {
4987
+        eprintln!("skipping: xcrun as/ld unavailable");
4988
+        return;
4989
+    }
4990
+    let Some(sdk) = sdk_path() else {
4991
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4992
+        return;
4993
+    };
4994
+    let Some(sdk_ver) = sdk_version() else {
4995
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
4996
+        return;
4997
+    };
4998
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4999
+    if !tbd.exists() {
5000
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5001
+        return;
5002
+    }
5003
+
5004
+    let obj = scratch("import-parity.o");
5005
+    let our_out = scratch("import-parity-ours.out");
5006
+    let apple_out = scratch("import-parity-apple.out");
5007
+    let src = r#"
5008
+        .section __TEXT,__text,regular,pure_instructions
5009
+        .globl _main
5010
+        _main:
5011
+            adrp x0, _write@GOTPAGE
5012
+            ldr x0, [x0, _write@GOTPAGEOFF]
5013
+            bl _write
5014
+            ret
5015
+        .subsections_via_symbols
5016
+    "#;
5017
+    if let Err(e) = assemble(src, &obj) {
5018
+        eprintln!("skipping: assemble failed: {e}");
5019
+        return;
5020
+    }
5021
+
5022
+    let opts = LinkOptions {
5023
+        inputs: vec![obj.clone(), tbd],
5024
+        output: Some(our_out.clone()),
5025
+        kind: OutputKind::Executable,
5026
+        ..LinkOptions::default()
5027
+    };
5028
+    Linker::run(&opts).unwrap();
5029
+    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5030
+
5031
+    let our_bytes = fs::read(&our_out).unwrap();
5032
+    let apple_bytes = fs::read(&apple_out).unwrap();
5033
+
5034
+    for (segname, sectname) in [("__TEXT", "__stubs"), ("__TEXT", "__stub_helper")] {
5035
+        let (_, ours) = output_section(&our_bytes, segname, sectname).unwrap();
5036
+        let (_, apple) = output_section(&apple_bytes, segname, sectname).unwrap();
5037
+        let diff = diff_macho(&ours, &apple);
5038
+        assert!(
5039
+            diff.is_clean(),
5040
+            "{segname},{sectname} diverged from Apple ld: {:#?}",
5041
+            diff.critical
5042
+        );
5043
+    }
5044
+
5045
+    let (our_helper_addr, _) = output_section(&our_bytes, "__TEXT", "__stub_helper").unwrap();
5046
+    let (apple_helper_addr, _) = output_section(&apple_bytes, "__TEXT", "__stub_helper").unwrap();
5047
+    let (_, our_lazy) = output_section(&our_bytes, "__DATA", "__la_symbol_ptr").unwrap();
5048
+    let (_, apple_lazy) = output_section(&apple_bytes, "__DATA", "__la_symbol_ptr").unwrap();
5049
+    assert_eq!(
5050
+        u64::from_le_bytes(our_lazy[0..8].try_into().unwrap()) - our_helper_addr,
5051
+        24
5052
+    );
5053
+    assert_eq!(
5054
+        u64::from_le_bytes(apple_lazy[0..8].try_into().unwrap()) - apple_helper_addr,
5055
+        24
5056
+    );
5057
+
5058
+    assert_eq!(
5059
+        load_dylib_names(&our_bytes).unwrap(),
5060
+        load_dylib_names(&apple_bytes).unwrap()
5061
+    );
5062
+    assert_eq!(
5063
+        segment_flags(&our_bytes, "__DATA_CONST"),
5064
+        Some(SG_READ_ONLY)
5065
+    );
5066
+    assert_eq!(
5067
+        segment_flags(&our_bytes, "__DATA_CONST"),
5068
+        segment_flags(&apple_bytes, "__DATA_CONST")
5069
+    );
5070
+
5071
+    let our_rebases = decode_rebase_records(&our_bytes).unwrap();
5072
+    let apple_rebases = decode_rebase_records(&apple_bytes).unwrap();
5073
+    assert!(our_rebases
5074
+        .iter()
5075
+        .all(|record| record.rebase_type == REBASE_TYPE_POINTER));
5076
+    assert_eq!(our_rebases, apple_rebases);
5077
+    assert_eq!(
5078
+        decode_bind_records(&our_bytes, false).unwrap(),
5079
+        decode_bind_records(&apple_bytes, false).unwrap()
5080
+    );
5081
+    assert_eq!(
5082
+        dyld_info_stream(&our_bytes, DyldInfoStreamKind::WeakBind).unwrap(),
5083
+        dyld_info_stream(&apple_bytes, DyldInfoStreamKind::WeakBind).unwrap()
5084
+    );
5085
+    assert_eq!(
5086
+        decode_bind_records(&our_bytes, true).unwrap(),
5087
+        decode_bind_records(&apple_bytes, true).unwrap()
5088
+    );
5089
+    assert_eq!(
5090
+        canonical_lazy_bind_stream(&our_bytes).unwrap(),
5091
+        canonical_lazy_bind_stream(&apple_bytes).unwrap()
5092
+    );
5093
+    assert_eq!(
5094
+        indirect_symbol_table(&our_bytes),
5095
+        indirect_symbol_table(&apple_bytes)
5096
+    );
5097
+
5098
+    let _ = fs::remove_file(apple_out);
5099
+    let _ = fs::remove_file(our_out);
5100
+    let _ = fs::remove_file(obj);
5101
+}
5102
+
5103
+#[test]
5104
+fn classic_lazy_surfaces_match_apple_ld_across_fixture_matrix() {
5105
+    if !have_xcrun() || !have_xcrun_tool("ld") {
5106
+        eprintln!("skipping: xcrun as/ld unavailable");
5107
+        return;
5108
+    }
5109
+    let Some(sdk) = sdk_path() else {
5110
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
5111
+        return;
5112
+    };
5113
+    let Some(sdk_ver) = sdk_version() else {
5114
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
5115
+        return;
5116
+    };
5117
+
5118
+    let cases = [
5119
+        ClassicLazyParityCase {
5120
+            name: "single-got-and-call",
5121
+            src: r#"
5122
+                .section __TEXT,__text,regular,pure_instructions
5123
+                .globl _main
5124
+                _main:
5125
+                    adrp x0, _write@GOTPAGE
5126
+                    ldr x0, [x0, _write@GOTPAGEOFF]
5127
+                    bl _write
27985128
                     ret
27995129
                 .subsections_via_symbols
28005130
             "#,
2801
-            check: ParityCheck::PageRef {
2802
-                section: TEXT,
2803
-                site_offset: 0x5000,
2804
-                target_offset: 0,
2805
-                kind: PageRefKind::Add,
2806
-            },
28075131
         },
2808
-        ParityCase {
2809
-            name: "adrp-ldr-x-intra-text",
5132
+        ClassicLazyParityCase {
5133
+            name: "batched-got-and-calls",
28105134
             src: r#"
28115135
                 .section __TEXT,__text,regular,pure_instructions
28125136
                 .globl _main
28135137
                 _main:
2814
-                    adrp x0, _target@PAGE
2815
-                    ldr x1, [x0, _target@PAGEOFF]
5138
+                    adrp x0, _write@GOTPAGE
5139
+                    ldr x0, [x0, _write@GOTPAGEOFF]
5140
+                    bl _write
5141
+                    adrp x1, _close@GOTPAGE
5142
+                    ldr x1, [x1, _close@GOTPAGEOFF]
5143
+                    bl _close
5144
+                    adrp x2, _read@GOTPAGE
5145
+                    ldr x2, [x2, _read@GOTPAGEOFF]
5146
+                    bl _read
28165147
                     ret
2817
-                .space 0x3f4
2818
-                _target:
2819
-                    .quad 0x1122334455667788
28205148
                 .subsections_via_symbols
28215149
             "#,
2822
-            check: ParityCheck::PageRef {
2823
-                section: TEXT,
2824
-                site_offset: 0,
2825
-                target_offset: 0x400,
2826
-                kind: PageRefKind::Load,
2827
-            },
28285150
         },
2829
-        ParityCase {
2830
-            name: "adrp-ldr-w-intra-text",
5151
+        ClassicLazyParityCase {
5152
+            name: "branch-only-calls",
28315153
             src: r#"
28325154
                 .section __TEXT,__text,regular,pure_instructions
28335155
                 .globl _main
28345156
                 _main:
2835
-                    adrp x0, _target@PAGE
2836
-                    ldr w1, [x0, _target@PAGEOFF]
5157
+                    bl _write
5158
+                    bl _close
5159
+                    bl _read
28375160
                     ret
2838
-                .space 0x2f4
2839
-                _target:
2840
-                    .long 0x11223344
28415161
                 .subsections_via_symbols
28425162
             "#,
2843
-            check: ParityCheck::PageRef {
2844
-                section: TEXT,
2845
-                site_offset: 0,
2846
-                target_offset: 0x300,
2847
-                kind: PageRefKind::Load,
2848
-            },
28495163
         },
2850
-        ParityCase {
2851
-            name: "adrp-ldrh-intra-text",
5164
+        ClassicLazyParityCase {
5165
+            name: "deduped-import",
28525166
             src: r#"
28535167
                 .section __TEXT,__text,regular,pure_instructions
28545168
                 .globl _main
28555169
                 _main:
2856
-                    adrp x0, _target@PAGE
2857
-                    ldrh w1, [x0, _target@PAGEOFF]
5170
+                    adrp x0, _write@GOTPAGE
5171
+                    ldr x0, [x0, _write@GOTPAGEOFF]
5172
+                    bl _write
5173
+                    bl _write
5174
+                    adrp x1, _write@GOTPAGE
5175
+                    ldr x1, [x1, _write@GOTPAGEOFF]
28585176
                     ret
2859
-                .space 0x1f4
2860
-                _target:
2861
-                    .hword 0x3344
28625177
                 .subsections_via_symbols
28635178
             "#,
2864
-            check: ParityCheck::PageRef {
2865
-                section: TEXT,
2866
-                site_offset: 0,
2867
-                target_offset: 0x200,
2868
-                kind: PageRefKind::Load,
2869
-            },
2870
-        },
2871
-        ParityCase {
2872
-            name: "adrp-ldrb-intra-text",
2873
-            src: r#"
2874
-                .section __TEXT,__text,regular,pure_instructions
2875
-                .globl _main
2876
-                _main:
2877
-                    adrp x0, _target@PAGE
2878
-                    ldrb w1, [x0, _target@PAGEOFF]
2879
-                    ret
2880
-                .space 0xf4
2881
-                _target:
2882
-                    .byte 0x44
2883
-                .subsections_via_symbols
5179
+        },
5180
+    ];
5181
+
5182
+    let mut failures = Vec::new();
5183
+    for case in &cases {
5184
+        if let Err(err) = assert_classic_lazy_case_matches_apple_ld(case, &sdk, &sdk_ver) {
5185
+            failures.push(err);
5186
+        }
5187
+    }
5188
+
5189
+    assert!(
5190
+        failures.is_empty(),
5191
+        "Apple ld classic-lazy parity failures ({} cases):\n{}",
5192
+        failures.len(),
5193
+        failures.join("\n\n")
5194
+    );
5195
+}
5196
+
5197
+#[test]
5198
+fn linker_run_binds_direct_dylib_import_pointers() {
5199
+    if !have_xcrun() || !have_tool("codesign") {
5200
+        eprintln!("skipping: xcrun clang or codesign unavailable");
5201
+        return;
5202
+    }
5203
+    let Some(sdk) = sdk_path() else {
5204
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
5205
+        return;
5206
+    };
5207
+    let Some(sdk_ver) = sdk_version() else {
5208
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
5209
+        return;
5210
+    };
5211
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5212
+    if !tbd.exists() {
5213
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5214
+        return;
5215
+    }
5216
+
5217
+    let dylib_src = r#"
5218
+        int ext_data = 5;
5219
+    "#;
5220
+    let direct_case = DirectBindParityCase {
5221
+        name: "direct-data",
5222
+        dylib_src,
5223
+        main_src: r#"
5224
+            extern int ext_data;
5225
+            int *p = &ext_data;
5226
+            int main(void) { return *p == 5 ? 0 : 1; }
5227
+        "#,
5228
+    };
5229
+    if let Err(e) = assert_direct_bind_case_matches_apple_ld(&direct_case, &sdk, &sdk_ver) {
5230
+        panic!("{e}");
5231
+    }
5232
+
5233
+    let dylib = scratch("direct-data.dylib");
5234
+    let obj = scratch("direct-data.o");
5235
+    let our_out = scratch("direct-data-ours.out");
5236
+
5237
+    if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
5238
+        eprintln!("skipping: dylib compile failed: {e}");
5239
+        return;
5240
+    }
5241
+
5242
+    let main_src = r#"
5243
+        extern int ext_data;
5244
+        int *p = &ext_data;
5245
+        int main(void) { return *p == 5 ? 0 : 1; }
5246
+    "#;
5247
+    if let Err(e) = compile_c(main_src, &obj) {
5248
+        eprintln!("skipping: compile failed: {e}");
5249
+        return;
5250
+    }
5251
+
5252
+    let opts = LinkOptions {
5253
+        inputs: vec![obj.clone(), tbd.clone(), dylib.clone()],
5254
+        output: Some(our_out.clone()),
5255
+        kind: OutputKind::Executable,
5256
+        ..LinkOptions::default()
5257
+    };
5258
+    Linker::run(&opts).unwrap();
5259
+    let our_bytes = fs::read(&our_out).unwrap();
5260
+    let binds = decode_bind_records(&our_bytes, false).unwrap();
5261
+    assert!(
5262
+        binds.iter().any(|record| {
5263
+            record.segment == "__DATA"
5264
+                && record.section == "__data"
5265
+                && record.section_offset == 0
5266
+                && record.symbol == "_ext_data"
5267
+        }),
5268
+        "missing direct bind for imported data: {binds:#?}"
5269
+    );
5270
+    let verify = Command::new("codesign")
5271
+        .arg("-v")
5272
+        .arg(&our_out)
5273
+        .output()
5274
+        .unwrap();
5275
+    assert!(
5276
+        verify.status.success(),
5277
+        "codesign verify failed: {}",
5278
+        String::from_utf8_lossy(&verify.stderr)
5279
+    );
5280
+    let status = Command::new(&our_out).status().unwrap();
5281
+    assert_eq!(
5282
+        status.code(),
5283
+        Some(0),
5284
+        "expected direct-import pointer executable to exit 0"
5285
+    );
5286
+
5287
+    let _ = fs::remove_file(dylib);
5288
+    let _ = fs::remove_file(obj);
5289
+    let _ = fs::remove_file(our_out);
5290
+}
5291
+
5292
+#[test]
5293
+fn direct_bind_surfaces_match_apple_ld_across_fixture_matrix() {
5294
+    if !have_xcrun() || !have_tool("codesign") {
5295
+        eprintln!("skipping: xcrun clang or codesign unavailable");
5296
+        return;
5297
+    }
5298
+    let Some(sdk) = sdk_path() else {
5299
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
5300
+        return;
5301
+    };
5302
+    let Some(sdk_ver) = sdk_version() else {
5303
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
5304
+        return;
5305
+    };
5306
+
5307
+    let cases = [
5308
+        DirectBindParityCase {
5309
+            name: "direct-multi-data",
5310
+            dylib_src: r#"
5311
+                int ext_data = 5;
5312
+                int more_data = 9;
5313
+            "#,
5314
+            main_src: r#"
5315
+                extern int ext_data;
5316
+                extern int more_data;
5317
+                int *p = &ext_data;
5318
+                int *q = &more_data;
5319
+                int main(void) { return (*p == 5 && *q == 9) ? 0 : 1; }
28845320
             "#,
2885
-            check: ParityCheck::PageRef {
2886
-                section: TEXT,
2887
-                site_offset: 0,
2888
-                target_offset: 0x100,
2889
-                kind: PageRefKind::Load,
2890
-            },
28915321
         },
2892
-        ParityCase {
2893
-            name: "mixed-branch-adrp-text",
2894
-            src: r#"
2895
-                .section __TEXT,__text,regular,pure_instructions
2896
-                .globl _main
2897
-                .globl _helper
2898
-                _main:
2899
-                    adrp x0, _target@PAGE
2900
-                    add x0, x0, _target@PAGEOFF
2901
-                    bl _helper
2902
-                    ret
2903
-                _helper:
2904
-                    ret
2905
-                .space 0xff0
2906
-                _target:
2907
-                    .quad 0x99
2908
-                .subsections_via_symbols
5322
+        DirectBindParityCase {
5323
+            name: "direct-and-call-mixed",
5324
+            dylib_src: r#"
5325
+                int ext_data = 5;
5326
+                int ext_fn(void) { return ext_data + 1; }
29095327
             "#,
2910
-            check: ParityCheck::PageRef {
2911
-                section: TEXT,
2912
-                site_offset: 0,
2913
-                target_offset: 0x1004,
2914
-                kind: PageRefKind::Add,
2915
-            },
2916
-        },
2917
-        ParityCase {
2918
-            name: "subtractor-positive",
2919
-            src: r#"
2920
-                .section __TEXT,__text,regular,pure_instructions
2921
-                .globl _helper
2922
-                _helper:
2923
-                    ret
2924
-                .globl _main
2925
-                _main:
2926
-                    bl _helper
2927
-                    ret
2928
-                .section __TEXT,__const
2929
-                .p2align 3
2930
-                _delta:
2931
-                    .quad _helper - _main
2932
-                .subsections_via_symbols
5328
+            main_src: r#"
5329
+                extern int ext_data;
5330
+                extern int ext_fn(void);
5331
+                int *p = &ext_data;
5332
+                int main(void) { return *p + ext_fn() == 11 ? 0 : 1; }
29335333
             "#,
2934
-            check: ParityCheck::ExactSections(&[CONST]),
29355334
         },
2936
-        ParityCase {
2937
-            name: "subtractor-negative",
2938
-            src: r#"
2939
-                .section __TEXT,__text,regular,pure_instructions
2940
-                .globl _helper
2941
-                _helper:
2942
-                    ret
2943
-                .globl _main
2944
-                _main:
2945
-                    ret
2946
-                .section __TEXT,__const
2947
-                .p2align 3
2948
-                _delta:
2949
-                    .quad _main - _helper
2950
-                .subsections_via_symbols
5335
+        DirectBindParityCase {
5336
+            name: "direct-deduped",
5337
+            dylib_src: r#"
5338
+                int ext_data = 5;
29515339
             "#,
2952
-            check: ParityCheck::ExactSections(&[CONST]),
2953
-        },
2954
-        ParityCase {
2955
-            name: "branch-and-subtractor",
2956
-            src: r#"
2957
-                .section __TEXT,__text,regular,pure_instructions
2958
-                .globl _helper
2959
-                _helper:
2960
-                    ret
2961
-                .globl _main
2962
-                _main:
2963
-                    bl _helper
2964
-                    ret
2965
-                .section __TEXT,__const
2966
-                .p2align 3
2967
-                _delta:
2968
-                    .quad _main - _helper
2969
-                .subsections_via_symbols
5340
+            main_src: r#"
5341
+                extern int ext_data;
5342
+                int *p = &ext_data;
5343
+                int *q = &ext_data;
5344
+                int main(void) { return (*p == 5 && *q == 5) ? 0 : 1; }
29705345
             "#,
2971
-            check: ParityCheck::ExactSections(&[TEXT, CONST]),
29725346
         },
29735347
     ];
29745348
 
2975
-    let mut failures = Vec::new();
2976
-    for case in &cases {
2977
-        if let Err(err) = assert_case_matches_apple_ld(case, &sdk, &sdk_ver) {
2978
-            failures.push(err);
2979
-        }
5349
+    let mut failures = Vec::new();
5350
+    for case in &cases {
5351
+        if let Err(err) = assert_direct_bind_case_matches_apple_ld(case, &sdk, &sdk_ver) {
5352
+            failures.push(err);
5353
+        }
5354
+    }
5355
+
5356
+    assert!(
5357
+        failures.is_empty(),
5358
+        "Apple ld direct-bind parity failures ({} cases):\n{}",
5359
+        failures.len(),
5360
+        failures.join("\n\n")
5361
+    );
5362
+}
5363
+
5364
+#[test]
5365
+fn linker_run_rebases_local_absolute_pointers_like_ld() {
5366
+    if !have_xcrun() {
5367
+        eprintln!("skipping: xcrun unavailable");
5368
+        return;
5369
+    }
5370
+    let Some(sdk) = sdk_path() else {
5371
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
5372
+        return;
5373
+    };
5374
+    let Some(sdk_ver) = sdk_version() else {
5375
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
5376
+        return;
5377
+    };
5378
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5379
+    if !tbd.exists() {
5380
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5381
+        return;
5382
+    }
5383
+
5384
+    let obj = scratch("local-rebase.o");
5385
+    let our_out = scratch("local-rebase-ours.out");
5386
+    let apple_out = scratch("local-rebase-apple.out");
5387
+    let src = r#"
5388
+        int ext = 7;
5389
+        int *p = &ext;
5390
+        int main(void) { return *p == 7 ? 0 : 1; }
5391
+    "#;
5392
+    if let Err(e) = compile_c(src, &obj) {
5393
+        eprintln!("skipping: clang compile failed: {e}");
5394
+        return;
5395
+    }
5396
+
5397
+    let opts = LinkOptions {
5398
+        inputs: vec![obj.clone(), tbd],
5399
+        output: Some(our_out.clone()),
5400
+        kind: OutputKind::Executable,
5401
+        ..LinkOptions::default()
5402
+    };
5403
+    Linker::run(&opts).unwrap();
5404
+    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5405
+
5406
+    let our_bytes = fs::read(&our_out).unwrap();
5407
+    let apple_bytes = fs::read(&apple_out).unwrap();
5408
+    assert!(!dyld_info_stream(&our_bytes, DyldInfoStreamKind::Rebase)
5409
+        .unwrap()
5410
+        .is_empty());
5411
+    assert_eq!(
5412
+        dyld_info_stream(&our_bytes, DyldInfoStreamKind::Rebase).unwrap(),
5413
+        dyld_info_stream(&apple_bytes, DyldInfoStreamKind::Rebase).unwrap()
5414
+    );
5415
+    assert_eq!(
5416
+        decode_rebase_records(&our_bytes).unwrap(),
5417
+        decode_rebase_records(&apple_bytes).unwrap()
5418
+    );
5419
+
5420
+    let our_status = Command::new(&our_out).status().unwrap();
5421
+    let apple_status = Command::new(&apple_out).status().unwrap();
5422
+    assert_eq!(our_status.code(), Some(0));
5423
+    assert_eq!(apple_status.code(), Some(0));
5424
+
5425
+    let _ = fs::remove_file(obj);
5426
+    let _ = fs::remove_file(our_out);
5427
+    let _ = fs::remove_file(apple_out);
5428
+}
5429
+
5430
+#[test]
5431
+fn linker_run_routes_local_got_loads_through_rebased_slots() {
5432
+    if !have_xcrun() || !have_tool("codesign") {
5433
+        eprintln!("skipping: xcrun or codesign unavailable");
5434
+        return;
5435
+    }
5436
+    let Some(sdk) = sdk_path() else {
5437
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
5438
+        return;
5439
+    };
5440
+    let Some(sdk_ver) = sdk_version() else {
5441
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
5442
+        return;
5443
+    };
5444
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5445
+    if !tbd.exists() {
5446
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5447
+        return;
5448
+    }
5449
+
5450
+    let obj = scratch("local-got.o");
5451
+    let our_out = scratch("local-got-ours.out");
5452
+    let apple_out = scratch("local-got-apple.out");
5453
+    let src = r#"
5454
+        .section __TEXT,__text,regular,pure_instructions
5455
+        .globl _main
5456
+        _main:
5457
+            adrp x8, _value@GOTPAGE
5458
+            ldr x8, [x8, _value@GOTPAGEOFF]
5459
+            ldr w0, [x8]
5460
+            ret
5461
+
5462
+        .section __DATA,__data
5463
+        .globl _value
5464
+        .p2align 2
5465
+        _value:
5466
+            .long 7
5467
+        .subsections_via_symbols
5468
+    "#;
5469
+    if let Err(e) = assemble(src, &obj) {
5470
+        eprintln!("skipping: assemble failed: {e}");
5471
+        return;
5472
+    }
5473
+
5474
+    let opts = LinkOptions {
5475
+        inputs: vec![obj.clone(), tbd.clone()],
5476
+        output: Some(our_out.clone()),
5477
+        kind: OutputKind::Executable,
5478
+        ..LinkOptions::default()
5479
+    };
5480
+    Linker::run(&opts).unwrap();
5481
+    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5482
+
5483
+    let our_bytes = fs::read(&our_out).unwrap();
5484
+    let apple_bytes = fs::read(&apple_out).unwrap();
5485
+    let our_binds = decode_bind_records(&our_bytes, false).unwrap();
5486
+    let apple_binds = decode_bind_records(&apple_bytes, false).unwrap();
5487
+    assert_eq!(our_binds, apple_binds);
5488
+    assert!(
5489
+        our_binds.iter().all(|record| record.symbol != "_value"),
5490
+        "local GOT target should not be emitted as a dylib bind: {our_binds:#?}"
5491
+    );
5492
+    assert_eq!(
5493
+        output_section(&our_bytes, "__DATA_CONST", "__got")
5494
+            .expect("missing __got section")
5495
+            .1
5496
+            .len(),
5497
+        8
5498
+    );
5499
+    let verify = Command::new("codesign")
5500
+        .arg("-v")
5501
+        .arg(&our_out)
5502
+        .output()
5503
+        .unwrap();
5504
+    assert!(
5505
+        verify.status.success(),
5506
+        "codesign verify failed: {}",
5507
+        String::from_utf8_lossy(&verify.stderr)
5508
+    );
5509
+    let status = Command::new(&our_out).status().unwrap();
5510
+    assert_eq!(
5511
+        status.code(),
5512
+        Some(7),
5513
+        "expected local GOT executable to exit 7"
5514
+    );
5515
+
5516
+    let _ = fs::remove_file(obj);
5517
+    let _ = fs::remove_file(our_out);
5518
+    let _ = fs::remove_file(apple_out);
5519
+}
5520
+
5521
+#[test]
5522
+fn linker_run_dead_strip_prunes_synthetic_import_sections() {
5523
+    if !have_xcrun() || !have_tool("codesign") {
5524
+        eprintln!("skipping: xcrun or codesign unavailable");
5525
+        return;
5526
+    }
5527
+    let Some(sdk) = sdk_path() else {
5528
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
5529
+        return;
5530
+    };
5531
+    let Some(sdk_ver) = sdk_version() else {
5532
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
5533
+        return;
5534
+    };
5535
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5536
+    if !tbd.exists() {
5537
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5538
+        return;
5539
+    }
5540
+
5541
+    let obj = scratch("dead-strip-import.o");
5542
+    let our_out = scratch("dead-strip-import-ours.out");
5543
+    let apple_out = scratch("dead-strip-import-apple.out");
5544
+    let src = r#"
5545
+        .section __TEXT,__text,regular,pure_instructions
5546
+        .globl _main
5547
+        _main:
5548
+            mov w0, #0
5549
+            ret
5550
+
5551
+        .globl _unused
5552
+        _unused:
5553
+            bl _puts
5554
+            mov w0, #0
5555
+            ret
5556
+        .subsections_via_symbols
5557
+    "#;
5558
+    if let Err(e) = assemble(src, &obj) {
5559
+        eprintln!("skipping: assemble failed: {e}");
5560
+        return;
5561
+    }
5562
+
5563
+    let opts = LinkOptions {
5564
+        inputs: vec![obj.clone(), tbd.clone()],
5565
+        output: Some(our_out.clone()),
5566
+        dead_strip: true,
5567
+        kind: OutputKind::Executable,
5568
+        ..LinkOptions::default()
5569
+    };
5570
+    Linker::run(&opts).unwrap();
5571
+    apple_link_with_args(
5572
+        &obj,
5573
+        &apple_out,
5574
+        "_main",
5575
+        &sdk,
5576
+        &sdk_ver,
5577
+        &["-dead_strip", "-no_fixup_chains"],
5578
+    )
5579
+    .unwrap();
5580
+
5581
+    let our_bytes = fs::read(&our_out).unwrap();
5582
+    let apple_bytes = fs::read(&apple_out).unwrap();
5583
+    for (segname, sectname) in [
5584
+        ("__TEXT", "__stubs"),
5585
+        ("__TEXT", "__stub_helper"),
5586
+        ("__DATA", "__la_symbol_ptr"),
5587
+        ("__DATA_CONST", "__got"),
5588
+    ] {
5589
+        assert!(
5590
+            output_section(&our_bytes, segname, sectname).is_none(),
5591
+            "unexpected synthetic section {segname},{sectname} in our output"
5592
+        );
5593
+        assert!(
5594
+            output_section(&apple_bytes, segname, sectname).is_none(),
5595
+            "unexpected synthetic section {segname},{sectname} in apple output"
5596
+        );
5597
+    }
5598
+    assert!(decode_bind_records(&our_bytes, false).unwrap().is_empty());
5599
+    assert_eq!(
5600
+        decode_bind_records(&our_bytes, false).unwrap(),
5601
+        decode_bind_records(&apple_bytes, false).unwrap()
5602
+    );
5603
+
5604
+    let verify = Command::new("codesign")
5605
+        .arg("-v")
5606
+        .arg(&our_out)
5607
+        .output()
5608
+        .unwrap();
5609
+    assert!(
5610
+        verify.status.success(),
5611
+        "codesign verify failed: {}",
5612
+        String::from_utf8_lossy(&verify.stderr)
5613
+    );
5614
+    let status = Command::new(&our_out).status().unwrap();
5615
+    assert_eq!(status.code(), Some(0));
5616
+
5617
+    let _ = fs::remove_file(obj);
5618
+    let _ = fs::remove_file(our_out);
5619
+    let _ = fs::remove_file(apple_out);
5620
+}
5621
+
5622
+#[test]
5623
+fn linker_run_relaxes_hidden_got_loads_like_apple_ld() {
5624
+    if !have_xcrun() || !have_tool("codesign") {
5625
+        eprintln!("skipping: xcrun or codesign unavailable");
5626
+        return;
5627
+    }
5628
+    let Some(sdk) = sdk_path() else {
5629
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
5630
+        return;
5631
+    };
5632
+    let Some(sdk_ver) = sdk_version() else {
5633
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
5634
+        return;
5635
+    };
5636
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5637
+    if !tbd.exists() {
5638
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5639
+        return;
5640
+    }
5641
+
5642
+    let obj = scratch("hidden-got.o");
5643
+    let our_out = scratch("hidden-got-ours.out");
5644
+    let apple_out = scratch("hidden-got-apple.out");
5645
+    let src = r#"
5646
+        .section __TEXT,__text,regular,pure_instructions
5647
+        .globl _main
5648
+        _main:
5649
+            adrp x8, _value@GOTPAGE
5650
+            ldr x8, [x8, _value@GOTPAGEOFF]
5651
+            ldr w0, [x8]
5652
+            ret
5653
+
5654
+        .private_extern _value
5655
+        .section __DATA,__data
5656
+        .p2align 2
5657
+        _value:
5658
+            .long 7
5659
+        .subsections_via_symbols
5660
+    "#;
5661
+    if let Err(e) = assemble(src, &obj) {
5662
+        eprintln!("skipping: assemble failed: {e}");
5663
+        return;
5664
+    }
5665
+
5666
+    let opts = LinkOptions {
5667
+        inputs: vec![obj.clone(), tbd.clone()],
5668
+        output: Some(our_out.clone()),
5669
+        kind: OutputKind::Executable,
5670
+        ..LinkOptions::default()
5671
+    };
5672
+    Linker::run(&opts).unwrap();
5673
+    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5674
+
5675
+    let our_bytes = fs::read(&our_out).unwrap();
5676
+    let apple_bytes = fs::read(&apple_out).unwrap();
5677
+    let (our_text_addr, our_text) = output_section(&our_bytes, "__TEXT", "__text").unwrap();
5678
+    let (apple_text_addr, apple_text) = output_section(&apple_bytes, "__TEXT", "__text").unwrap();
5679
+    assert_eq!(
5680
+        decode_page_reference(&our_text, our_text_addr, 0, &PageRefKind::Add).unwrap(),
5681
+        decode_page_reference(&apple_text, apple_text_addr, 0, &PageRefKind::Add).unwrap()
5682
+    );
5683
+    assert_eq!(our_text, apple_text);
5684
+    assert!(output_section(&our_bytes, "__DATA_CONST", "__got").is_none());
5685
+    assert!(output_section(&apple_bytes, "__DATA_CONST", "__got").is_none());
5686
+
5687
+    let verify = Command::new("codesign")
5688
+        .arg("-v")
5689
+        .arg(&our_out)
5690
+        .output()
5691
+        .unwrap();
5692
+    assert!(
5693
+        verify.status.success(),
5694
+        "codesign verify failed: {}",
5695
+        String::from_utf8_lossy(&verify.stderr)
5696
+    );
5697
+    let status = Command::new(&our_out).status().unwrap();
5698
+    assert_eq!(
5699
+        status.code(),
5700
+        Some(7),
5701
+        "expected hidden GOT executable to exit 7"
5702
+    );
5703
+
5704
+    let _ = fs::remove_file(obj);
5705
+    let _ = fs::remove_file(our_out);
5706
+    let _ = fs::remove_file(apple_out);
5707
+}
5708
+
5709
+#[test]
5710
+fn linker_run_partitions_symtab_like_ld() {
5711
+    if !have_xcrun() {
5712
+        eprintln!("skipping: xcrun unavailable");
5713
+        return;
5714
+    }
5715
+
5716
+    let dylib = scratch("symtab-partition.dylib");
5717
+    let obj = scratch("symtab-partition.o");
5718
+    let our_out = scratch("symtab-partition-ours.out");
5719
+    let apple_out = scratch("symtab-partition-apple.out");
5720
+
5721
+    let dylib_src = r#"
5722
+        int ext_data = 5;
5723
+    "#;
5724
+    if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
5725
+        eprintln!("skipping: dylib compile failed: {e}");
5726
+        return;
5727
+    }
5728
+
5729
+    let asm = r#"
5730
+        .text
5731
+        .private_extern _hidden
5732
+        .globl _visible
5733
+        .globl _main
5734
+        .p2align 2
5735
+    _local:
5736
+        ret
5737
+    _hidden:
5738
+        ret
5739
+    _visible:
5740
+        ret
5741
+    _main:
5742
+        ret
5743
+
5744
+        .data
5745
+        .quad _ext_data
5746
+        .subsections_via_symbols
5747
+    "#;
5748
+    if let Err(e) = assemble(asm, &obj) {
5749
+        eprintln!("skipping: assemble failed: {e}");
5750
+        return;
5751
+    }
5752
+
5753
+    let opts = LinkOptions {
5754
+        inputs: vec![obj.clone(), dylib.clone()],
5755
+        output: Some(our_out.clone()),
5756
+        kind: OutputKind::Executable,
5757
+        ..LinkOptions::default()
5758
+    };
5759
+    Linker::run(&opts).unwrap();
5760
+
5761
+    let apple = Command::new("xcrun")
5762
+        .args(["ld", "-arch", "arm64", "-e", "_main", "-o"])
5763
+        .arg(&apple_out)
5764
+        .arg(&obj)
5765
+        .arg(&dylib)
5766
+        .output()
5767
+        .unwrap();
5768
+    assert!(
5769
+        apple.status.success(),
5770
+        "xcrun ld failed: {}",
5771
+        String::from_utf8_lossy(&apple.stderr)
5772
+    );
5773
+
5774
+    let our_bytes = fs::read(&our_out).unwrap();
5775
+    let apple_bytes = fs::read(&apple_out).unwrap();
5776
+    let (our_symtab, our_dysymtab) = symtab_and_dysymtab(&our_bytes);
5777
+    let (apple_symtab, apple_dysymtab) = symtab_and_dysymtab(&apple_bytes);
5778
+
5779
+    assert_eq!(our_symtab.nsyms, apple_symtab.nsyms);
5780
+    assert_eq!(our_dysymtab.ilocalsym, apple_dysymtab.ilocalsym);
5781
+    assert_eq!(our_dysymtab.nlocalsym, apple_dysymtab.nlocalsym);
5782
+    assert_eq!(our_dysymtab.iextdefsym, apple_dysymtab.iextdefsym);
5783
+    assert_eq!(our_dysymtab.nextdefsym, apple_dysymtab.nextdefsym);
5784
+    assert_eq!(our_dysymtab.iundefsym, apple_dysymtab.iundefsym);
5785
+    assert_eq!(our_dysymtab.nundefsym, apple_dysymtab.nundefsym);
5786
+    assert_eq!(
5787
+        canonical_symbol_records(&our_bytes),
5788
+        canonical_symbol_records(&apple_bytes)
5789
+    );
5790
+    assert_strtab_within_five_percent(
5791
+        &raw_string_table(&our_bytes),
5792
+        &raw_string_table(&apple_bytes),
5793
+    );
5794
+
5795
+    assert_eq!(
5796
+        symbol_partition_names(&our_bytes),
5797
+        symbol_partition_names(&apple_bytes)
5798
+    );
5799
+
5800
+    let _ = fs::remove_file(dylib);
5801
+    let _ = fs::remove_file(obj);
5802
+    let _ = fs::remove_file(our_out);
5803
+    let _ = fs::remove_file(apple_out);
5804
+}
5805
+
5806
+#[test]
5807
+fn linker_run_strips_locals_with_x_like_ld() {
5808
+    if !have_xcrun() {
5809
+        eprintln!("skipping: xcrun unavailable");
5810
+        return;
5811
+    }
5812
+
5813
+    let dylib = scratch("symtab-strip.dylib");
5814
+    let obj = scratch("symtab-strip.o");
5815
+    let our_out = scratch("symtab-strip-ours.out");
5816
+    let apple_out = scratch("symtab-strip-apple.out");
5817
+
5818
+    let dylib_src = r#"
5819
+        int ext_data = 5;
5820
+    "#;
5821
+    if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
5822
+        eprintln!("skipping: dylib compile failed: {e}");
5823
+        return;
5824
+    }
5825
+
5826
+    let asm = r#"
5827
+        .text
5828
+        .private_extern _hidden
5829
+        .globl _visible
5830
+        .globl _main
5831
+        .p2align 2
5832
+    _local:
5833
+        ret
5834
+    _hidden:
5835
+        ret
5836
+    _visible:
5837
+        ret
5838
+    _main:
5839
+        ret
5840
+
5841
+        .data
5842
+        .quad _ext_data
5843
+        .subsections_via_symbols
5844
+    "#;
5845
+    if let Err(e) = assemble(asm, &obj) {
5846
+        eprintln!("skipping: assemble failed: {e}");
5847
+        return;
29805848
     }
29815849
 
5850
+    let opts = LinkOptions {
5851
+        inputs: vec![obj.clone(), dylib.clone()],
5852
+        output: Some(our_out.clone()),
5853
+        kind: OutputKind::Executable,
5854
+        strip_locals: true,
5855
+        ..LinkOptions::default()
5856
+    };
5857
+    Linker::run(&opts).unwrap();
5858
+
5859
+    let apple = Command::new("xcrun")
5860
+        .args(["ld", "-arch", "arm64", "-x", "-e", "_main", "-o"])
5861
+        .arg(&apple_out)
5862
+        .arg(&obj)
5863
+        .arg(&dylib)
5864
+        .output()
5865
+        .unwrap();
29825866
     assert!(
2983
-        failures.is_empty(),
2984
-        "Apple ld parity failures ({} cases):\n{}",
2985
-        failures.len(),
2986
-        failures.join("\n\n")
5867
+        apple.status.success(),
5868
+        "xcrun ld failed: {}",
5869
+        String::from_utf8_lossy(&apple.stderr)
5870
+    );
5871
+
5872
+    let our_bytes = fs::read(&our_out).unwrap();
5873
+    let apple_bytes = fs::read(&apple_out).unwrap();
5874
+    let (our_symtab, our_dysymtab) = symtab_and_dysymtab(&our_bytes);
5875
+    let (apple_symtab, apple_dysymtab) = symtab_and_dysymtab(&apple_bytes);
5876
+
5877
+    assert_eq!(our_symtab.nsyms, apple_symtab.nsyms);
5878
+    assert_eq!(our_dysymtab.ilocalsym, apple_dysymtab.ilocalsym);
5879
+    assert_eq!(our_dysymtab.nlocalsym, apple_dysymtab.nlocalsym);
5880
+    assert_eq!(our_dysymtab.iextdefsym, apple_dysymtab.iextdefsym);
5881
+    assert_eq!(our_dysymtab.nextdefsym, apple_dysymtab.nextdefsym);
5882
+    assert_eq!(our_dysymtab.iundefsym, apple_dysymtab.iundefsym);
5883
+    assert_eq!(our_dysymtab.nundefsym, apple_dysymtab.nundefsym);
5884
+    assert_eq!(
5885
+        canonical_symbol_records(&our_bytes),
5886
+        canonical_symbol_records(&apple_bytes)
5887
+    );
5888
+
5889
+    let (locals, extdefs, undefs) = symbol_partition_names(&our_bytes);
5890
+    assert!(locals.is_empty());
5891
+    assert_eq!(
5892
+        extdefs,
5893
+        vec![
5894
+            "__mh_execute_header".to_string(),
5895
+            "_main".to_string(),
5896
+            "_visible".to_string()
5897
+        ]
5898
+    );
5899
+    assert_eq!(undefs, vec!["_ext_data".to_string()]);
5900
+
5901
+    let _ = fs::remove_file(dylib);
5902
+    let _ = fs::remove_file(obj);
5903
+    let _ = fs::remove_file(our_out);
5904
+    let _ = fs::remove_file(apple_out);
5905
+}
5906
+
5907
+#[test]
5908
+fn linker_run_emits_leaf_unwind_info_like_ld() {
5909
+    if !have_xcrun() {
5910
+        eprintln!("skipping: xcrun unavailable");
5911
+        return;
5912
+    }
5913
+    let Some(sdk) = sdk_path() else {
5914
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
5915
+        return;
5916
+    };
5917
+    let Some(sdk_ver) = sdk_version() else {
5918
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
5919
+        return;
5920
+    };
5921
+
5922
+    let obj = scratch("unwind-leaf.o");
5923
+    let our_out = scratch("unwind-leaf-ours.out");
5924
+    let apple_out = scratch("unwind-leaf-apple.out");
5925
+    let src = r#"
5926
+        int main(void) {
5927
+            return 0;
5928
+        }
5929
+    "#;
5930
+    if let Err(e) = compile_c(src, &obj) {
5931
+        eprintln!("skipping: clang compile failed: {e}");
5932
+        return;
5933
+    }
5934
+
5935
+    let opts = LinkOptions {
5936
+        inputs: vec![obj.clone()],
5937
+        output: Some(our_out.clone()),
5938
+        kind: OutputKind::Executable,
5939
+        ..LinkOptions::default()
5940
+    };
5941
+    Linker::run(&opts).unwrap();
5942
+    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5943
+
5944
+    let our_bytes = fs::read(&our_out).unwrap();
5945
+    let apple_bytes = fs::read(&apple_out).unwrap();
5946
+    assert_eq!(
5947
+        rebased_unwind_bytes(&our_bytes),
5948
+        rebased_unwind_bytes(&apple_bytes)
5949
+    );
5950
+    assert!(output_section(&our_bytes, "__LD", "__compact_unwind").is_none());
5951
+
5952
+    let _ = fs::remove_file(obj);
5953
+    let _ = fs::remove_file(our_out);
5954
+    let _ = fs::remove_file(apple_out);
5955
+}
5956
+
5957
+#[test]
5958
+fn linker_run_emits_multi_function_unwind_info_like_ld() {
5959
+    if !have_xcrun() {
5960
+        eprintln!("skipping: xcrun unavailable");
5961
+        return;
5962
+    }
5963
+    let Some(sdk) = sdk_path() else {
5964
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
5965
+        return;
5966
+    };
5967
+    let Some(sdk_ver) = sdk_version() else {
5968
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
5969
+        return;
5970
+    };
5971
+
5972
+    let obj = scratch("unwind-mixed.o");
5973
+    let our_out = scratch("unwind-mixed-ours.out");
5974
+    let apple_out = scratch("unwind-mixed-apple.out");
5975
+    let src = r#"
5976
+        int helper(void) {
5977
+            return 1;
5978
+        }
5979
+
5980
+        int main(void) {
5981
+            return helper();
5982
+        }
5983
+    "#;
5984
+    if let Err(e) = compile_c(src, &obj) {
5985
+        eprintln!("skipping: clang compile failed: {e}");
5986
+        return;
5987
+    }
5988
+
5989
+    let opts = LinkOptions {
5990
+        inputs: vec![obj.clone()],
5991
+        output: Some(our_out.clone()),
5992
+        kind: OutputKind::Executable,
5993
+        ..LinkOptions::default()
5994
+    };
5995
+    Linker::run(&opts).unwrap();
5996
+    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5997
+
5998
+    let our_bytes = fs::read(&our_out).unwrap();
5999
+    let apple_bytes = fs::read(&apple_out).unwrap();
6000
+    assert_eq!(
6001
+        rebased_unwind_bytes(&our_bytes),
6002
+        rebased_unwind_bytes(&apple_bytes)
29876003
     );
6004
+    assert!(output_section(&our_bytes, "__LD", "__compact_unwind").is_none());
6005
+
6006
+    let _ = fs::remove_file(obj);
6007
+    let _ = fs::remove_file(our_out);
6008
+    let _ = fs::remove_file(apple_out);
29886009
 }
29896010
 
29906011
 #[test]
2991
-fn linker_run_rejects_out_of_range_branch26() {
6012
+fn linker_run_dead_strip_prunes_unused_unwind_records_like_ld() {
29926013
     if !have_xcrun() {
2993
-        eprintln!("skipping: xcrun as unavailable");
6014
+        eprintln!("skipping: xcrun unavailable");
29946015
         return;
29956016
     }
6017
+    let Some(sdk) = sdk_path() else {
6018
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
6019
+        return;
6020
+    };
6021
+    let Some(sdk_ver) = sdk_version() else {
6022
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
6023
+        return;
6024
+    };
29966025
 
2997
-    let obj = scratch("branch26-range.o");
2998
-    let out = scratch("branch26-range.out");
6026
+    let obj = scratch("unwind-dead-strip.o");
6027
+    let our_out = scratch("unwind-dead-strip-ours.out");
6028
+    let apple_out = scratch("unwind-dead-strip-apple.out");
29996029
     let src = r#"
3000
-        .section __TEXT,__text,regular,pure_instructions
3001
-        .globl _main
3002
-        _main:
3003
-            bl _helper
3004
-            ret
6030
+        int helper(void) {
6031
+            return 1;
6032
+        }
30056033
 
3006
-        .zerofill __DATA,__bss,_gap,0x9000000,0
6034
+        int unused(void) {
6035
+            return 2;
6036
+        }
30076037
 
3008
-        .section __FAR,__text,regular,pure_instructions
3009
-        .globl _helper
3010
-        _helper:
3011
-            ret
3012
-        .subsections_via_symbols
6038
+        int main(void) {
6039
+            return helper();
6040
+        }
30136041
     "#;
3014
-    if let Err(e) = assemble(src, &obj) {
3015
-        eprintln!("skipping: assemble failed: {e}");
6042
+    if let Err(e) = compile_c(src, &obj) {
6043
+        eprintln!("skipping: clang compile failed: {e}");
30166044
         return;
30176045
     }
30186046
 
30196047
     let opts = LinkOptions {
30206048
         inputs: vec![obj.clone()],
3021
-        output: Some(out),
6049
+        output: Some(our_out.clone()),
30226050
         kind: OutputKind::Executable,
6051
+        dead_strip: true,
30236052
         ..LinkOptions::default()
30246053
     };
3025
-    let err = Linker::run(&opts).unwrap_err();
3026
-    match err {
3027
-        LinkError::Reloc(err) => {
3028
-            let msg = err.to_string();
3029
-            assert!(msg.contains("Branch26"), "{msg}");
3030
-            assert!(msg.contains("out of BRANCH26 range"), "{msg}");
3031
-            assert!(msg.contains("_helper"), "{msg}");
3032
-        }
3033
-        other => panic!("expected Reloc error, got {other:?}"),
3034
-    }
6054
+    Linker::run(&opts).unwrap();
6055
+    apple_link_with_args(&obj, &apple_out, "_main", &sdk, &sdk_ver, &["-dead_strip"]).unwrap();
6056
+
6057
+    let our_bytes = fs::read(&our_out).unwrap();
6058
+    let apple_bytes = fs::read(&apple_out).unwrap();
6059
+    let (_, our_unwind) = output_section(&our_bytes, "__TEXT", "__unwind_info").unwrap();
6060
+    let (_, apple_unwind) = output_section(&apple_bytes, "__TEXT", "__unwind_info").unwrap();
6061
+    let our_decoded = decode_unwind_info(&our_unwind).unwrap();
6062
+    let apple_decoded = decode_unwind_info(&apple_unwind).unwrap();
6063
+    let normalize = |records: &[afs_ld::synth::unwind::DecodedUnwindRecord]| {
6064
+        let base = records
6065
+            .first()
6066
+            .map(|record| record.function_offset)
6067
+            .unwrap_or(0);
6068
+        records
6069
+            .iter()
6070
+            .map(|record| (record.function_offset - base, record.encoding))
6071
+            .collect::<Vec<_>>()
6072
+    };
6073
+    assert_eq!(
6074
+        normalize(&our_decoded.records),
6075
+        normalize(&apple_decoded.records)
6076
+    );
6077
+    assert_eq!(our_decoded.records.len(), 2);
30356078
 
30366079
     let _ = fs::remove_file(obj);
6080
+    let _ = fs::remove_file(our_out);
6081
+    let _ = fs::remove_file(apple_out);
30376082
 }
30386083
 
30396084
 #[test]
3040
-fn linker_run_routes_dylib_imports_through_synthetic_sections() {
6085
+fn linker_run_handles_large_unwind_function_gaps() {
30416086
     if !have_xcrun() {
30426087
         eprintln!("skipping: xcrun unavailable");
30436088
         return;
30446089
     }
3045
-    let Some(sdk) = sdk_path() else {
3046
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
3047
-        return;
3048
-    };
3049
-    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
3050
-    if !tbd.exists() {
3051
-        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
3052
-        return;
3053
-    }
30546090
 
3055
-    let obj = scratch("import-reloc.o");
3056
-    let out = scratch("import-reloc.out");
3057
-    let src = r#"
3058
-        .section __TEXT,__text,regular,pure_instructions
6091
+    let obj = scratch("unwind-gap.o");
6092
+    let out = scratch("unwind-gap-ours.out");
6093
+    let asm = r#"
6094
+        .text
30596095
         .globl _main
3060
-        _main:
3061
-            adrp x0, _write@GOTPAGE
3062
-            ldr x0, [x0, _write@GOTPAGEOFF]
3063
-            bl _write
3064
-            ret
6096
+        .p2align 2
6097
+    _main:
6098
+        .cfi_startproc
6099
+        bl _helper
6100
+        ret
6101
+        .cfi_endproc
6102
+        .space 0x1000010
6103
+        .globl _helper
6104
+        .p2align 2
6105
+    _helper:
6106
+        .cfi_startproc
6107
+        ret
6108
+        .cfi_endproc
30656109
         .subsections_via_symbols
30666110
     "#;
3067
-    if let Err(e) = assemble(src, &obj) {
6111
+    if let Err(e) = assemble(asm, &obj) {
30686112
         eprintln!("skipping: assemble failed: {e}");
30696113
         return;
30706114
     }
30716115
 
30726116
     let opts = LinkOptions {
3073
-        inputs: vec![obj.clone(), tbd.clone()],
6117
+        inputs: vec![obj.clone()],
30746118
         output: Some(out.clone()),
30756119
         kind: OutputKind::Executable,
30766120
         ..LinkOptions::default()
@@ -3078,136 +6122,24 @@ fn linker_run_routes_dylib_imports_through_synthetic_sections() {
30786122
     Linker::run(&opts).unwrap();
30796123
 
30806124
     let bytes = fs::read(&out).unwrap();
3081
-    let header = parse_header(&bytes).unwrap();
3082
-    let commands = parse_commands(&header, &bytes).unwrap();
3083
-    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
3084
-    let (stubs_addr, stubs) = output_section(&bytes, "__TEXT", "__stubs").unwrap();
3085
-    let (helper_addr, helper) = output_section(&bytes, "__TEXT", "__stub_helper").unwrap();
3086
-    let (got_addr, got) = output_section(&bytes, "__DATA_CONST", "__got").unwrap();
3087
-    let (lazy_addr, lazy) = output_section(&bytes, "__DATA", "__la_symbol_ptr").unwrap();
3088
-    let (dyld_private_addr, _) = output_section(&bytes, "__DATA", "__data").unwrap();
3089
-    let stubs_hdr = output_section_header(&bytes, "__TEXT", "__stubs").unwrap();
3090
-    let got_hdr = output_section_header(&bytes, "__DATA_CONST", "__got").unwrap();
3091
-    let lazy_hdr = output_section_header(&bytes, "__DATA", "__la_symbol_ptr").unwrap();
3092
-
3093
-    let symtab = commands
3094
-        .iter()
3095
-        .find_map(|cmd| match cmd {
3096
-            LoadCommand::Symtab(cmd) => Some(*cmd),
3097
-            _ => None,
3098
-        })
3099
-        .unwrap();
3100
-    let dysymtab = commands
3101
-        .iter()
3102
-        .find_map(|cmd| match cmd {
3103
-            LoadCommand::Dysymtab(cmd) => Some(*cmd),
3104
-            _ => None,
3105
-        })
3106
-        .unwrap();
3107
-    let dyld_info = commands
3108
-        .iter()
3109
-        .find_map(|cmd| match cmd {
3110
-            LoadCommand::DyldInfoOnly(cmd) => Some(*cmd),
3111
-            _ => None,
3112
-        })
3113
-        .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();
3126
-    let symbols = parse_nlist_table(&bytes, symtab.symoff, symtab.nsyms).unwrap();
3127
-    let strings = StringTable::from_file(&bytes, symtab.stroff, symtab.strsize).unwrap();
3128
-    let symbol_names: Vec<&str> = symbols
3129
-        .iter()
3130
-        .map(|symbol| strings.get(symbol.strx()).unwrap())
3131
-        .collect();
3132
-
3133
-    assert_eq!(got.len(), 16);
3134
-    assert_eq!(stubs.len(), 12);
3135
-    assert_eq!(helper.len(), 36);
3136
-    assert_eq!(lazy.len(), 8);
3137
-    assert_eq!(symtab.nsyms, 5);
3138
-    assert_eq!(dysymtab.nlocalsym, 1);
3139
-    assert_eq!(dysymtab.nextdefsym, 2);
3140
-    assert_eq!(dysymtab.nundefsym, 2);
3141
-    assert_eq!(dysymtab.nindirectsyms, 4);
3142
-    assert_eq!(stubs_hdr.reserved1, 0);
3143
-    assert_eq!(got_hdr.reserved1, 1);
3144
-    assert_eq!(lazy_hdr.reserved1, 3);
3145
-    assert_eq!(stubs_hdr.reserved2, 12);
3146
-    assert!(libsystem_load.current_version >= (1 << 16));
3147
-    assert_eq!(libsystem_load.compatibility_version, 1 << 16);
3148
-    assert!(dyld_info.rebase_size > 0);
3149
-    assert!(dyld_info.bind_size > 0);
3150
-    assert!(dyld_info.lazy_bind_size > 0);
3151
-    assert_eq!(
3152
-        decode_page_reference(&text, text_addr, 0, &PageRefKind::Load).unwrap(),
3153
-        got_addr
3154
-    );
3155
-    assert_eq!(
3156
-        decode_branch_target(&text, text_addr, 8).unwrap(),
3157
-        stubs_addr
3158
-    );
3159
-    assert_eq!(
3160
-        decode_page_reference(&stubs, stubs_addr, 0, &PageRefKind::Load).unwrap(),
3161
-        lazy_addr
3162
-    );
3163
-    assert_eq!(read_insn(&stubs, 8).unwrap(), 0xd61f0200);
3164
-    assert_eq!(
3165
-        u64::from_le_bytes(lazy[0..8].try_into().unwrap()),
3166
-        helper_addr + 24
3167
-    );
3168
-    assert_eq!(
3169
-        decode_page_reference(&helper, helper_addr, 0, &PageRefKind::Add).unwrap(),
3170
-        dyld_private_addr
3171
-    );
3172
-    assert_eq!(
3173
-        decode_page_reference(&helper, helper_addr, 12, &PageRefKind::Load).unwrap(),
3174
-        got_addr + 8
3175
-    );
3176
-    assert_eq!(read_insn(&helper, 20).unwrap(), 0xd61f0200);
3177
-    assert_eq!(read_insn(&helper, 24).unwrap(), 0x1800_0050);
3178
-    assert_eq!(
3179
-        decode_branch_target(&helper, helper_addr, 28).unwrap(),
3180
-        helper_addr
3181
-    );
3182
-    assert_eq!(u32::from_le_bytes(helper[32..36].try_into().unwrap()), 0);
3183
-    let (locals, extdefs, undefs) = symbol_partition_names(&bytes);
3184
-    assert_eq!(locals, vec!["__dyld_private".to_string()]);
3185
-    assert_eq!(
3186
-        extdefs,
3187
-        vec!["__mh_execute_header".to_string(), "_main".to_string()]
3188
-    );
3189
-    assert_eq!(
3190
-        undefs,
3191
-        vec!["_write".to_string(), "dyld_stub_binder".to_string()]
6125
+    let (_, unwind) = output_section(&bytes, "__TEXT", "__unwind_info").unwrap();
6126
+    let decoded = decode_unwind_info(&unwind).unwrap();
6127
+    assert!(
6128
+        decoded
6129
+            .records
6130
+            .windows(2)
6131
+            .all(|pair| pair[0].function_offset < pair[1].function_offset),
6132
+        "expected strictly ascending unwind records after large-gap pagination"
31926133
     );
3193
-    assert!(symbol_names.contains(&"__dyld_private"));
3194
-    assert!(symbols[dysymtab.iundefsym as usize..]
3195
-        .iter()
3196
-        .all(|symbol| symbol.kind() == SymKind::Undef));
3197
-    assert!(symbols[dysymtab.iundefsym as usize..]
3198
-        .iter()
3199
-        .all(|symbol| symbol.library_ordinal().unwrap() > 0));
3200
-    assert!(symbol_names.contains(&"_write"));
3201
-    assert!(symbol_names.contains(&"dyld_stub_binder"));
32026134
 
3203
-    let _ = fs::remove_file(out);
32046135
     let _ = fs::remove_file(obj);
6136
+    let _ = fs::remove_file(out);
32056137
 }
32066138
 
32076139
 #[test]
3208
-fn synthetic_import_surfaces_match_apple_ld_classic_lazy_model() {
3209
-    if !have_xcrun() || !have_xcrun_tool("ld") {
3210
-        eprintln!("skipping: xcrun as/ld unavailable");
6140
+fn linker_run_preserves_eh_frame_like_ld() {
6141
+    if !have_xcrun() || !have_xcrun_tool("dwarfdump") {
6142
+        eprintln!("skipping: xcrun dwarfdump unavailable");
32116143
         return;
32126144
     }
32136145
     let Some(sdk) = sdk_path() else {
@@ -3218,115 +6150,82 @@ fn synthetic_import_surfaces_match_apple_ld_classic_lazy_model() {
32186150
         eprintln!("skipping: xcrun --show-sdk-version unavailable");
32196151
         return;
32206152
     };
3221
-    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
3222
-    if !tbd.exists() {
3223
-        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
3224
-        return;
3225
-    }
32266153
 
3227
-    let obj = scratch("import-parity.o");
3228
-    let our_out = scratch("import-parity-ours.out");
3229
-    let apple_out = scratch("import-parity-apple.out");
3230
-    let src = r#"
3231
-        .section __TEXT,__text,regular,pure_instructions
3232
-        .globl _main
3233
-        _main:
3234
-            adrp x0, _write@GOTPAGE
3235
-            ldr x0, [x0, _write@GOTPAGEOFF]
3236
-            bl _write
3237
-            ret
6154
+    let obj = scratch("eh-frame.o");
6155
+    let our_out = scratch("eh-frame-ours.out");
6156
+    let apple_out = scratch("eh-frame-apple.out");
6157
+    let asm = r#"
6158
+        .text
6159
+        .globl _main
6160
+        .p2align 2
6161
+    _main:
6162
+        .cfi_startproc
6163
+        sub sp, sp, #16
6164
+        .cfi_def_cfa_offset 16
6165
+        str x30, [sp, #8]
6166
+        .cfi_offset w30, -8
6167
+        bl _helper
6168
+        ldr x30, [sp, #8]
6169
+        add sp, sp, #16
6170
+        ret
6171
+        .cfi_endproc
6172
+
6173
+        .globl _helper
6174
+        .p2align 2
6175
+    _helper:
6176
+        .cfi_startproc
6177
+        ret
6178
+        .cfi_endproc
32386179
         .subsections_via_symbols
32396180
     "#;
3240
-    if let Err(e) = assemble(src, &obj) {
6181
+    if let Err(e) = assemble(asm, &obj) {
32416182
         eprintln!("skipping: assemble failed: {e}");
32426183
         return;
32436184
     }
32446185
 
32456186
     let opts = LinkOptions {
3246
-        inputs: vec![obj.clone(), tbd],
6187
+        inputs: vec![obj.clone()],
32476188
         output: Some(our_out.clone()),
32486189
         kind: OutputKind::Executable,
32496190
         ..LinkOptions::default()
32506191
     };
32516192
     Linker::run(&opts).unwrap();
3252
-    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
6193
+    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
32536194
 
32546195
     let our_bytes = fs::read(&our_out).unwrap();
32556196
     let apple_bytes = fs::read(&apple_out).unwrap();
3256
-
3257
-    for (segname, sectname) in [("__TEXT", "__stubs"), ("__TEXT", "__stub_helper")] {
3258
-        let (_, ours) = output_section(&our_bytes, segname, sectname).unwrap();
3259
-        let (_, apple) = output_section(&apple_bytes, segname, sectname).unwrap();
3260
-        let diff = diff_macho(&ours, &apple);
3261
-        assert!(
3262
-            diff.is_clean(),
3263
-            "{segname},{sectname} diverged from Apple ld: {:#?}",
3264
-            diff.critical
3265
-        );
3266
-    }
3267
-
3268
-    let (our_helper_addr, _) = output_section(&our_bytes, "__TEXT", "__stub_helper").unwrap();
3269
-    let (apple_helper_addr, _) = output_section(&apple_bytes, "__TEXT", "__stub_helper").unwrap();
3270
-    let (_, our_lazy) = output_section(&our_bytes, "__DATA", "__la_symbol_ptr").unwrap();
3271
-    let (_, apple_lazy) = output_section(&apple_bytes, "__DATA", "__la_symbol_ptr").unwrap();
3272
-    assert_eq!(
3273
-        u64::from_le_bytes(our_lazy[0..8].try_into().unwrap()) - our_helper_addr,
3274
-        24
3275
-    );
3276
-    assert_eq!(
3277
-        u64::from_le_bytes(apple_lazy[0..8].try_into().unwrap()) - apple_helper_addr,
3278
-        24
3279
-    );
3280
-
3281
-    assert_eq!(
3282
-        load_dylib_names(&our_bytes).unwrap(),
3283
-        load_dylib_names(&apple_bytes).unwrap()
3284
-    );
3285
-    assert_eq!(
3286
-        segment_flags(&our_bytes, "__DATA_CONST"),
3287
-        Some(SG_READ_ONLY)
3288
-    );
3289
-    assert_eq!(
3290
-        segment_flags(&our_bytes, "__DATA_CONST"),
3291
-        segment_flags(&apple_bytes, "__DATA_CONST")
3292
-    );
3293
-
3294
-    let our_rebases = decode_rebase_records(&our_bytes).unwrap();
3295
-    let apple_rebases = decode_rebase_records(&apple_bytes).unwrap();
3296
-    assert!(our_rebases
3297
-        .iter()
3298
-        .all(|record| record.rebase_type == REBASE_TYPE_POINTER));
3299
-    assert_eq!(our_rebases, apple_rebases);
3300
-    assert_eq!(
3301
-        decode_bind_records(&our_bytes, false).unwrap(),
3302
-        decode_bind_records(&apple_bytes, false).unwrap()
3303
-    );
3304
-    assert_eq!(
3305
-        dyld_info_stream(&our_bytes, DyldInfoStreamKind::WeakBind).unwrap(),
3306
-        dyld_info_stream(&apple_bytes, DyldInfoStreamKind::WeakBind).unwrap()
3307
-    );
3308
-    assert_eq!(
3309
-        decode_bind_records(&our_bytes, true).unwrap(),
3310
-        decode_bind_records(&apple_bytes, true).unwrap()
3311
-    );
3312
-    assert_eq!(
3313
-        canonical_lazy_bind_stream(&our_bytes).unwrap(),
3314
-        canonical_lazy_bind_stream(&apple_bytes).unwrap()
3315
-    );
6197
+    assert!(output_section(&our_bytes, "__TEXT", "__eh_frame").is_some());
33166198
     assert_eq!(
3317
-        indirect_symbol_table(&our_bytes),
3318
-        indirect_symbol_table(&apple_bytes)
6199
+        output_section(&our_bytes, "__TEXT", "__eh_frame")
6200
+            .unwrap()
6201
+            .1
6202
+            .len(),
6203
+        output_section(&apple_bytes, "__TEXT", "__eh_frame")
6204
+            .unwrap()
6205
+            .1
6206
+            .len()
33196207
     );
6208
+    let our_dump = normalized_eh_frame_dump(
6209
+        &our_out,
6210
+        output_section(&our_bytes, "__TEXT", "__text").unwrap().0,
6211
+    )
6212
+    .unwrap();
6213
+    let apple_dump = normalized_eh_frame_dump(
6214
+        &apple_out,
6215
+        output_section(&apple_bytes, "__TEXT", "__text").unwrap().0,
6216
+    )
6217
+    .unwrap();
6218
+    assert_eq!(our_dump, apple_dump);
33206219
 
3321
-    let _ = fs::remove_file(apple_out);
3322
-    let _ = fs::remove_file(our_out);
33236220
     let _ = fs::remove_file(obj);
6221
+    let _ = fs::remove_file(our_out);
6222
+    let _ = fs::remove_file(apple_out);
33246223
 }
33256224
 
33266225
 #[test]
3327
-fn classic_lazy_surfaces_match_apple_ld_across_fixture_matrix() {
3328
-    if !have_xcrun() || !have_xcrun_tool("ld") {
3329
-        eprintln!("skipping: xcrun as/ld unavailable");
6226
+fn linker_run_dead_strip_preserves_pruned_eh_frame_like_ld() {
6227
+    if !have_xcrun() || !have_xcrun_tool("dwarfdump") {
6228
+        eprintln!("skipping: xcrun dwarfdump unavailable");
33306229
         return;
33316230
     }
33326231
     let Some(sdk) = sdk_path() else {
@@ -3338,89 +6237,101 @@ fn classic_lazy_surfaces_match_apple_ld_across_fixture_matrix() {
33386237
         return;
33396238
     };
33406239
 
3341
-    let cases = [
3342
-        ClassicLazyParityCase {
3343
-            name: "single-got-and-call",
3344
-            src: r#"
3345
-                .section __TEXT,__text,regular,pure_instructions
3346
-                .globl _main
3347
-                _main:
3348
-                    adrp x0, _write@GOTPAGE
3349
-                    ldr x0, [x0, _write@GOTPAGEOFF]
3350
-                    bl _write
3351
-                    ret
3352
-                .subsections_via_symbols
3353
-            "#,
3354
-        },
3355
-        ClassicLazyParityCase {
3356
-            name: "batched-got-and-calls",
3357
-            src: r#"
3358
-                .section __TEXT,__text,regular,pure_instructions
3359
-                .globl _main
3360
-                _main:
3361
-                    adrp x0, _write@GOTPAGE
3362
-                    ldr x0, [x0, _write@GOTPAGEOFF]
3363
-                    bl _write
3364
-                    adrp x1, _close@GOTPAGE
3365
-                    ldr x1, [x1, _close@GOTPAGEOFF]
3366
-                    bl _close
3367
-                    adrp x2, _read@GOTPAGE
3368
-                    ldr x2, [x2, _read@GOTPAGEOFF]
3369
-                    bl _read
3370
-                    ret
3371
-                .subsections_via_symbols
3372
-            "#,
3373
-        },
3374
-        ClassicLazyParityCase {
3375
-            name: "branch-only-calls",
3376
-            src: r#"
3377
-                .section __TEXT,__text,regular,pure_instructions
3378
-                .globl _main
3379
-                _main:
3380
-                    bl _write
3381
-                    bl _close
3382
-                    bl _read
3383
-                    ret
3384
-                .subsections_via_symbols
3385
-            "#,
3386
-        },
3387
-        ClassicLazyParityCase {
3388
-            name: "deduped-import",
3389
-            src: r#"
3390
-                .section __TEXT,__text,regular,pure_instructions
3391
-                .globl _main
3392
-                _main:
3393
-                    adrp x0, _write@GOTPAGE
3394
-                    ldr x0, [x0, _write@GOTPAGEOFF]
3395
-                    bl _write
3396
-                    bl _write
3397
-                    adrp x1, _write@GOTPAGE
3398
-                    ldr x1, [x1, _write@GOTPAGEOFF]
3399
-                    ret
3400
-                .subsections_via_symbols
3401
-            "#,
3402
-        },
3403
-    ];
6240
+    let obj = scratch("eh-frame-dead-strip.o");
6241
+    let our_out = scratch("eh-frame-dead-strip-ours.out");
6242
+    let apple_out = scratch("eh-frame-dead-strip-apple.out");
6243
+    let asm = r#"
6244
+        .text
6245
+        .globl _main
6246
+        .p2align 2
6247
+    _main:
6248
+        .cfi_startproc
6249
+        sub sp, sp, #16
6250
+        .cfi_def_cfa_offset 16
6251
+        str x30, [sp, #8]
6252
+        .cfi_offset w30, -8
6253
+        bl _helper
6254
+        ldr x30, [sp, #8]
6255
+        add sp, sp, #16
6256
+        ret
6257
+        .cfi_endproc
34046258
 
3405
-    let mut failures = Vec::new();
3406
-    for case in &cases {
3407
-        if let Err(err) = assert_classic_lazy_case_matches_apple_ld(case, &sdk, &sdk_ver) {
3408
-            failures.push(err);
3409
-        }
6259
+        .globl _helper
6260
+        .p2align 2
6261
+    _helper:
6262
+        .cfi_startproc
6263
+        sub sp, sp, #16
6264
+        .cfi_def_cfa_offset 16
6265
+        str x30, [sp, #8]
6266
+        .cfi_offset w30, -8
6267
+        ldr x30, [sp, #8]
6268
+        add sp, sp, #16
6269
+        ret
6270
+        .cfi_endproc
6271
+
6272
+        .globl _unused
6273
+        .p2align 2
6274
+    _unused:
6275
+        .cfi_startproc
6276
+        sub sp, sp, #16
6277
+        .cfi_def_cfa_offset 16
6278
+        str x30, [sp, #8]
6279
+        .cfi_offset w30, -8
6280
+        ldr x30, [sp, #8]
6281
+        add sp, sp, #16
6282
+        ret
6283
+        .cfi_endproc
6284
+        .subsections_via_symbols
6285
+    "#;
6286
+    if let Err(e) = assemble(asm, &obj) {
6287
+        eprintln!("skipping: assemble failed: {e}");
6288
+        return;
34106289
     }
34116290
 
3412
-    assert!(
3413
-        failures.is_empty(),
3414
-        "Apple ld classic-lazy parity failures ({} cases):\n{}",
3415
-        failures.len(),
3416
-        failures.join("\n\n")
6291
+    let opts = LinkOptions {
6292
+        inputs: vec![obj.clone()],
6293
+        output: Some(our_out.clone()),
6294
+        kind: OutputKind::Executable,
6295
+        dead_strip: true,
6296
+        ..LinkOptions::default()
6297
+    };
6298
+    Linker::run(&opts).unwrap();
6299
+    apple_link_with_args(&obj, &apple_out, "_main", &sdk, &sdk_ver, &["-dead_strip"]).unwrap();
6300
+
6301
+    let our_bytes = fs::read(&our_out).unwrap();
6302
+    let apple_bytes = fs::read(&apple_out).unwrap();
6303
+    assert!(output_section(&our_bytes, "__TEXT", "__eh_frame").is_some());
6304
+    assert_eq!(
6305
+        output_section(&our_bytes, "__TEXT", "__eh_frame")
6306
+            .unwrap()
6307
+            .1
6308
+            .len(),
6309
+        output_section(&apple_bytes, "__TEXT", "__eh_frame")
6310
+            .unwrap()
6311
+            .1
6312
+            .len()
34176313
     );
6314
+    let our_dump = normalized_eh_frame_dump(
6315
+        &our_out,
6316
+        output_section(&our_bytes, "__TEXT", "__text").unwrap().0,
6317
+    )
6318
+    .unwrap();
6319
+    let apple_dump = normalized_eh_frame_dump(
6320
+        &apple_out,
6321
+        output_section(&apple_bytes, "__TEXT", "__text").unwrap().0,
6322
+    )
6323
+    .unwrap();
6324
+    assert_eq!(our_dump, apple_dump);
6325
+
6326
+    let _ = fs::remove_file(obj);
6327
+    let _ = fs::remove_file(our_out);
6328
+    let _ = fs::remove_file(apple_out);
34186329
 }
34196330
 
34206331
 #[test]
3421
-fn linker_run_binds_direct_dylib_import_pointers() {
3422
-    if !have_xcrun() || !have_tool("codesign") {
3423
-        eprintln!("skipping: xcrun clang or codesign unavailable");
6332
+fn linker_run_emits_backtrace_metadata_like_apple_ld() {
6333
+    if !have_xcrun() {
6334
+        eprintln!("skipping: xcrun unavailable");
34246335
         return;
34256336
     }
34266337
     let Some(sdk) = sdk_path() else {
@@ -3437,85 +6348,139 @@ fn linker_run_binds_direct_dylib_import_pointers() {
34376348
         return;
34386349
     }
34396350
 
3440
-    let dylib_src = r#"
3441
-        int ext_data = 5;
6351
+    let obj = scratch("unwind-backtrace.o");
6352
+    let our_out = scratch("unwind-backtrace-ours.out");
6353
+    let apple_out = scratch("unwind-backtrace-apple.out");
6354
+    let src = r#"
6355
+        #include <unwind.h>
6356
+
6357
+        static _Unwind_Reason_Code cb(struct _Unwind_Context* ctx, void* arg) {
6358
+            (void)ctx;
6359
+            int* count = (int*)arg;
6360
+            (*count)++;
6361
+            return *count >= 8 ? _URC_END_OF_STACK : _URC_NO_REASON;
6362
+        }
6363
+
6364
+        __attribute__((noinline)) int helper(void) {
6365
+            int count = 0;
6366
+            _Unwind_Backtrace(cb, &count);
6367
+            return count;
6368
+        }
6369
+
6370
+        int main(void) {
6371
+            return helper() > 1 ? 0 : 1;
6372
+        }
34426373
     "#;
3443
-    let direct_case = DirectBindParityCase {
3444
-        name: "direct-data",
3445
-        dylib_src,
3446
-        main_src: r#"
3447
-            extern int ext_data;
3448
-            int *p = &ext_data;
3449
-            int main(void) { return *p == 5 ? 0 : 1; }
3450
-        "#,
3451
-    };
3452
-    if let Err(e) = assert_direct_bind_case_matches_apple_ld(&direct_case, &sdk, &sdk_ver) {
3453
-        panic!("{e}");
6374
+    if let Err(e) = compile_c(src, &obj) {
6375
+        eprintln!("skipping: clang compile failed: {e}");
6376
+        return;
34546377
     }
34556378
 
3456
-    let dylib = scratch("direct-data.dylib");
3457
-    let obj = scratch("direct-data.o");
3458
-    let our_out = scratch("direct-data-ours.out");
6379
+    let opts = LinkOptions {
6380
+        inputs: vec![obj.clone(), tbd],
6381
+        output: Some(our_out.clone()),
6382
+        kind: OutputKind::Executable,
6383
+        ..LinkOptions::default()
6384
+    };
6385
+    Linker::run(&opts).unwrap();
6386
+    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
34596387
 
3460
-    if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
3461
-        eprintln!("skipping: dylib compile failed: {e}");
6388
+    let our_bytes = fs::read(&our_out).unwrap();
6389
+    let apple_bytes = fs::read(&apple_out).unwrap();
6390
+    assert_eq!(
6391
+        rebased_unwind_bytes(&our_bytes),
6392
+        rebased_unwind_bytes(&apple_bytes)
6393
+    );
6394
+    assert_eq!(
6395
+        normalize_function_start_offsets(&decode_function_starts(&our_bytes)),
6396
+        normalize_function_start_offsets(&decode_function_starts(&apple_bytes))
6397
+    );
6398
+
6399
+    let _ = fs::remove_file(obj);
6400
+    let _ = fs::remove_file(our_out);
6401
+    let _ = fs::remove_file(apple_out);
6402
+}
6403
+
6404
+#[test]
6405
+fn linker_run_preserves_exception_unwind_metadata_like_apple_ld() {
6406
+    if !have_xcrun() || !have_xcrun_tool("clang++") || !have_tool("codesign") {
6407
+        eprintln!("skipping: xcrun clang++ or codesign unavailable");
34626408
         return;
34636409
     }
34646410
 
3465
-    let main_src = r#"
3466
-        extern int ext_data;
3467
-        int *p = &ext_data;
3468
-        int main(void) { return *p == 5 ? 0 : 1; }
6411
+    let Some(sdk) = sdk_path() else {
6412
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
6413
+        return;
6414
+    };
6415
+    let libsystem = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6416
+    let libcxx = PathBuf::from(format!("{sdk}/usr/lib/libc++.tbd"));
6417
+    if !libsystem.exists() {
6418
+        eprintln!("skipping: no libSystem.tbd at {}", libsystem.display());
6419
+        return;
6420
+    }
6421
+    if !libcxx.exists() {
6422
+        eprintln!("skipping: no libc++.tbd at {}", libcxx.display());
6423
+        return;
6424
+    }
6425
+
6426
+    let obj = scratch("cxx-exc.o");
6427
+    let our_out = scratch("cxx-exc-ours.out");
6428
+    let apple_out = scratch("cxx-exc-apple.out");
6429
+    let src = r#"
6430
+        int helper() { throw 7; }
6431
+        int main() {
6432
+            try { return helper(); }
6433
+            catch (...) { return 42; }
6434
+        }
34696435
     "#;
3470
-    if let Err(e) = compile_c(main_src, &obj) {
3471
-        eprintln!("skipping: compile failed: {e}");
6436
+    if let Err(e) = compile_cxx(src, &obj) {
6437
+        eprintln!("skipping: clang++ compile failed: {e}");
34726438
         return;
34736439
     }
34746440
 
34756441
     let opts = LinkOptions {
3476
-        inputs: vec![obj.clone(), tbd.clone(), dylib.clone()],
6442
+        inputs: vec![obj.clone(), libcxx.clone(), libsystem.clone()],
34776443
         output: Some(our_out.clone()),
34786444
         kind: OutputKind::Executable,
34796445
         ..LinkOptions::default()
34806446
     };
34816447
     Linker::run(&opts).unwrap();
6448
+    apple_link_cxx_classic(&obj, &apple_out).unwrap();
6449
+
34826450
     let our_bytes = fs::read(&our_out).unwrap();
3483
-    let binds = decode_bind_records(&our_bytes, false).unwrap();
3484
-    assert!(
3485
-        binds.iter().any(|record| {
3486
-            record.segment == "__DATA"
3487
-                && record.section == "__data"
3488
-                && record.section_offset == 0
3489
-                && record.symbol == "_ext_data"
3490
-        }),
3491
-        "missing direct bind for imported data: {binds:#?}"
6451
+    let apple_bytes = fs::read(&apple_out).unwrap();
6452
+    assert_eq!(
6453
+        decode_bind_records(&our_bytes, false).unwrap(),
6454
+        decode_bind_records(&apple_bytes, false).unwrap()
34926455
     );
3493
-    let verify = Command::new("codesign")
3494
-        .arg("-v")
3495
-        .arg(&our_out)
3496
-        .output()
3497
-        .unwrap();
3498
-    assert!(
3499
-        verify.status.success(),
3500
-        "codesign verify failed: {}",
3501
-        String::from_utf8_lossy(&verify.stderr)
6456
+    assert_eq!(
6457
+        decode_bind_records(&our_bytes, true).unwrap(),
6458
+        decode_bind_records(&apple_bytes, true).unwrap()
35026459
     );
3503
-    let status = Command::new(&our_out).status().unwrap();
35046460
     assert_eq!(
3505
-        status.code(),
3506
-        Some(0),
3507
-        "expected direct-import pointer executable to exit 0"
6461
+        canonical_lazy_bind_stream(&our_bytes).unwrap(),
6462
+        canonical_lazy_bind_stream(&apple_bytes).unwrap()
35086463
     );
6464
+    let our_decoded = canonical_unwind_info(&our_bytes);
6465
+    let apple_decoded = canonical_unwind_info(&apple_bytes);
6466
+    assert_eq!(our_decoded, apple_decoded);
6467
+    assert_eq!(our_decoded.personalities.len(), 1);
6468
+    assert_eq!(our_decoded.lsdas.len(), 1);
6469
+    assert!(output_section(&our_bytes, "__TEXT", "__gcc_except_tab").is_some());
6470
+    let our_status = Command::new(&our_out).status().unwrap();
6471
+    let apple_status = Command::new(&apple_out).status().unwrap();
6472
+    assert_eq!(our_status.code(), Some(42));
6473
+    assert_eq!(apple_status.code(), Some(42));
35096474
 
3510
-    let _ = fs::remove_file(dylib);
35116475
     let _ = fs::remove_file(obj);
35126476
     let _ = fs::remove_file(our_out);
6477
+    let _ = fs::remove_file(apple_out);
35136478
 }
35146479
 
35156480
 #[test]
3516
-fn direct_bind_surfaces_match_apple_ld_across_fixture_matrix() {
3517
-    if !have_xcrun() || !have_tool("codesign") {
3518
-        eprintln!("skipping: xcrun clang or codesign unavailable");
6481
+fn linker_run_resolves_backtrace_symbols_at_runtime() {
6482
+    if !have_xcrun() {
6483
+        eprintln!("skipping: xcrun unavailable");
35196484
         return;
35206485
     }
35216486
     let Some(sdk) = sdk_path() else {
@@ -3526,66 +6491,86 @@ fn direct_bind_surfaces_match_apple_ld_across_fixture_matrix() {
35266491
         eprintln!("skipping: xcrun --show-sdk-version unavailable");
35276492
         return;
35286493
     };
6494
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6495
+    if !tbd.exists() {
6496
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
6497
+        return;
6498
+    }
35296499
 
3530
-    let cases = [
3531
-        DirectBindParityCase {
3532
-            name: "direct-multi-data",
3533
-            dylib_src: r#"
3534
-                int ext_data = 5;
3535
-                int more_data = 9;
3536
-            "#,
3537
-            main_src: r#"
3538
-                extern int ext_data;
3539
-                extern int more_data;
3540
-                int *p = &ext_data;
3541
-                int *q = &more_data;
3542
-                int main(void) { return (*p == 5 && *q == 9) ? 0 : 1; }
3543
-            "#,
3544
-        },
3545
-        DirectBindParityCase {
3546
-            name: "direct-and-call-mixed",
3547
-            dylib_src: r#"
3548
-                int ext_data = 5;
3549
-                int ext_fn(void) { return ext_data + 1; }
3550
-            "#,
3551
-            main_src: r#"
3552
-                extern int ext_data;
3553
-                extern int ext_fn(void);
3554
-                int *p = &ext_data;
3555
-                int main(void) { return *p + ext_fn() == 11 ? 0 : 1; }
3556
-            "#,
3557
-        },
3558
-        DirectBindParityCase {
3559
-            name: "direct-deduped",
3560
-            dylib_src: r#"
3561
-                int ext_data = 5;
3562
-            "#,
3563
-            main_src: r#"
3564
-                extern int ext_data;
3565
-                int *p = &ext_data;
3566
-                int *q = &ext_data;
3567
-                int main(void) { return (*p == 5 && *q == 5) ? 0 : 1; }
3568
-            "#,
3569
-        },
3570
-    ];
6500
+    let obj = scratch("execinfo-backtrace.o");
6501
+    let our_out = scratch("execinfo-backtrace-ours.out");
6502
+    let apple_out = scratch("execinfo-backtrace-apple.out");
6503
+    let src = r#"
6504
+        #include <execinfo.h>
6505
+        #include <stdio.h>
6506
+        #include <stdlib.h>
6507
+        #include <string.h>
35716508
 
3572
-    let mut failures = Vec::new();
3573
-    for case in &cases {
3574
-        if let Err(err) = assert_direct_bind_case_matches_apple_ld(case, &sdk, &sdk_ver) {
3575
-            failures.push(err);
6509
+        __attribute__((noinline)) int helper(void) {
6510
+            void *frames[8];
6511
+            int n = backtrace(frames, 8);
6512
+            char **syms = backtrace_symbols(frames, n);
6513
+            int saw_helper = 0;
6514
+            int saw_main = 0;
6515
+            if (!syms) return 2;
6516
+            for (int i = 0; i < n; i++) {
6517
+                puts(syms[i]);
6518
+                saw_helper |= strstr(syms[i], "helper") != NULL;
6519
+                saw_main |= strstr(syms[i], "main") != NULL;
6520
+            }
6521
+            free(syms);
6522
+            return (saw_helper && saw_main) ? 0 : 1;
6523
+        }
6524
+
6525
+        int main(void) {
6526
+            return helper();
35766527
         }
6528
+    "#;
6529
+    if let Err(e) = compile_c(src, &obj) {
6530
+        eprintln!("skipping: clang compile failed: {e}");
6531
+        return;
35776532
     }
35786533
 
6534
+    let opts = LinkOptions {
6535
+        inputs: vec![obj.clone(), tbd],
6536
+        output: Some(our_out.clone()),
6537
+        kind: OutputKind::Executable,
6538
+        ..LinkOptions::default()
6539
+    };
6540
+    Linker::run(&opts).unwrap();
6541
+    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
6542
+
6543
+    let our_output = Command::new(&our_out).output().unwrap();
6544
+    let apple_output = Command::new(&apple_out).output().unwrap();
6545
+    let our_stdout = String::from_utf8_lossy(&our_output.stdout);
6546
+    let apple_stdout = String::from_utf8_lossy(&apple_output.stdout);
6547
+
6548
+    assert_eq!(our_output.status.code(), Some(0));
6549
+    assert_eq!(apple_output.status.code(), Some(0));
35796550
     assert!(
3580
-        failures.is_empty(),
3581
-        "Apple ld direct-bind parity failures ({} cases):\n{}",
3582
-        failures.len(),
3583
-        failures.join("\n\n")
6551
+        our_stdout.contains("helper"),
6552
+        "expected helper in output: {our_stdout}"
6553
+    );
6554
+    assert!(
6555
+        our_stdout.contains("main"),
6556
+        "expected main in output: {our_stdout}"
6557
+    );
6558
+    assert!(
6559
+        apple_stdout.contains("helper"),
6560
+        "expected helper in apple output: {apple_stdout}"
6561
+    );
6562
+    assert!(
6563
+        apple_stdout.contains("main"),
6564
+        "expected main in apple output: {apple_stdout}"
35846565
     );
6566
+
6567
+    let _ = fs::remove_file(obj);
6568
+    let _ = fs::remove_file(our_out);
6569
+    let _ = fs::remove_file(apple_out);
35856570
 }
35866571
 
35876572
 #[test]
3588
-fn linker_run_rebases_local_absolute_pointers_like_ld() {
6573
+fn linker_run_emits_function_starts_like_ld() {
35896574
     if !have_xcrun() {
35906575
         eprintln!("skipping: xcrun unavailable");
35916576
         return;
@@ -3604,16 +6589,22 @@ fn linker_run_rebases_local_absolute_pointers_like_ld() {
36046589
         return;
36056590
     }
36066591
 
3607
-    let obj = scratch("local-rebase.o");
3608
-    let our_out = scratch("local-rebase-ours.out");
3609
-    let apple_out = scratch("local-rebase-apple.out");
3610
-    let src = r#"
3611
-        int ext = 7;
3612
-        int *p = &ext;
3613
-        int main(void) { return *p == 7 ? 0 : 1; }
6592
+    let obj = scratch("function-starts.o");
6593
+    let our_out = scratch("function-starts-ours.out");
6594
+    let apple_out = scratch("function-starts-apple.out");
6595
+    let asm = r#"
6596
+        .section __TEXT,__text,regular,pure_instructions
6597
+        .globl _main
6598
+        .p2align 2
6599
+    _main:
6600
+        adrp x0, _write@GOTPAGE
6601
+        ldr x0, [x0, _write@GOTPAGEOFF]
6602
+        bl _write
6603
+        ret
6604
+        .subsections_via_symbols
36146605
     "#;
3615
-    if let Err(e) = compile_c(src, &obj) {
3616
-        eprintln!("skipping: clang compile failed: {e}");
6606
+    if let Err(e) = assemble(asm, &obj) {
6607
+        eprintln!("skipping: assemble failed: {e}");
36176608
         return;
36186609
     }
36196610
 
@@ -3628,22 +6619,34 @@ fn linker_run_rebases_local_absolute_pointers_like_ld() {
36286619
 
36296620
     let our_bytes = fs::read(&our_out).unwrap();
36306621
     let apple_bytes = fs::read(&apple_out).unwrap();
3631
-    assert!(!dyld_info_stream(&our_bytes, DyldInfoStreamKind::Rebase)
3632
-        .unwrap()
3633
-        .is_empty());
6622
+    let our_fstarts = raw_linkedit_data_cmd(&our_bytes, LC_FUNCTION_STARTS);
6623
+    let apple_fstarts = raw_linkedit_data_cmd(&apple_bytes, LC_FUNCTION_STARTS);
6624
+    assert_ne!(our_fstarts.0, 0);
6625
+    assert_eq!(our_fstarts.1, apple_fstarts.1);
6626
+    assert_eq!(our_fstarts.1, 8);
6627
+    assert!(output_section(&our_bytes, "__TEXT", "__stubs").is_some());
6628
+    assert!(output_section(&our_bytes, "__TEXT", "__stub_helper").is_some());
6629
+    assert_eq!(decode_function_starts(&our_bytes).len(), 1);
6630
+    assert_eq!(decode_function_starts(&apple_bytes).len(), 1);
6631
+    let our_text_addr = output_section(&our_bytes, "__TEXT", "__text").unwrap().0;
6632
+    let apple_text_addr = output_section(&apple_bytes, "__TEXT", "__text").unwrap().0;
6633
+    let our_text_base = segment_vmaddr(&our_bytes, "__TEXT").unwrap();
6634
+    let apple_text_base = segment_vmaddr(&apple_bytes, "__TEXT").unwrap();
36346635
     assert_eq!(
3635
-        dyld_info_stream(&our_bytes, DyldInfoStreamKind::Rebase).unwrap(),
3636
-        dyld_info_stream(&apple_bytes, DyldInfoStreamKind::Rebase).unwrap()
6636
+        decode_function_starts(&our_bytes),
6637
+        vec![our_text_addr - our_text_base]
36376638
     );
36386639
     assert_eq!(
3639
-        decode_rebase_records(&our_bytes).unwrap(),
3640
-        decode_rebase_records(&apple_bytes).unwrap()
6640
+        decode_function_starts(&apple_bytes),
6641
+        vec![apple_text_addr - apple_text_base]
36416642
     );
36426643
 
3643
-    let our_status = Command::new(&our_out).status().unwrap();
3644
-    let apple_status = Command::new(&apple_out).status().unwrap();
3645
-    assert_eq!(our_status.code(), Some(0));
3646
-    assert_eq!(apple_status.code(), Some(0));
6644
+    let our_dic = raw_linkedit_data_cmd(&our_bytes, LC_DATA_IN_CODE);
6645
+    let apple_dic = raw_linkedit_data_cmd(&apple_bytes, LC_DATA_IN_CODE);
6646
+    assert_ne!(our_dic.0, 0);
6647
+    assert_eq!(our_dic.1, apple_dic.1);
6648
+    assert_eq!(our_dic.0, our_fstarts.0 + our_fstarts.1);
6649
+    assert_eq!(apple_dic.0, apple_fstarts.0 + apple_fstarts.1);
36476650
 
36486651
     let _ = fs::remove_file(obj);
36496652
     let _ = fs::remove_file(our_out);
@@ -3651,9 +6654,9 @@ fn linker_run_rebases_local_absolute_pointers_like_ld() {
36516654
 }
36526655
 
36536656
 #[test]
3654
-fn linker_run_routes_local_got_loads_through_rebased_slots() {
3655
-    if !have_xcrun() || !have_tool("codesign") {
3656
-        eprintln!("skipping: xcrun or codesign unavailable");
6657
+fn linker_run_emits_function_starts_for_other_text_sections_like_ld() {
6658
+    if !have_xcrun() {
6659
+        eprintln!("skipping: xcrun unavailable");
36576660
         return;
36586661
     }
36596662
     let Some(sdk) = sdk_path() else {
@@ -3664,76 +6667,54 @@ fn linker_run_routes_local_got_loads_through_rebased_slots() {
36646667
         eprintln!("skipping: xcrun --show-sdk-version unavailable");
36656668
         return;
36666669
     };
3667
-    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
3668
-    if !tbd.exists() {
3669
-        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
3670
-        return;
3671
-    }
36726670
 
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#"
6671
+    let obj = scratch("function-starts-textcoal.o");
6672
+    let our_out = scratch("function-starts-textcoal-ours.out");
6673
+    let apple_out = scratch("function-starts-textcoal-apple.out");
6674
+    let asm = r#"
36776675
         .section __TEXT,__text,regular,pure_instructions
36786676
         .globl _main
3679
-        _main:
3680
-            adrp x8, _value@GOTPAGE
3681
-            ldr x8, [x8, _value@GOTPAGEOFF]
3682
-            ldr w0, [x8]
3683
-            ret
6677
+        .p2align 2
6678
+    _main:
6679
+        ret
36846680
 
3685
-        .section __DATA,__data
3686
-        .globl _value
6681
+        .section __TEXT,__textcoal_nt,regular,pure_instructions
6682
+        .globl _helper
36876683
         .p2align 2
3688
-        _value:
3689
-            .long 7
6684
+    _helper:
6685
+        ret
36906686
         .subsections_via_symbols
36916687
     "#;
3692
-    if let Err(e) = assemble(src, &obj) {
6688
+    if let Err(e) = assemble(asm, &obj) {
36936689
         eprintln!("skipping: assemble failed: {e}");
36946690
         return;
36956691
     }
36966692
 
36976693
     let opts = LinkOptions {
3698
-        inputs: vec![obj.clone(), tbd.clone()],
6694
+        inputs: vec![obj.clone()],
36996695
         output: Some(our_out.clone()),
37006696
         kind: OutputKind::Executable,
37016697
         ..LinkOptions::default()
37026698
     };
37036699
     Linker::run(&opts).unwrap();
3704
-    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
6700
+    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
37056701
 
37066702
     let our_bytes = fs::read(&our_out).unwrap();
37076703
     let apple_bytes = fs::read(&apple_out).unwrap();
3708
-    let our_binds = decode_bind_records(&our_bytes, false).unwrap();
3709
-    let apple_binds = decode_bind_records(&apple_bytes, false).unwrap();
3710
-    assert_eq!(our_binds, apple_binds);
3711
-    assert!(
3712
-        our_binds.iter().all(|record| record.symbol != "_value"),
3713
-        "local GOT target should not be emitted as a dylib bind: {our_binds:#?}"
3714
-    );
3715
-    assert_eq!(
3716
-        output_section(&our_bytes, "__DATA_CONST", "__got")
3717
-            .expect("missing __got section")
3718
-            .1
3719
-            .len(),
3720
-        8
3721
-    );
3722
-    let verify = Command::new("codesign")
3723
-        .arg("-v")
3724
-        .arg(&our_out)
3725
-        .output()
3726
-        .unwrap();
3727
-    assert!(
3728
-        verify.status.success(),
3729
-        "codesign verify failed: {}",
3730
-        String::from_utf8_lossy(&verify.stderr)
3731
-    );
3732
-    let status = Command::new(&our_out).status().unwrap();
6704
+    assert_eq!(decode_function_starts(&our_bytes).len(), 2);
6705
+    assert_eq!(decode_function_starts(&apple_bytes).len(), 2);
6706
+
6707
+    let our_text_addr = output_section(&our_bytes, "__TEXT", "__text").unwrap().0;
6708
+    let our_textcoal_addr = output_section(&our_bytes, "__TEXT", "__textcoal_nt")
6709
+        .unwrap()
6710
+        .0;
6711
+    let our_text_base = segment_vmaddr(&our_bytes, "__TEXT").unwrap();
37336712
     assert_eq!(
3734
-        status.code(),
3735
-        Some(7),
3736
-        "expected local GOT executable to exit 7"
6713
+        decode_function_starts(&our_bytes),
6714
+        vec![
6715
+            our_text_addr - our_text_base,
6716
+            our_textcoal_addr - our_text_base
6717
+        ]
37376718
     );
37386719
 
37396720
     let _ = fs::remove_file(obj);
@@ -3742,9 +6723,9 @@ fn linker_run_routes_local_got_loads_through_rebased_slots() {
37426723
 }
37436724
 
37446725
 #[test]
3745
-fn linker_run_relaxes_hidden_got_loads_like_apple_ld() {
3746
-    if !have_xcrun() || !have_tool("codesign") {
3747
-        eprintln!("skipping: xcrun or codesign unavailable");
6726
+fn linker_run_remaps_data_in_code_like_ld() {
6727
+    if !have_xcrun() {
6728
+        eprintln!("skipping: xcrun unavailable");
37486729
         return;
37496730
     }
37506731
     let Some(sdk) = sdk_path() else {
@@ -3761,66 +6742,66 @@ fn linker_run_relaxes_hidden_got_loads_like_apple_ld() {
37616742
         return;
37626743
     }
37636744
 
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
6745
+    let obj = scratch("data-in-code.o");
6746
+    let our_out = scratch("data-in-code-ours.out");
6747
+    let apple_out = scratch("data-in-code-apple.out");
6748
+    let asm = r#"
6749
+        .text
37696750
         .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
37786751
         .p2align 2
3779
-        _value:
3780
-            .long 7
6752
+    _main:
6753
+        mov w0, #0
6754
+        b Ldispatch
6755
+        .p2align 2
6756
+    Ltable:
6757
+        .data_region jt32
6758
+        .long Lcase0-Ltable
6759
+        .long Lcase1-Ltable
6760
+        .end_data_region
6761
+    Ldispatch:
6762
+        cmp w0, #0
6763
+        b.eq Lcase0
6764
+        b Lcase1
6765
+    Lcase0:
6766
+        mov w0, #1
6767
+        ret
6768
+    Lcase1:
6769
+        mov w0, #2
6770
+        ret
37816771
         .subsections_via_symbols
37826772
     "#;
3783
-    if let Err(e) = assemble(src, &obj) {
6773
+    if let Err(e) = assemble(asm, &obj) {
37846774
         eprintln!("skipping: assemble failed: {e}");
37856775
         return;
37866776
     }
37876777
 
37886778
     let opts = LinkOptions {
3789
-        inputs: vec![obj.clone(), tbd.clone()],
6779
+        inputs: vec![obj.clone(), tbd],
37906780
         output: Some(our_out.clone()),
37916781
         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)
6782
+        ..LinkOptions::default()
6783
+    };
6784
+    Linker::run(&opts).unwrap();
6785
+    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
6786
+
6787
+    let our_bytes = fs::read(&our_out).unwrap();
6788
+    let apple_bytes = fs::read(&apple_out).unwrap();
6789
+    let our_dic = raw_linkedit_data_cmd(&our_bytes, LC_DATA_IN_CODE);
6790
+    let apple_dic = raw_linkedit_data_cmd(&apple_bytes, LC_DATA_IN_CODE);
6791
+    assert_ne!(our_dic.1, 0);
6792
+    assert_eq!(our_dic.1, apple_dic.1);
6793
+    assert_eq!(decode_data_in_code(&our_bytes).len(), 1);
6794
+    assert_eq!(
6795
+        canonical_data_in_code(&our_bytes),
6796
+        canonical_data_in_code(&apple_bytes)
38186797
     );
3819
-    let status = Command::new(&our_out).status().unwrap();
38206798
     assert_eq!(
3821
-        status.code(),
3822
-        Some(7),
3823
-        "expected hidden GOT executable to exit 7"
6799
+        canonical_data_in_code(&our_bytes),
6800
+        vec![DataInCodeRecord {
6801
+            offset: 8,
6802
+            length: 8,
6803
+            kind: DICE_KIND_JUMP_TABLE32,
6804
+        }]
38246805
     );
38256806
 
38266807
     let _ = fs::remove_file(obj);
@@ -3829,42 +6810,52 @@ fn linker_run_relaxes_hidden_got_loads_like_apple_ld() {
38296810
 }
38306811
 
38316812
 #[test]
3832
-fn linker_run_partitions_symtab_like_ld() {
6813
+fn linker_run_remaps_data_in_code_in_later_text_section_like_ld() {
38336814
     if !have_xcrun() {
38346815
         eprintln!("skipping: xcrun unavailable");
38356816
         return;
38366817
     }
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}");
6818
+    let Some(sdk) = sdk_path() else {
6819
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
6820
+        return;
6821
+    };
6822
+    let Some(sdk_ver) = sdk_version() else {
6823
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
6824
+        return;
6825
+    };
6826
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6827
+    if !tbd.exists() {
6828
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
38486829
         return;
38496830
     }
38506831
 
6832
+    let obj = scratch("data-in-code-late.o");
6833
+    let our_out = scratch("data-in-code-late-ours.out");
6834
+    let apple_out = scratch("data-in-code-late-apple.out");
38516835
     let asm = r#"
38526836
         .text
3853
-        .private_extern _hidden
3854
-        .globl _visible
38556837
         .globl _main
38566838
         .p2align 2
3857
-    _local:
6839
+    _main:
38586840
         ret
3859
-    _hidden:
6841
+
6842
+        .section __TEXT,__text2,regular,pure_instructions
6843
+        .globl _helper
6844
+        .p2align 2
6845
+    _helper:
6846
+        b Ldispatch
6847
+        .p2align 2
6848
+    Ltable:
6849
+        .data_region jt32
6850
+        .long Lcase0-Ltable
6851
+        .long Lcase1-Ltable
6852
+        .end_data_region
6853
+    Ldispatch:
38606854
         ret
3861
-    _visible:
6855
+    Lcase0:
38626856
         ret
3863
-    _main:
6857
+    Lcase1:
38646858
         ret
3865
-
3866
-        .data
3867
-        .quad _ext_data
38686859
         .subsections_via_symbols
38696860
     "#;
38706861
     if let Err(e) = assemble(asm, &obj) {
@@ -3873,95 +6864,85 @@ fn linker_run_partitions_symtab_like_ld() {
38736864
     }
38746865
 
38756866
     let opts = LinkOptions {
3876
-        inputs: vec![obj.clone(), dylib.clone()],
6867
+        inputs: vec![obj.clone(), tbd],
38776868
         output: Some(our_out.clone()),
38786869
         kind: OutputKind::Executable,
38796870
         ..LinkOptions::default()
38806871
     };
38816872
     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
-    );
6873
+    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
38956874
 
38966875
     let our_bytes = fs::read(&our_out).unwrap();
38976876
     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);
39086877
     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),
6878
+        canonical_data_in_code(&our_bytes),
6879
+        canonical_data_in_code(&apple_bytes)
39156880
     );
3916
-
39176881
     assert_eq!(
3918
-        symbol_partition_names(&our_bytes),
3919
-        symbol_partition_names(&apple_bytes)
6882
+        canonical_data_in_code(&our_bytes),
6883
+        vec![DataInCodeRecord {
6884
+            offset: 8,
6885
+            length: 8,
6886
+            kind: DICE_KIND_JUMP_TABLE32,
6887
+        }]
39206888
     );
39216889
 
3922
-    let _ = fs::remove_file(dylib);
39236890
     let _ = fs::remove_file(obj);
39246891
     let _ = fs::remove_file(our_out);
39256892
     let _ = fs::remove_file(apple_out);
39266893
 }
39276894
 
39286895
 #[test]
3929
-fn linker_run_strips_locals_with_x_like_ld() {
6896
+fn linker_run_remaps_data_in_code_after_large_first_text_section_like_ld() {
39306897
     if !have_xcrun() {
39316898
         eprintln!("skipping: xcrun unavailable");
39326899
         return;
39336900
     }
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}");
6901
+    let Some(sdk) = sdk_path() else {
6902
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
6903
+        return;
6904
+    };
6905
+    let Some(sdk_ver) = sdk_version() else {
6906
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
6907
+        return;
6908
+    };
6909
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6910
+    if !tbd.exists() {
6911
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
39456912
         return;
39466913
     }
39476914
 
6915
+    let obj = scratch("data-in-code-large-first.o");
6916
+    let our_out = scratch("data-in-code-large-first-ours.out");
6917
+    let apple_out = scratch("data-in-code-large-first-apple.out");
39486918
     let asm = r#"
39496919
         .text
3950
-        .private_extern _hidden
3951
-        .globl _visible
39526920
         .globl _main
39536921
         .p2align 2
3954
-    _local:
6922
+    _main:
6923
+        nop
6924
+        nop
6925
+        nop
6926
+        nop
6927
+        nop
39556928
         ret
3956
-    _hidden:
6929
+
6930
+        .section __TEXT,__text2,regular,pure_instructions
6931
+        .globl _helper
6932
+    _helper:
6933
+        b Ldispatch
6934
+        .p2align 2
6935
+    Ltable:
6936
+        .data_region jt32
6937
+        .long Lcase0-Ltable
6938
+        .long Lcase1-Ltable
6939
+        .end_data_region
6940
+    Ldispatch:
39576941
         ret
3958
-    _visible:
6942
+    Lcase0:
39596943
         ret
3960
-    _main:
6944
+    Lcase1:
39616945
         ret
3962
-
3963
-        .data
3964
-        .quad _ext_data
39656946
         .subsections_via_symbols
39666947
     "#;
39676948
     if let Err(e) = assemble(asm, &obj) {
@@ -3970,92 +6951,79 @@ fn linker_run_strips_locals_with_x_like_ld() {
39706951
     }
39716952
 
39726953
     let opts = LinkOptions {
3973
-        inputs: vec![obj.clone(), dylib.clone()],
6954
+        inputs: vec![obj.clone(), tbd],
39746955
         output: Some(our_out.clone()),
39756956
         kind: OutputKind::Executable,
3976
-        strip_locals: true,
39776957
         ..LinkOptions::default()
39786958
     };
39796959
     Linker::run(&opts).unwrap();
3980
-
3981
-    let apple = Command::new("xcrun")
3982
-        .args(["ld", "-arch", "arm64", "-x", "-e", "_main", "-o"])
3983
-        .arg(&apple_out)
3984
-        .arg(&obj)
3985
-        .arg(&dylib)
3986
-        .output()
3987
-        .unwrap();
3988
-    assert!(
3989
-        apple.status.success(),
3990
-        "xcrun ld failed: {}",
3991
-        String::from_utf8_lossy(&apple.stderr)
3992
-    );
6960
+    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
39936961
 
39946962
     let our_bytes = fs::read(&our_out).unwrap();
39956963
     let apple_bytes = fs::read(&apple_out).unwrap();
3996
-    let (our_symtab, our_dysymtab) = symtab_and_dysymtab(&our_bytes);
3997
-    let (apple_symtab, apple_dysymtab) = symtab_and_dysymtab(&apple_bytes);
3998
-
3999
-    assert_eq!(our_symtab.nsyms, apple_symtab.nsyms);
4000
-    assert_eq!(our_dysymtab.ilocalsym, apple_dysymtab.ilocalsym);
4001
-    assert_eq!(our_dysymtab.nlocalsym, apple_dysymtab.nlocalsym);
4002
-    assert_eq!(our_dysymtab.iextdefsym, apple_dysymtab.iextdefsym);
4003
-    assert_eq!(our_dysymtab.nextdefsym, apple_dysymtab.nextdefsym);
4004
-    assert_eq!(our_dysymtab.iundefsym, apple_dysymtab.iundefsym);
4005
-    assert_eq!(our_dysymtab.nundefsym, apple_dysymtab.nundefsym);
40066964
     assert_eq!(
4007
-        canonical_symbol_records(&our_bytes),
4008
-        canonical_symbol_records(&apple_bytes)
6965
+        canonical_data_in_code(&our_bytes),
6966
+        canonical_data_in_code(&apple_bytes)
40096967
     );
4010
-
4011
-    let (locals, extdefs, undefs) = symbol_partition_names(&our_bytes);
4012
-    assert!(locals.is_empty());
40136968
     assert_eq!(
4014
-        extdefs,
4015
-        vec![
4016
-            "__mh_execute_header".to_string(),
4017
-            "_main".to_string(),
4018
-            "_visible".to_string()
4019
-        ]
6969
+        canonical_data_in_code(&our_bytes),
6970
+        vec![DataInCodeRecord {
6971
+            offset: 28,
6972
+            length: 8,
6973
+            kind: DICE_KIND_JUMP_TABLE32,
6974
+        }]
40206975
     );
4021
-    assert_eq!(undefs, vec!["_ext_data".to_string()]);
40226976
 
4023
-    let _ = fs::remove_file(dylib);
40246977
     let _ = fs::remove_file(obj);
40256978
     let _ = fs::remove_file(our_out);
40266979
     let _ = fs::remove_file(apple_out);
40276980
 }
40286981
 
40296982
 #[test]
4030
-fn linker_run_emits_leaf_unwind_info_like_ld() {
6983
+fn linker_run_dedups_output_strtab_like_ld() {
40316984
     if !have_xcrun() {
40326985
         eprintln!("skipping: xcrun unavailable");
40336986
         return;
40346987
     }
4035
-    let Some(sdk) = sdk_path() else {
4036
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4037
-        return;
4038
-    };
4039
-    let Some(sdk_ver) = sdk_version() else {
4040
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
4041
-        return;
4042
-    };
4043
-
4044
-    let obj = scratch("unwind-leaf.o");
4045
-    let our_out = scratch("unwind-leaf-ours.out");
4046
-    let apple_out = scratch("unwind-leaf-apple.out");
4047
-    let src = r#"
4048
-        int main(void) {
4049
-            return 0;
4050
-        }
4051
-    "#;
4052
-    if let Err(e) = compile_c(src, &obj) {
4053
-        eprintln!("skipping: clang compile failed: {e}");
6988
+    let Some(sdk) = sdk_path() else {
6989
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
6990
+        return;
6991
+    };
6992
+    let Some(sdk_ver) = sdk_version() else {
6993
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
6994
+        return;
6995
+    };
6996
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6997
+    if !tbd.exists() {
6998
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
6999
+        return;
7000
+    }
7001
+
7002
+    let obj = scratch("strtab-dedup.o");
7003
+    let our_out = scratch("strtab-dedup-ours.out");
7004
+    let apple_out = scratch("strtab-dedup-apple.out");
7005
+    let mut asm =
7006
+        String::from("        .text\n        .globl _afs_array_sum\n        .globl _main\n");
7007
+    for idx in 0..20 {
7008
+        let symbol = format!("_pad_symbol_{idx:02}");
7009
+        asm.push_str(&format!("        .globl {symbol}\n"));
7010
+    }
7011
+    asm.push_str("        .p2align 2\n");
7012
+    asm.push_str("    _array_sum:\n        ret\n");
7013
+    asm.push_str("    _afs_array_sum:\n        ret\n");
7014
+    for idx in 0..20 {
7015
+        let symbol = format!("_pad_symbol_{idx:02}");
7016
+        asm.push_str(&format!("    {symbol}:\n        ret\n"));
7017
+    }
7018
+    asm.push_str("    _main:\n        bl _afs_array_sum\n        ret\n");
7019
+    asm.push_str("        .subsections_via_symbols\n");
7020
+    if let Err(e) = assemble(&asm, &obj) {
7021
+        eprintln!("skipping: assemble failed: {e}");
40547022
         return;
40557023
     }
40567024
 
40577025
     let opts = LinkOptions {
4058
-        inputs: vec![obj.clone()],
7026
+        inputs: vec![obj.clone(), tbd],
40597027
         output: Some(our_out.clone()),
40607028
         kind: OutputKind::Executable,
40617029
         ..LinkOptions::default()
@@ -4066,10 +7034,21 @@ fn linker_run_emits_leaf_unwind_info_like_ld() {
40667034
     let our_bytes = fs::read(&our_out).unwrap();
40677035
     let apple_bytes = fs::read(&apple_out).unwrap();
40687036
     assert_eq!(
4069
-        rebased_unwind_bytes(&our_bytes),
4070
-        rebased_unwind_bytes(&apple_bytes)
7037
+        canonical_symbol_records(&our_bytes),
7038
+        canonical_symbol_records(&apple_bytes)
7039
+    );
7040
+    let our_strtab = raw_string_table(&our_bytes);
7041
+    let apple_strtab = raw_string_table(&apple_bytes);
7042
+    assert_strtab_within_five_percent(&our_strtab, &apple_strtab);
7043
+    assert!(
7044
+        our_strtab.len() <= apple_strtab.len(),
7045
+        "suffix dedup should not grow the output string table: ours={} apple={}",
7046
+        our_strtab.len(),
7047
+        apple_strtab.len()
40717048
     );
4072
-    assert!(output_section(&our_bytes, "__LD", "__compact_unwind").is_none());
7049
+
7050
+    let offsets = symbol_name_offsets(&our_bytes);
7051
+    assert_eq!(offsets["_array_sum"], offsets["_afs_array_sum"] + 4);
40737052
 
40747053
     let _ = fs::remove_file(obj);
40757054
     let _ = fs::remove_file(our_out);
@@ -4077,93 +7056,109 @@ fn linker_run_emits_leaf_unwind_info_like_ld() {
40777056
 }
40787057
 
40797058
 #[test]
4080
-fn linker_run_emits_multi_function_unwind_info_like_ld() {
4081
-    if !have_xcrun() {
4082
-        eprintln!("skipping: xcrun unavailable");
7059
+fn linker_run_launches_with_classic_lazy_dylib_import() {
7060
+    if !have_xcrun() || !have_tool("codesign") {
7061
+        eprintln!("skipping: xcrun clang or codesign unavailable");
40837062
         return;
40847063
     }
40857064
     let Some(sdk) = sdk_path() else {
40867065
         eprintln!("skipping: xcrun --show-sdk-path unavailable");
40877066
         return;
40887067
     };
4089
-    let Some(sdk_ver) = sdk_version() else {
4090
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
7068
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
7069
+    if !tbd.exists() {
7070
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
40917071
         return;
4092
-    };
7072
+    }
40937073
 
4094
-    let obj = scratch("unwind-mixed.o");
4095
-    let our_out = scratch("unwind-mixed-ours.out");
4096
-    let apple_out = scratch("unwind-mixed-apple.out");
4097
-    let src = r#"
4098
-        int helper(void) {
4099
-            return 1;
4100
-        }
7074
+    let dylib = scratch("lazy-runtime.dylib");
7075
+    let obj = scratch("lazy-runtime.o");
7076
+    let out = scratch("lazy-runtime.out");
41017077
 
4102
-        int main(void) {
4103
-            return helper();
4104
-        }
7078
+    let dylib_src = r#"
7079
+        int ext_fn(void) { return 7; }
41057080
     "#;
4106
-    if let Err(e) = compile_c(src, &obj) {
4107
-        eprintln!("skipping: clang compile failed: {e}");
7081
+    if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
7082
+        eprintln!("skipping: dylib compile failed: {e}");
7083
+        return;
7084
+    }
7085
+
7086
+    let main_src = r#"
7087
+        int ext_fn(void);
7088
+        int main(void) { return ext_fn() == 7 ? 0 : 1; }
7089
+    "#;
7090
+    if let Err(e) = compile_c(main_src, &obj) {
7091
+        eprintln!("skipping: compile failed: {e}");
41087092
         return;
41097093
     }
41107094
 
41117095
     let opts = LinkOptions {
4112
-        inputs: vec![obj.clone()],
4113
-        output: Some(our_out.clone()),
7096
+        inputs: vec![obj.clone(), tbd, dylib.clone()],
7097
+        output: Some(out.clone()),
41147098
         kind: OutputKind::Executable,
41157099
         ..LinkOptions::default()
41167100
     };
41177101
     Linker::run(&opts).unwrap();
4118
-    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
41197102
 
4120
-    let our_bytes = fs::read(&our_out).unwrap();
4121
-    let apple_bytes = fs::read(&apple_out).unwrap();
7103
+    let verify = Command::new("codesign")
7104
+        .arg("-v")
7105
+        .arg(&out)
7106
+        .output()
7107
+        .unwrap();
7108
+    assert!(
7109
+        verify.status.success(),
7110
+        "codesign verify failed: {}",
7111
+        String::from_utf8_lossy(&verify.stderr)
7112
+    );
7113
+    let status = Command::new(&out).status().unwrap();
41227114
     assert_eq!(
4123
-        rebased_unwind_bytes(&our_bytes),
4124
-        rebased_unwind_bytes(&apple_bytes)
7115
+        status.code(),
7116
+        Some(0),
7117
+        "expected dylib-import executable to exit 0"
41257118
     );
4126
-    assert!(output_section(&our_bytes, "__LD", "__compact_unwind").is_none());
41277119
 
7120
+    let _ = fs::remove_file(dylib);
41287121
     let _ = fs::remove_file(obj);
4129
-    let _ = fs::remove_file(our_out);
4130
-    let _ = fs::remove_file(apple_out);
7122
+    let _ = fs::remove_file(out);
41317123
 }
41327124
 
41337125
 #[test]
4134
-fn linker_run_handles_large_unwind_function_gaps() {
4135
-    if !have_xcrun() {
4136
-        eprintln!("skipping: xcrun unavailable");
7126
+fn linker_run_handles_local_tlv_descriptors() {
7127
+    if !have_xcrun() || !have_tool("codesign") {
7128
+        eprintln!("skipping: xcrun clang or codesign unavailable");
7129
+        return;
7130
+    }
7131
+    let Some(sdk) = sdk_path() else {
7132
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
7133
+        return;
7134
+    };
7135
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
7136
+    if !tbd.exists() {
7137
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
41377138
         return;
41387139
     }
41397140
 
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
7141
+    let obj = scratch("tlvp-local.o");
7142
+    let out = scratch("tlvp-local.out");
7143
+    let src = r#"
7144
+        __thread long tls_a = 7;
7145
+        __thread long tls_b;
7146
+
7147
+        static long tls_sum(void) {
7148
+            return tls_a + tls_b;
7149
+        }
7150
+
7151
+        int main(void) {
7152
+            return tls_sum() == 7 ? 0 : 1;
7153
+        }
41597154
     "#;
4160
-    if let Err(e) = assemble(asm, &obj) {
4161
-        eprintln!("skipping: assemble failed: {e}");
7155
+    if let Err(e) = compile_c(src, &obj) {
7156
+        eprintln!("skipping: compile failed: {e}");
41627157
         return;
41637158
     }
41647159
 
41657160
     let opts = LinkOptions {
4166
-        inputs: vec![obj.clone()],
7161
+        inputs: vec![obj.clone(), tbd],
41677162
         output: Some(out.clone()),
41687163
         kind: OutputKind::Executable,
41697164
         ..LinkOptions::default()
@@ -4171,118 +7166,214 @@ fn linker_run_handles_large_unwind_function_gaps() {
41717166
     Linker::run(&opts).unwrap();
41727167
 
41737168
     let bytes = fs::read(&out).unwrap();
4174
-    let (_, unwind) = output_section(&bytes, "__TEXT", "__unwind_info").unwrap();
4175
-    let decoded = decode_unwind_info(&unwind).unwrap();
7169
+    let (_, thread_vars) = output_section(&bytes, "__DATA", "__thread_vars").unwrap();
7170
+    let (_, thread_data) = output_section(&bytes, "__DATA", "__thread_data").unwrap();
7171
+    assert!(output_section(&bytes, "__DATA", "__thread_ptrs").is_none());
7172
+    assert_eq!(thread_vars.len(), 48);
7173
+    assert_eq!(thread_data.len(), 8);
7174
+    assert_eq!(
7175
+        u64::from_le_bytes(thread_vars[16..24].try_into().unwrap()),
7176
+        0
7177
+    );
7178
+    assert_eq!(
7179
+        u64::from_le_bytes(thread_vars[40..48].try_into().unwrap()),
7180
+        8
7181
+    );
7182
+
7183
+    let binds = decode_bind_records(&bytes, false).unwrap();
7184
+    let mut tlv_binds: Vec<_> = binds
7185
+        .into_iter()
7186
+        .filter(|record| record.section == "__thread_vars" && record.symbol == "__tlv_bootstrap")
7187
+        .collect();
7188
+    tlv_binds.sort_by_key(|record| record.section_offset);
7189
+    assert_eq!(tlv_binds.len(), 2);
7190
+    assert_eq!(tlv_binds[0].section_offset, 0);
7191
+    assert_eq!(tlv_binds[1].section_offset, 24);
7192
+
7193
+    let header = parse_header(&bytes).unwrap();
7194
+    let commands = parse_commands(&header, &bytes).unwrap();
7195
+    let symtab = commands
7196
+        .iter()
7197
+        .find_map(|cmd| match cmd {
7198
+            LoadCommand::Symtab(cmd) => Some(*cmd),
7199
+            _ => None,
7200
+        })
7201
+        .unwrap();
7202
+    let symbols = parse_nlist_table(&bytes, symtab.symoff, symtab.nsyms).unwrap();
7203
+    let strings = StringTable::from_file(&bytes, symtab.stroff, symtab.strsize).unwrap();
7204
+    let symbol_names: Vec<&str> = symbols
7205
+        .iter()
7206
+        .map(|symbol| strings.get(symbol.strx()).unwrap())
7207
+        .collect();
7208
+    assert!(symbol_names.contains(&"__tlv_bootstrap"));
7209
+
7210
+    let verify = Command::new("codesign")
7211
+        .arg("-v")
7212
+        .arg(&out)
7213
+        .output()
7214
+        .unwrap();
41767215
     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"
7216
+        verify.status.success(),
7217
+        "codesign verify failed: {}",
7218
+        String::from_utf8_lossy(&verify.stderr)
41827219
     );
7220
+    let status = Command::new(&out).status().unwrap();
7221
+    assert_eq!(status.code(), Some(0), "expected TLV executable to exit 0");
41837222
 
41847223
     let _ = fs::remove_file(obj);
41857224
     let _ = fs::remove_file(out);
41867225
 }
41877226
 
41887227
 #[test]
4189
-fn linker_run_preserves_eh_frame_like_ld() {
4190
-    if !have_xcrun() || !have_xcrun_tool("dwarfdump") {
4191
-        eprintln!("skipping: xcrun dwarfdump unavailable");
7228
+fn linker_run_routes_imported_tlv_through_got() {
7229
+    if !have_xcrun() || !have_tool("codesign") {
7230
+        eprintln!("skipping: xcrun clang or codesign unavailable");
7231
+        return;
7232
+    }
7233
+    let Some(sdk) = sdk_path() else {
7234
+        eprintln!("skipping: xcrun --show-sdk-path unavailable");
7235
+        return;
7236
+    };
7237
+    let Some(sdk_ver) = sdk_version() else {
7238
+        eprintln!("skipping: xcrun --show-sdk-version unavailable");
7239
+        return;
7240
+    };
7241
+    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
7242
+    if !tbd.exists() {
7243
+        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
7244
+        return;
7245
+    }
7246
+
7247
+    let dylib = scratch("libtlvprobe.dylib");
7248
+    let obj = scratch("imported-tlv.o");
7249
+    let our_out = scratch("imported-tlv-ours.out");
7250
+    let apple_out = scratch("imported-tlv-apple.out");
7251
+
7252
+    let dylib_src = r#"
7253
+        __thread long ext_tls = 5;
7254
+        long read_lib_tls(void) { return ext_tls; }
7255
+    "#;
7256
+    if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
7257
+        eprintln!("skipping: dylib compile failed: {e}");
41927258
         return;
41937259
     }
4194
-    let Some(sdk) = sdk_path() else {
4195
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4196
-        return;
4197
-    };
4198
-    let Some(sdk_ver) = sdk_version() else {
4199
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
4200
-        return;
4201
-    };
4202
-
4203
-    let obj = scratch("eh-frame.o");
4204
-    let our_out = scratch("eh-frame-ours.out");
4205
-    let apple_out = scratch("eh-frame-apple.out");
4206
-    let asm = r#"
4207
-        .text
4208
-        .globl _main
4209
-        .p2align 2
4210
-    _main:
4211
-        .cfi_startproc
4212
-        sub sp, sp, #16
4213
-        .cfi_def_cfa_offset 16
4214
-        str x30, [sp, #8]
4215
-        .cfi_offset w30, -8
4216
-        bl _helper
4217
-        ldr x30, [sp, #8]
4218
-        add sp, sp, #16
4219
-        ret
4220
-        .cfi_endproc
42217260
 
4222
-        .globl _helper
4223
-        .p2align 2
4224
-    _helper:
4225
-        .cfi_startproc
4226
-        ret
4227
-        .cfi_endproc
4228
-        .subsections_via_symbols
7261
+    let main_src = r#"
7262
+        extern __thread long ext_tls;
7263
+        int main(void) { return ext_tls == 5 ? 0 : 1; }
42297264
     "#;
4230
-    if let Err(e) = assemble(asm, &obj) {
4231
-        eprintln!("skipping: assemble failed: {e}");
7265
+    if let Err(e) = compile_c(main_src, &obj) {
7266
+        eprintln!("skipping: compile failed: {e}");
42327267
         return;
42337268
     }
42347269
 
42357270
     let opts = LinkOptions {
4236
-        inputs: vec![obj.clone()],
7271
+        inputs: vec![obj.clone(), tbd.clone(), dylib.clone()],
42377272
         output: Some(our_out.clone()),
42387273
         kind: OutputKind::Executable,
42397274
         ..LinkOptions::default()
42407275
     };
42417276
     Linker::run(&opts).unwrap();
4242
-    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
7277
+
7278
+    let apple = Command::new("xcrun")
7279
+        .args([
7280
+            "ld",
7281
+            "-arch",
7282
+            "arm64",
7283
+            "-platform_version",
7284
+            "macos",
7285
+            &sdk_ver,
7286
+            &sdk_ver,
7287
+            "-syslibroot",
7288
+            &sdk,
7289
+            "-no_fixup_chains",
7290
+            "-lSystem",
7291
+            "-e",
7292
+            "_main",
7293
+            "-o",
7294
+        ])
7295
+        .arg(&apple_out)
7296
+        .arg(&obj)
7297
+        .arg(&dylib)
7298
+        .output()
7299
+        .unwrap();
7300
+    assert!(
7301
+        apple.status.success(),
7302
+        "xcrun ld failed: {}",
7303
+        String::from_utf8_lossy(&apple.stderr)
7304
+    );
42437305
 
42447306
     let our_bytes = fs::read(&our_out).unwrap();
42457307
     let apple_bytes = fs::read(&apple_out).unwrap();
4246
-    assert!(output_section(&our_bytes, "__TEXT", "__eh_frame").is_some());
7308
+    let (our_text_addr, our_text) = output_section(&our_bytes, "__TEXT", "__text").unwrap();
7309
+    let (apple_text_addr, apple_text) = output_section(&apple_bytes, "__TEXT", "__text").unwrap();
7310
+    let (our_got_addr, our_got) = output_section(&our_bytes, "__DATA_CONST", "__got").unwrap();
7311
+    let (apple_got_addr, apple_got) =
7312
+        output_section(&apple_bytes, "__DATA_CONST", "__got").unwrap();
7313
+
7314
+    assert!(output_section(&our_bytes, "__DATA", "__thread_ptrs").is_none());
7315
+    assert!(output_section(&apple_bytes, "__DATA", "__thread_ptrs").is_none());
7316
+    assert_eq!(our_got.len(), 8);
7317
+    assert_eq!(our_got, apple_got);
42477318
     assert_eq!(
4248
-        output_section(&our_bytes, "__TEXT", "__eh_frame")
4249
-            .unwrap()
4250
-            .1
4251
-            .len(),
4252
-        output_section(&apple_bytes, "__TEXT", "__eh_frame")
4253
-            .unwrap()
4254
-            .1
4255
-            .len()
7319
+        decode_page_reference(&our_text, our_text_addr, 20, &PageRefKind::Load).unwrap(),
7320
+        our_got_addr
7321
+    );
7322
+    assert_eq!(
7323
+        decode_page_reference(&apple_text, apple_text_addr, 20, &PageRefKind::Load).unwrap(),
7324
+        apple_got_addr
7325
+    );
7326
+    assert_eq!(our_text, apple_text);
7327
+    assert_eq!(read_insn(&our_text, 24).unwrap(), 0xf9400000);
7328
+    assert_eq!(read_insn(&our_text, 28).unwrap(), 0xf9400008);
7329
+    assert_eq!(read_insn(&our_text, 32).unwrap(), 0xd63f0100);
7330
+    assert_eq!(
7331
+        decode_bind_records(&our_bytes, false).unwrap(),
7332
+        decode_bind_records(&apple_bytes, false).unwrap()
7333
+    );
7334
+    assert_eq!(
7335
+        load_dylib_names(&our_bytes).unwrap(),
7336
+        load_dylib_names(&apple_bytes).unwrap()
7337
+    );
7338
+    let verify = Command::new("codesign")
7339
+        .arg("-v")
7340
+        .arg(&our_out)
7341
+        .output()
7342
+        .unwrap();
7343
+    assert!(
7344
+        verify.status.success(),
7345
+        "codesign verify failed: {}",
7346
+        String::from_utf8_lossy(&verify.stderr)
7347
+    );
7348
+    let status = Command::new(&our_out).status().unwrap();
7349
+    assert_eq!(
7350
+        status.code(),
7351
+        Some(0),
7352
+        "expected imported TLV executable to exit 0"
42567353
     );
4257
-    let our_dump = normalized_eh_frame_dump(
4258
-        &our_out,
4259
-        output_section(&our_bytes, "__TEXT", "__text").unwrap().0,
4260
-    )
4261
-    .unwrap();
4262
-    let apple_dump = normalized_eh_frame_dump(
4263
-        &apple_out,
4264
-        output_section(&apple_bytes, "__TEXT", "__text").unwrap().0,
4265
-    )
4266
-    .unwrap();
4267
-    assert_eq!(our_dump, apple_dump);
42687354
 
7355
+    let _ = fs::remove_file(dylib);
42697356
     let _ = fs::remove_file(obj);
42707357
     let _ = fs::remove_file(our_out);
42717358
     let _ = fs::remove_file(apple_out);
42727359
 }
42737360
 
42747361
 #[test]
4275
-fn linker_run_emits_backtrace_metadata_like_apple_ld() {
4276
-    if !have_xcrun() {
4277
-        eprintln!("skipping: xcrun unavailable");
7362
+fn linker_run_preserves_runtime_tlv_descriptor_offsets() {
7363
+    if !have_xcrun() || !have_tool("codesign") {
7364
+        eprintln!("skipping: xcrun or codesign unavailable");
42787365
         return;
42797366
     }
7367
+    let Some(runtime) = find_runtime_archive() else {
7368
+        eprintln!("skipping: libarmfortas_rt.a not built");
7369
+        return;
7370
+    };
42807371
     let Some(sdk) = sdk_path() else {
4281
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
7372
+        eprintln!("skipping: no macOS SDK path");
42827373
         return;
42837374
     };
42847375
     let Some(sdk_ver) = sdk_version() else {
4285
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
7376
+        eprintln!("skipping: no macOS SDK version");
42867377
         return;
42877378
     };
42887379
     let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
@@ -4291,147 +7382,183 @@ fn linker_run_emits_backtrace_metadata_like_apple_ld() {
42917382
         return;
42927383
     }
42937384
 
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");
7385
+    let obj = scratch("runtime-hello.o");
7386
+    let out = scratch("runtime-hello.out");
7387
+    let apple_out = scratch("runtime-hello-apple.out");
42977388
     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
-        }
7389
+        extern void afs_program_init(void);
7390
+        extern void afs_program_finalize(void);
7391
+        extern void afs_write_string(int, const char *, long);
7392
+        extern void afs_write_newline(int);
43127393
 
43137394
         int main(void) {
4314
-            return helper() > 1 ? 0 : 1;
7395
+            afs_program_init();
7396
+            afs_write_string(6, "Hello, World!", 13);
7397
+            afs_write_newline(6);
7398
+            afs_program_finalize();
7399
+            return 0;
43157400
         }
43167401
     "#;
43177402
     if let Err(e) = compile_c(src, &obj) {
4318
-        eprintln!("skipping: clang compile failed: {e}");
7403
+        eprintln!("skipping: compile failed: {e}");
43197404
         return;
43207405
     }
43217406
 
43227407
     let opts = LinkOptions {
4323
-        inputs: vec![obj.clone(), tbd],
4324
-        output: Some(our_out.clone()),
7408
+        inputs: vec![obj.clone(), runtime.clone(), tbd],
7409
+        output: Some(out.clone()),
43257410
         kind: OutputKind::Executable,
43267411
         ..LinkOptions::default()
43277412
     };
43287413
     Linker::run(&opts).unwrap();
4329
-    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
43307414
 
4331
-    let our_bytes = fs::read(&our_out).unwrap();
7415
+    let apple = Command::new("xcrun")
7416
+        .args([
7417
+            "ld",
7418
+            "-arch",
7419
+            "arm64",
7420
+            "-platform_version",
7421
+            "macos",
7422
+            &sdk_ver,
7423
+            &sdk_ver,
7424
+            "-syslibroot",
7425
+            &sdk,
7426
+            "-lSystem",
7427
+            "-e",
7428
+            "_main",
7429
+            "-no_fixup_chains",
7430
+            "-o",
7431
+        ])
7432
+        .arg(&apple_out)
7433
+        .arg(&obj)
7434
+        .arg(&runtime)
7435
+        .output()
7436
+        .unwrap();
7437
+    assert!(
7438
+        apple.status.success(),
7439
+        "xcrun ld failed: {}",
7440
+        String::from_utf8_lossy(&apple.stderr)
7441
+    );
7442
+
7443
+    let verify = Command::new("codesign")
7444
+        .arg("-v")
7445
+        .arg(&out)
7446
+        .output()
7447
+        .unwrap();
7448
+    assert!(
7449
+        verify.status.success(),
7450
+        "codesign verify failed: {}",
7451
+        String::from_utf8_lossy(&verify.stderr)
7452
+    );
7453
+
7454
+    let bytes = fs::read(&out).unwrap();
43327455
     let apple_bytes = fs::read(&apple_out).unwrap();
7456
+    assert!(
7457
+        output_section(&bytes, "__DATA_CONST", "__const").is_some(),
7458
+        "runtime hello should promote file-backed __const data into __DATA_CONST"
7459
+    );
7460
+    assert!(
7461
+        output_section(&bytes, "__DATA", "__const").is_none(),
7462
+        "runtime hello should not leave file-backed __const data in __DATA"
7463
+    );
7464
+    let (thread_vars_addr, thread_vars) =
7465
+        output_section(&bytes, "__DATA", "__thread_vars").unwrap();
7466
+    let (thread_data_addr, _) = output_section(&bytes, "__DATA", "__thread_data").unwrap();
7467
+    let symbols = symbol_values(&bytes);
7468
+    let tlv_binds: Vec<_> = decode_bind_records(&bytes, false)
7469
+        .unwrap()
7470
+        .into_iter()
7471
+        .filter(|record| record.section == "__thread_vars")
7472
+        .collect();
7473
+    assert_eq!(
7474
+        tlv_binds.len(),
7475
+        thread_vars.len() / 24,
7476
+        "every TLV descriptor should carry exactly one bootstrap bind"
7477
+    );
7478
+    assert!(tlv_binds
7479
+        .iter()
7480
+        .all(|record| record.symbol == "__tlv_bootstrap"));
7481
+
7482
+    assert_eq!(
7483
+        decode_bind_records(&bytes, false).unwrap(),
7484
+        decode_bind_records(&apple_bytes, false).unwrap(),
7485
+        "runtime hello bind records diverged from Apple ld"
7486
+    );
43337487
     assert_eq!(
4334
-        rebased_unwind_bytes(&our_bytes),
4335
-        rebased_unwind_bytes(&apple_bytes)
7488
+        decode_bind_records(&bytes, true).unwrap(),
7489
+        decode_bind_records(&apple_bytes, true).unwrap(),
7490
+        "runtime hello lazy-bind records diverged from Apple ld"
43367491
     );
43377492
     assert_eq!(
4338
-        normalize_function_start_offsets(&decode_function_starts(&our_bytes)),
4339
-        normalize_function_start_offsets(&decode_function_starts(&apple_bytes))
7493
+        decode_rebase_records(&bytes).unwrap(),
7494
+        decode_rebase_records(&apple_bytes).unwrap(),
7495
+        "runtime hello rebase records diverged from Apple ld"
7496
+    );
7497
+    assert_eq!(
7498
+        indirect_symbol_identities(&bytes),
7499
+        indirect_symbol_identities(&apple_bytes),
7500
+        "runtime hello indirect symbol identities diverged from Apple ld"
43407501
     );
43417502
 
4342
-    let _ = fs::remove_file(obj);
4343
-    let _ = fs::remove_file(our_out);
4344
-    let _ = fs::remove_file(apple_out);
4345
-}
4346
-
4347
-#[test]
4348
-fn linker_run_preserves_exception_unwind_metadata_like_apple_ld() {
4349
-    if !have_xcrun() || !have_xcrun_tool("clang++") || !have_tool("codesign") {
4350
-        eprintln!("skipping: xcrun clang++ or codesign unavailable");
4351
-        return;
4352
-    }
4353
-
4354
-    let Some(sdk) = sdk_path() else {
4355
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4356
-        return;
4357
-    };
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;
7503
+    for (name, descriptor_addr) in symbols.iter().filter(|(name, value)| {
7504
+        !name.ends_with("$tlv$init")
7505
+            && **value >= thread_vars_addr
7506
+            && **value < thread_vars_addr + thread_vars.len() as u64
7507
+    }) {
7508
+        let init_name = format!("{name}$tlv$init");
7509
+        let Some(init_addr) = symbols.get(&init_name) else {
7510
+            continue;
7511
+        };
7512
+        let offset = (*descriptor_addr - thread_vars_addr) as usize;
7513
+        let actual = u64::from_le_bytes(thread_vars[offset + 16..offset + 24].try_into().unwrap());
7514
+        let expected = init_addr - thread_data_addr;
7515
+        assert_eq!(
7516
+            actual, expected,
7517
+            "TLV descriptor {} should point at {} via template offset",
7518
+            name, init_name
7519
+        );
43827520
     }
43837521
 
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();
7522
+    let output = Command::new(&out).output().unwrap();
7523
+    let apple_output = Command::new(&apple_out).output().unwrap();
43957524
     assert_eq!(
4396
-        decode_bind_records(&our_bytes, false).unwrap(),
4397
-        decode_bind_records(&apple_bytes, false).unwrap()
7525
+        output.status.code(),
7526
+        Some(0),
7527
+        "expected runtime hello executable to exit 0, stderr={}",
7528
+        String::from_utf8_lossy(&output.stderr)
43987529
     );
43997530
     assert_eq!(
4400
-        decode_bind_records(&our_bytes, true).unwrap(),
4401
-        decode_bind_records(&apple_bytes, true).unwrap()
7531
+        apple_output.status.code(),
7532
+        Some(0),
7533
+        "expected Apple-linked runtime hello executable to exit 0, stderr={}",
7534
+        String::from_utf8_lossy(&apple_output.stderr)
44027535
     );
44037536
     assert_eq!(
4404
-        canonical_lazy_bind_stream(&our_bytes).unwrap(),
4405
-        canonical_lazy_bind_stream(&apple_bytes).unwrap()
7537
+        String::from_utf8_lossy(&output.stdout),
7538
+        String::from_utf8_lossy(&apple_output.stdout)
44067539
     );
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));
44177540
 
44187541
     let _ = fs::remove_file(obj);
4419
-    let _ = fs::remove_file(our_out);
7542
+    let _ = fs::remove_file(out);
44207543
     let _ = fs::remove_file(apple_out);
44217544
 }
44227545
 
44237546
 #[test]
4424
-fn linker_run_resolves_backtrace_symbols_at_runtime() {
4425
-    if !have_xcrun() {
4426
-        eprintln!("skipping: xcrun unavailable");
7547
+fn linker_run_rebases_runtime_init_metadata_like_apple_ld() {
7548
+    if !have_xcrun() || !have_tool("codesign") {
7549
+        eprintln!("skipping: xcrun or codesign unavailable");
44277550
         return;
44287551
     }
7552
+    let Some(runtime) = find_runtime_archive() else {
7553
+        eprintln!("skipping: libarmfortas_rt.a not built");
7554
+        return;
7555
+    };
44297556
     let Some(sdk) = sdk_path() else {
4430
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
7557
+        eprintln!("skipping: no macOS SDK path");
44317558
         return;
44327559
     };
44337560
     let Some(sdk_ver) = sdk_version() else {
4434
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
7561
+        eprintln!("skipping: no macOS SDK version");
44357562
         return;
44367563
     };
44377564
     let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
@@ -4440,1161 +7567,1090 @@ fn linker_run_resolves_backtrace_symbols_at_runtime() {
44407567
         return;
44417568
     }
44427569
 
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");
7570
+    let obj = scratch("runtime-init-only.o");
7571
+    let our_out = scratch("runtime-init-only-ours.out");
7572
+    let apple_out = scratch("runtime-init-only-apple.out");
44467573
     let src = r#"
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
-        }
7574
+        extern void afs_program_init(void);
44677575
 
44687576
         int main(void) {
4469
-            return helper();
7577
+            afs_program_init();
7578
+            return 0;
44707579
         }
44717580
     "#;
44727581
     if let Err(e) = compile_c(src, &obj) {
4473
-        eprintln!("skipping: clang compile failed: {e}");
7582
+        eprintln!("skipping: compile failed: {e}");
44747583
         return;
44757584
     }
44767585
 
44777586
     let opts = LinkOptions {
4478
-        inputs: vec![obj.clone(), tbd],
7587
+        inputs: vec![obj.clone(), runtime.clone(), tbd],
44797588
         output: Some(our_out.clone()),
44807589
         kind: OutputKind::Executable,
44817590
         ..LinkOptions::default()
44827591
     };
44837592
     Linker::run(&opts).unwrap();
4484
-    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4485
-
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);
44907593
 
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}"
4496
-    );
7594
+    let apple = Command::new("xcrun")
7595
+        .args([
7596
+            "ld",
7597
+            "-arch",
7598
+            "arm64",
7599
+            "-platform_version",
7600
+            "macos",
7601
+            &sdk_ver,
7602
+            &sdk_ver,
7603
+            "-syslibroot",
7604
+            &sdk,
7605
+            "-lSystem",
7606
+            "-e",
7607
+            "_main",
7608
+            "-no_fixup_chains",
7609
+            "-o",
7610
+        ])
7611
+        .arg(&apple_out)
7612
+        .arg(&obj)
7613
+        .arg(&runtime)
7614
+        .output()
7615
+        .unwrap();
44977616
     assert!(
4498
-        our_stdout.contains("main"),
4499
-        "expected main in output: {our_stdout}"
7617
+        apple.status.success(),
7618
+        "xcrun ld failed: {}",
7619
+        String::from_utf8_lossy(&apple.stderr)
45007620
     );
4501
-    assert!(
4502
-        apple_stdout.contains("helper"),
4503
-        "expected helper in apple output: {apple_stdout}"
7621
+
7622
+    let our_bytes = fs::read(&our_out).unwrap();
7623
+    let apple_bytes = fs::read(&apple_out).unwrap();
7624
+    let our_rebases = decode_rebase_records(&our_bytes).unwrap();
7625
+    let apple_rebases = decode_rebase_records(&apple_bytes).unwrap();
7626
+    assert_eq!(
7627
+        our_rebases
7628
+            .iter()
7629
+            .filter(|record| record.section == "__const")
7630
+            .count(),
7631
+        apple_rebases
7632
+            .iter()
7633
+            .filter(|record| record.section == "__const")
7634
+            .count(),
7635
+        "runtime init const rebases diverged from Apple ld"
45047636
     );
4505
-    assert!(
4506
-        apple_stdout.contains("main"),
4507
-        "expected main in apple output: {apple_stdout}"
7637
+    assert_eq!(
7638
+        our_rebases
7639
+            .iter()
7640
+            .filter(|record| record.section == "__la_symbol_ptr")
7641
+            .count(),
7642
+        apple_rebases
7643
+            .iter()
7644
+            .filter(|record| record.section == "__la_symbol_ptr")
7645
+            .count(),
7646
+        "runtime init lazy-pointer rebases diverged from Apple ld"
45087647
     );
45097648
 
4510
-    let _ = fs::remove_file(obj);
4511
-    let _ = fs::remove_file(our_out);
4512
-    let _ = fs::remove_file(apple_out);
4513
-}
7649
+    let our_status = Command::new(&our_out).status().unwrap();
7650
+    let apple_status = Command::new(&apple_out).status().unwrap();
7651
+    assert_eq!(our_status.code(), Some(0));
7652
+    assert_eq!(apple_status.code(), Some(0));
45147653
 
4515
-#[test]
4516
-fn linker_run_emits_function_starts_like_ld() {
4517
-    if !have_xcrun() {
4518
-        eprintln!("skipping: xcrun unavailable");
4519
-        return;
4520
-    }
4521
-    let Some(sdk) = sdk_path() else {
4522
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4523
-        return;
4524
-    };
4525
-    let Some(sdk_ver) = sdk_version() else {
4526
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
7654
+    let _ = fs::remove_file(obj);
7655
+    let _ = fs::remove_file(our_out);
7656
+    let _ = fs::remove_file(apple_out);
7657
+}
7658
+
7659
+#[test]
7660
+fn linker_run_icf_safe_folds_identical_private_text() {
7661
+    if !have_xcrun() {
7662
+        eprintln!("skipping: xcrun unavailable");
45277663
         return;
45287664
     };
4529
-    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4530
-    if !tbd.exists() {
4531
-        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4532
-        return;
4533
-    }
45347665
 
4535
-    let obj = scratch("function-starts.o");
4536
-    let our_out = scratch("function-starts-ours.out");
4537
-    let apple_out = scratch("function-starts-apple.out");
4538
-    let asm = r#"
7666
+    let obj = scratch("icf-fold.o");
7667
+    let baseline_out = scratch("icf-fold-baseline.out");
7668
+    let our_out = scratch("icf-fold-ours.out");
7669
+    let src = r#"
45397670
         .section __TEXT,__text,regular,pure_instructions
45407671
         .globl _main
4541
-        .p2align 2
4542
-    _main:
4543
-        adrp x0, _write@GOTPAGE
4544
-        ldr x0, [x0, _write@GOTPAGEOFF]
4545
-        bl _write
4546
-        ret
7672
+        _main:
7673
+            stp x29, x30, [sp, #-16]!
7674
+            mov x29, sp
7675
+            bl _helper1
7676
+            bl _helper2
7677
+            ldp x29, x30, [sp], #16
7678
+            ret
7679
+
7680
+        .private_extern _helper1
7681
+        _helper1:
7682
+            mov w0, #7
7683
+            ret
7684
+
7685
+        .private_extern _helper2
7686
+        _helper2:
7687
+            mov w0, #7
7688
+            ret
45477689
         .subsections_via_symbols
45487690
     "#;
4549
-    if let Err(e) = assemble(asm, &obj) {
7691
+    if let Err(e) = assemble(src, &obj) {
45507692
         eprintln!("skipping: assemble failed: {e}");
45517693
         return;
45527694
     }
45537695
 
7696
+    let baseline_opts = LinkOptions {
7697
+        inputs: vec![obj.clone()],
7698
+        output: Some(baseline_out.clone()),
7699
+        kind: OutputKind::Executable,
7700
+        ..LinkOptions::default()
7701
+    };
7702
+    Linker::run(&baseline_opts).unwrap();
7703
+
45547704
     let opts = LinkOptions {
4555
-        inputs: vec![obj.clone(), tbd],
7705
+        inputs: vec![obj.clone()],
45567706
         output: Some(our_out.clone()),
45577707
         kind: OutputKind::Executable,
7708
+        icf_mode: afs_ld::IcfMode::Safe,
45587709
         ..LinkOptions::default()
45597710
     };
45607711
     Linker::run(&opts).unwrap();
4561
-    apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
45627712
 
7713
+    let baseline_bytes = fs::read(&baseline_out).unwrap();
45637714
     let our_bytes = fs::read(&our_out).unwrap();
4564
-    let apple_bytes = fs::read(&apple_out).unwrap();
4565
-    let our_fstarts = raw_linkedit_data_cmd(&our_bytes, LC_FUNCTION_STARTS);
4566
-    let apple_fstarts = raw_linkedit_data_cmd(&apple_bytes, LC_FUNCTION_STARTS);
4567
-    assert_ne!(our_fstarts.0, 0);
4568
-    assert_eq!(our_fstarts.1, apple_fstarts.1);
4569
-    assert_eq!(our_fstarts.1, 8);
4570
-    assert!(output_section(&our_bytes, "__TEXT", "__stubs").is_some());
4571
-    assert!(output_section(&our_bytes, "__TEXT", "__stub_helper").is_some());
4572
-    assert_eq!(decode_function_starts(&our_bytes).len(), 1);
4573
-    assert_eq!(decode_function_starts(&apple_bytes).len(), 1);
4574
-    let our_text_addr = output_section(&our_bytes, "__TEXT", "__text").unwrap().0;
4575
-    let apple_text_addr = output_section(&apple_bytes, "__TEXT", "__text").unwrap().0;
4576
-    let our_text_base = segment_vmaddr(&our_bytes, "__TEXT").unwrap();
4577
-    let apple_text_base = segment_vmaddr(&apple_bytes, "__TEXT").unwrap();
7715
+    let baseline_symbols = symbol_values(&baseline_bytes);
7716
+    let our_symbols = symbol_values(&our_bytes);
7717
+    let baseline_text = output_section(&baseline_bytes, "__TEXT", "__text")
7718
+        .unwrap()
7719
+        .1;
7720
+    let our_text = output_section(&our_bytes, "__TEXT", "__text").unwrap().1;
7721
+
45787722
     assert_eq!(
4579
-        decode_function_starts(&our_bytes),
4580
-        vec![our_text_addr - our_text_base]
7723
+        our_symbols.get("_helper1"),
7724
+        our_symbols.get("_helper2"),
7725
+        "expected afs-ld -icf=safe to coalesce identical private text atoms"
7726
+    );
7727
+    assert_ne!(
7728
+        baseline_symbols.get("_helper1"),
7729
+        baseline_symbols.get("_helper2"),
7730
+        "expected baseline link to keep identical helpers separate"
45817731
     );
45827732
     assert_eq!(
4583
-        decode_function_starts(&apple_bytes),
4584
-        vec![apple_text_addr - apple_text_base]
7733
+        Command::new(&our_out).status().unwrap().code(),
7734
+        Some(7),
7735
+        "folded executable should preserve runtime behavior"
7736
+    );
7737
+    assert!(
7738
+        our_text.len() < baseline_text.len(),
7739
+        "expected -icf=safe to reduce text size on identical helpers"
45857740
     );
4586
-
4587
-    let our_dic = raw_linkedit_data_cmd(&our_bytes, LC_DATA_IN_CODE);
4588
-    let apple_dic = raw_linkedit_data_cmd(&apple_bytes, LC_DATA_IN_CODE);
4589
-    assert_ne!(our_dic.0, 0);
4590
-    assert_eq!(our_dic.1, apple_dic.1);
4591
-    assert_eq!(our_dic.0, our_fstarts.0 + our_fstarts.1);
4592
-    assert_eq!(apple_dic.0, apple_fstarts.0 + apple_fstarts.1);
45937741
 
45947742
     let _ = fs::remove_file(obj);
7743
+    let _ = fs::remove_file(baseline_out);
45957744
     let _ = fs::remove_file(our_out);
4596
-    let _ = fs::remove_file(apple_out);
45977745
 }
45987746
 
45997747
 #[test]
4600
-fn linker_run_emits_function_starts_for_other_text_sections_like_ld() {
7748
+fn linker_run_icf_safe_keeps_address_taken_functions_distinct() {
46017749
     if !have_xcrun() {
46027750
         eprintln!("skipping: xcrun unavailable");
46037751
         return;
4604
-    }
4605
-    let Some(sdk) = sdk_path() else {
4606
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4607
-        return;
4608
-    };
4609
-    let Some(sdk_ver) = sdk_version() else {
4610
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
4611
-        return;
46127752
     };
46137753
 
4614
-    let obj = scratch("function-starts-textcoal.o");
4615
-    let our_out = scratch("function-starts-textcoal-ours.out");
4616
-    let apple_out = scratch("function-starts-textcoal-apple.out");
4617
-    let asm = r#"
7754
+    let obj = scratch("icf-address-taken.o");
7755
+    let baseline_out = scratch("icf-address-taken-baseline.out");
7756
+    let our_out = scratch("icf-address-taken-ours.out");
7757
+    let src = r#"
46187758
         .section __TEXT,__text,regular,pure_instructions
46197759
         .globl _main
4620
-        .p2align 2
4621
-    _main:
4622
-        ret
7760
+        _main:
7761
+            stp x29, x30, [sp, #-16]!
7762
+            mov x29, sp
7763
+            bl _helper1
7764
+            bl _helper2
7765
+            ldp x29, x30, [sp], #16
7766
+            ret
46237767
 
4624
-        .section __TEXT,__textcoal_nt,regular,pure_instructions
4625
-        .globl _helper
4626
-        .p2align 2
4627
-    _helper:
4628
-        ret
7768
+        .private_extern _helper1
7769
+        _helper1:
7770
+            mov w0, #7
7771
+            ret
7772
+
7773
+        .private_extern _helper2
7774
+        _helper2:
7775
+            mov w0, #7
7776
+            ret
7777
+
7778
+        .section __DATA,__const
7779
+        .p2align 3
7780
+        _ptrs:
7781
+            .quad _helper1
7782
+            .quad _helper2
46297783
         .subsections_via_symbols
46307784
     "#;
4631
-    if let Err(e) = assemble(asm, &obj) {
7785
+    if let Err(e) = assemble(src, &obj) {
46327786
         eprintln!("skipping: assemble failed: {e}");
46337787
         return;
46347788
     }
46357789
 
7790
+    let baseline_opts = LinkOptions {
7791
+        inputs: vec![obj.clone()],
7792
+        output: Some(baseline_out.clone()),
7793
+        kind: OutputKind::Executable,
7794
+        ..LinkOptions::default()
7795
+    };
7796
+    Linker::run(&baseline_opts).unwrap();
7797
+
46367798
     let opts = LinkOptions {
46377799
         inputs: vec![obj.clone()],
46387800
         output: Some(our_out.clone()),
46397801
         kind: OutputKind::Executable,
7802
+        icf_mode: afs_ld::IcfMode::Safe,
46407803
         ..LinkOptions::default()
46417804
     };
46427805
     Linker::run(&opts).unwrap();
4643
-    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
46447806
 
7807
+    let baseline_bytes = fs::read(&baseline_out).unwrap();
46457808
     let our_bytes = fs::read(&our_out).unwrap();
4646
-    let apple_bytes = fs::read(&apple_out).unwrap();
4647
-    assert_eq!(decode_function_starts(&our_bytes).len(), 2);
4648
-    assert_eq!(decode_function_starts(&apple_bytes).len(), 2);
4649
-
4650
-    let our_text_addr = output_section(&our_bytes, "__TEXT", "__text").unwrap().0;
4651
-    let our_textcoal_addr = output_section(&our_bytes, "__TEXT", "__textcoal_nt")
7809
+    let baseline_symbols = symbol_values(&baseline_bytes);
7810
+    let our_symbols = symbol_values(&our_bytes);
7811
+    let baseline_text = output_section(&baseline_bytes, "__TEXT", "__text")
46527812
         .unwrap()
4653
-        .0;
4654
-    let our_text_base = segment_vmaddr(&our_bytes, "__TEXT").unwrap();
7813
+        .1;
7814
+    let our_text = output_section(&our_bytes, "__TEXT", "__text").unwrap().1;
7815
+
7816
+    assert_ne!(
7817
+        our_symbols.get("_helper1"),
7818
+        our_symbols.get("_helper2"),
7819
+        "address-taken helpers should not be folded by afs-ld -icf=safe"
7820
+    );
7821
+    assert_ne!(
7822
+        baseline_symbols.get("_helper1"),
7823
+        baseline_symbols.get("_helper2"),
7824
+        "baseline link should keep address-taken helpers separate"
7825
+    );
46557826
     assert_eq!(
4656
-        decode_function_starts(&our_bytes),
4657
-        vec![
4658
-            our_text_addr - our_text_base,
4659
-            our_textcoal_addr - our_text_base
4660
-        ]
7827
+        Command::new(&our_out).status().unwrap().code(),
7828
+        Some(7),
7829
+        "address-taken executable should preserve runtime behavior"
7830
+    );
7831
+    assert_eq!(
7832
+        our_text.len(),
7833
+        baseline_text.len(),
7834
+        "address-taken helpers should not shrink under -icf=safe"
46617835
     );
46627836
 
46637837
     let _ = fs::remove_file(obj);
7838
+    let _ = fs::remove_file(baseline_out);
46647839
     let _ = fs::remove_file(our_out);
4665
-    let _ = fs::remove_file(apple_out);
46667840
 }
46677841
 
46687842
 #[test]
4669
-fn linker_run_remaps_data_in_code_like_ld() {
7843
+fn linker_run_icf_safe_keeps_adrp_add_address_taken_functions_distinct() {
46707844
     if !have_xcrun() {
46717845
         eprintln!("skipping: xcrun unavailable");
46727846
         return;
4673
-    }
4674
-    let Some(sdk) = sdk_path() else {
4675
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4676
-        return;
4677
-    };
4678
-    let Some(sdk_ver) = sdk_version() else {
4679
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
4680
-        return;
46817847
     };
4682
-    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4683
-    if !tbd.exists() {
4684
-        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4685
-        return;
4686
-    }
46877848
 
4688
-    let obj = scratch("data-in-code.o");
4689
-    let our_out = scratch("data-in-code-ours.out");
4690
-    let apple_out = scratch("data-in-code-apple.out");
4691
-    let asm = r#"
4692
-        .text
7849
+    let obj = scratch("icf-adrp-address-taken.o");
7850
+    let baseline_out = scratch("icf-adrp-address-taken-baseline.out");
7851
+    let our_out = scratch("icf-adrp-address-taken-ours.out");
7852
+    let src = r#"
7853
+        .section __TEXT,__text,regular,pure_instructions
46937854
         .globl _main
4694
-        .p2align 2
4695
-    _main:
4696
-        mov w0, #0
4697
-        b Ldispatch
4698
-        .p2align 2
4699
-    Ltable:
4700
-        .data_region jt32
4701
-        .long Lcase0-Ltable
4702
-        .long Lcase1-Ltable
4703
-        .end_data_region
4704
-    Ldispatch:
4705
-        cmp w0, #0
4706
-        b.eq Lcase0
4707
-        b Lcase1
4708
-    Lcase0:
4709
-        mov w0, #1
4710
-        ret
4711
-    Lcase1:
4712
-        mov w0, #2
4713
-        ret
7855
+        _main:
7856
+            stp x29, x30, [sp, #-16]!
7857
+            mov x29, sp
7858
+            adrp x10, _helper1@PAGE
7859
+            add x10, x10, _helper1@PAGEOFF
7860
+            adrp x11, _helper2@PAGE
7861
+            add x11, x11, _helper2@PAGEOFF
7862
+            cmp x10, x11
7863
+            b.ne 1f
7864
+            mov w0, #1
7865
+            ldp x29, x30, [sp], #16
7866
+            ret
7867
+        1:
7868
+            mov w0, #0
7869
+            ldp x29, x30, [sp], #16
7870
+            ret
7871
+
7872
+        .private_extern _helper1
7873
+        _helper1:
7874
+            mov w0, #7
7875
+            ret
7876
+
7877
+        .private_extern _helper2
7878
+        _helper2:
7879
+            mov w0, #7
7880
+            ret
47147881
         .subsections_via_symbols
47157882
     "#;
4716
-    if let Err(e) = assemble(asm, &obj) {
7883
+    if let Err(e) = assemble(src, &obj) {
47177884
         eprintln!("skipping: assemble failed: {e}");
47187885
         return;
47197886
     }
47207887
 
7888
+    let baseline_opts = LinkOptions {
7889
+        inputs: vec![obj.clone()],
7890
+        output: Some(baseline_out.clone()),
7891
+        kind: OutputKind::Executable,
7892
+        ..LinkOptions::default()
7893
+    };
7894
+    Linker::run(&baseline_opts).unwrap();
7895
+
47217896
     let opts = LinkOptions {
4722
-        inputs: vec![obj.clone(), tbd],
7897
+        inputs: vec![obj.clone()],
47237898
         output: Some(our_out.clone()),
47247899
         kind: OutputKind::Executable,
7900
+        icf_mode: afs_ld::IcfMode::Safe,
47257901
         ..LinkOptions::default()
47267902
     };
47277903
     Linker::run(&opts).unwrap();
4728
-    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
47297904
 
7905
+    let baseline_bytes = fs::read(&baseline_out).unwrap();
47307906
     let our_bytes = fs::read(&our_out).unwrap();
4731
-    let apple_bytes = fs::read(&apple_out).unwrap();
4732
-    let our_dic = raw_linkedit_data_cmd(&our_bytes, LC_DATA_IN_CODE);
4733
-    let apple_dic = raw_linkedit_data_cmd(&apple_bytes, LC_DATA_IN_CODE);
4734
-    assert_ne!(our_dic.1, 0);
4735
-    assert_eq!(our_dic.1, apple_dic.1);
4736
-    assert_eq!(decode_data_in_code(&our_bytes).len(), 1);
7907
+    let baseline_symbols = symbol_values(&baseline_bytes);
7908
+    let our_symbols = symbol_values(&our_bytes);
7909
+    let baseline_text = output_section(&baseline_bytes, "__TEXT", "__text")
7910
+        .unwrap()
7911
+        .1;
7912
+    let our_text = output_section(&our_bytes, "__TEXT", "__text").unwrap().1;
7913
+
7914
+    assert_ne!(
7915
+        our_symbols.get("_helper1"),
7916
+        our_symbols.get("_helper2"),
7917
+        "adrp/add address-taken helpers should not be folded by afs-ld -icf=safe"
7918
+    );
7919
+    assert_ne!(
7920
+        baseline_symbols.get("_helper1"),
7921
+        baseline_symbols.get("_helper2"),
7922
+        "baseline link should keep adrp/add address-taken helpers separate"
7923
+    );
47377924
     assert_eq!(
4738
-        canonical_data_in_code(&our_bytes),
4739
-        canonical_data_in_code(&apple_bytes)
7925
+        Command::new(&our_out).status().unwrap().code(),
7926
+        Some(0),
7927
+        "adrp/add address-taken executable should preserve pointer inequality"
47407928
     );
47417929
     assert_eq!(
4742
-        canonical_data_in_code(&our_bytes),
4743
-        vec![DataInCodeRecord {
4744
-            offset: 8,
4745
-            length: 8,
4746
-            kind: DICE_KIND_JUMP_TABLE32,
4747
-        }]
7930
+        our_text.len(),
7931
+        baseline_text.len(),
7932
+        "adrp/add address-taken helpers should not shrink under -icf=safe"
47487933
     );
47497934
 
47507935
     let _ = fs::remove_file(obj);
7936
+    let _ = fs::remove_file(baseline_out);
47517937
     let _ = fs::remove_file(our_out);
4752
-    let _ = fs::remove_file(apple_out);
47537938
 }
47547939
 
47557940
 #[test]
4756
-fn linker_run_remaps_data_in_code_in_later_text_section_like_ld() {
7941
+fn linker_run_icf_safe_folds_matching_branch_relocs() {
47577942
     if !have_xcrun() {
47587943
         eprintln!("skipping: xcrun unavailable");
47597944
         return;
4760
-    }
4761
-    let Some(sdk) = sdk_path() else {
4762
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4763
-        return;
4764
-    };
4765
-    let Some(sdk_ver) = sdk_version() else {
4766
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
4767
-        return;
47687945
     };
4769
-    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4770
-    if !tbd.exists() {
4771
-        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4772
-        return;
4773
-    }
4774
-
4775
-    let obj = scratch("data-in-code-late.o");
4776
-    let our_out = scratch("data-in-code-late-ours.out");
4777
-    let apple_out = scratch("data-in-code-late-apple.out");
4778
-    let asm = r#"
4779
-        .text
4780
-        .globl _main
4781
-        .p2align 2
4782
-    _main:
4783
-        ret
47847946
 
4785
-        .section __TEXT,__text2,regular,pure_instructions
4786
-        .globl _helper
4787
-        .p2align 2
4788
-    _helper:
4789
-        b Ldispatch
4790
-        .p2align 2
4791
-    Ltable:
4792
-        .data_region jt32
4793
-        .long Lcase0-Ltable
4794
-        .long Lcase1-Ltable
4795
-        .end_data_region
4796
-    Ldispatch:
4797
-        ret
4798
-    Lcase0:
4799
-        ret
4800
-    Lcase1:
4801
-        ret
7947
+    let obj = scratch("icf-branch-match.o");
7948
+    let baseline_out = scratch("icf-branch-match-baseline.out");
7949
+    let our_out = scratch("icf-branch-match-ours.out");
7950
+    let src = r#"
7951
+        .section __TEXT,__text,regular,pure_instructions
7952
+        .globl _main
7953
+        _main:
7954
+            stp x29, x30, [sp, #-32]!
7955
+            mov x29, sp
7956
+            bl _wrapper1
7957
+            str w0, [sp, #16]
7958
+            bl _wrapper2
7959
+            ldr w8, [sp, #16]
7960
+            add w0, w8, w0
7961
+            ldp x29, x30, [sp], #32
7962
+            ret
7963
+
7964
+        .private_extern _wrapper1
7965
+        _wrapper1:
7966
+            b _leaf
7967
+
7968
+        .private_extern _wrapper2
7969
+        _wrapper2:
7970
+            b _leaf
7971
+
7972
+        .private_extern _leaf
7973
+        _leaf:
7974
+            mov w0, #5
7975
+            ret
48027976
         .subsections_via_symbols
48037977
     "#;
4804
-    if let Err(e) = assemble(asm, &obj) {
7978
+    if let Err(e) = assemble(src, &obj) {
48057979
         eprintln!("skipping: assemble failed: {e}");
48067980
         return;
48077981
     }
48087982
 
7983
+    let baseline_opts = LinkOptions {
7984
+        inputs: vec![obj.clone()],
7985
+        output: Some(baseline_out.clone()),
7986
+        kind: OutputKind::Executable,
7987
+        ..LinkOptions::default()
7988
+    };
7989
+    Linker::run(&baseline_opts).unwrap();
7990
+
48097991
     let opts = LinkOptions {
4810
-        inputs: vec![obj.clone(), tbd],
7992
+        inputs: vec![obj.clone()],
48117993
         output: Some(our_out.clone()),
48127994
         kind: OutputKind::Executable,
7995
+        icf_mode: afs_ld::IcfMode::Safe,
48137996
         ..LinkOptions::default()
48147997
     };
48157998
     Linker::run(&opts).unwrap();
4816
-    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
48177999
 
8000
+    let baseline_bytes = fs::read(&baseline_out).unwrap();
48188001
     let our_bytes = fs::read(&our_out).unwrap();
4819
-    let apple_bytes = fs::read(&apple_out).unwrap();
8002
+    let baseline_symbols = symbol_values(&baseline_bytes);
8003
+    let our_symbols = symbol_values(&our_bytes);
8004
+    let baseline_text = output_section(&baseline_bytes, "__TEXT", "__text")
8005
+        .unwrap()
8006
+        .1;
8007
+    let our_text = output_section(&our_bytes, "__TEXT", "__text").unwrap().1;
8008
+
8009
+    assert_ne!(
8010
+        baseline_symbols.get("_wrapper1"),
8011
+        baseline_symbols.get("_wrapper2"),
8012
+        "baseline link should keep identical wrappers separate"
8013
+    );
48208014
     assert_eq!(
4821
-        canonical_data_in_code(&our_bytes),
4822
-        canonical_data_in_code(&apple_bytes)
8015
+        our_symbols.get("_wrapper1"),
8016
+        our_symbols.get("_wrapper2"),
8017
+        "matching branch relocations should fold under -icf=safe"
48238018
     );
48248019
     assert_eq!(
4825
-        canonical_data_in_code(&our_bytes),
4826
-        vec![DataInCodeRecord {
4827
-            offset: 8,
4828
-            length: 8,
4829
-            kind: DICE_KIND_JUMP_TABLE32,
4830
-        }]
8020
+        Command::new(&our_out).status().unwrap().code(),
8021
+        Some(10),
8022
+        "folded branch-reloc executable should preserve runtime behavior"
8023
+    );
8024
+    assert!(
8025
+        our_text.len() < baseline_text.len(),
8026
+        "expected matching branch-reloc wrappers to shrink under -icf=safe"
48318027
     );
48328028
 
48338029
     let _ = fs::remove_file(obj);
8030
+    let _ = fs::remove_file(baseline_out);
48348031
     let _ = fs::remove_file(our_out);
4835
-    let _ = fs::remove_file(apple_out);
48368032
 }
48378033
 
48388034
 #[test]
4839
-fn linker_run_remaps_data_in_code_after_large_first_text_section_like_ld() {
8035
+fn linker_run_icf_safe_keeps_distinct_branch_targets_unfolded() {
48408036
     if !have_xcrun() {
48418037
         eprintln!("skipping: xcrun unavailable");
48428038
         return;
4843
-    }
4844
-    let Some(sdk) = sdk_path() else {
4845
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4846
-        return;
4847
-    };
4848
-    let Some(sdk_ver) = sdk_version() else {
4849
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
4850
-        return;
48518039
     };
4852
-    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4853
-    if !tbd.exists() {
4854
-        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4855
-        return;
4856
-    }
48578040
 
4858
-    let obj = scratch("data-in-code-large-first.o");
4859
-    let our_out = scratch("data-in-code-large-first-ours.out");
4860
-    let apple_out = scratch("data-in-code-large-first-apple.out");
4861
-    let asm = r#"
4862
-        .text
8041
+    let obj = scratch("icf-branch-distinct.o");
8042
+    let baseline_out = scratch("icf-branch-distinct-baseline.out");
8043
+    let our_out = scratch("icf-branch-distinct-ours.out");
8044
+    let src = r#"
8045
+        .section __TEXT,__text,regular,pure_instructions
48638046
         .globl _main
4864
-        .p2align 2
4865
-    _main:
4866
-        nop
4867
-        nop
4868
-        nop
4869
-        nop
4870
-        nop
4871
-        ret
8047
+        _main:
8048
+            stp x29, x30, [sp, #-32]!
8049
+            mov x29, sp
8050
+            bl _wrapper1
8051
+            str w0, [sp, #16]
8052
+            bl _wrapper2
8053
+            ldr w8, [sp, #16]
8054
+            add w0, w8, w0
8055
+            ldp x29, x30, [sp], #32
8056
+            ret
48728057
 
4873
-        .section __TEXT,__text2,regular,pure_instructions
4874
-        .globl _helper
4875
-    _helper:
4876
-        b Ldispatch
4877
-        .p2align 2
4878
-    Ltable:
4879
-        .data_region jt32
4880
-        .long Lcase0-Ltable
4881
-        .long Lcase1-Ltable
4882
-        .end_data_region
4883
-    Ldispatch:
4884
-        ret
4885
-    Lcase0:
4886
-        ret
4887
-    Lcase1:
4888
-        ret
8058
+        .private_extern _wrapper1
8059
+        _wrapper1:
8060
+            b _leaf1
8061
+
8062
+        .private_extern _wrapper2
8063
+        _wrapper2:
8064
+            b _leaf2
8065
+
8066
+        .private_extern _leaf1
8067
+        _leaf1:
8068
+            mov w0, #3
8069
+            ret
8070
+
8071
+        .private_extern _leaf2
8072
+        _leaf2:
8073
+            mov w0, #5
8074
+            ret
48898075
         .subsections_via_symbols
48908076
     "#;
4891
-    if let Err(e) = assemble(asm, &obj) {
8077
+    if let Err(e) = assemble(src, &obj) {
48928078
         eprintln!("skipping: assemble failed: {e}");
48938079
         return;
48948080
     }
48958081
 
8082
+    let baseline_opts = LinkOptions {
8083
+        inputs: vec![obj.clone()],
8084
+        output: Some(baseline_out.clone()),
8085
+        kind: OutputKind::Executable,
8086
+        ..LinkOptions::default()
8087
+    };
8088
+    Linker::run(&baseline_opts).unwrap();
8089
+
48968090
     let opts = LinkOptions {
4897
-        inputs: vec![obj.clone(), tbd],
8091
+        inputs: vec![obj.clone()],
48988092
         output: Some(our_out.clone()),
48998093
         kind: OutputKind::Executable,
8094
+        icf_mode: afs_ld::IcfMode::Safe,
49008095
         ..LinkOptions::default()
49018096
     };
49028097
     Linker::run(&opts).unwrap();
4903
-    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
49048098
 
8099
+    let baseline_bytes = fs::read(&baseline_out).unwrap();
49058100
     let our_bytes = fs::read(&our_out).unwrap();
4906
-    let apple_bytes = fs::read(&apple_out).unwrap();
8101
+    let baseline_symbols = symbol_values(&baseline_bytes);
8102
+    let our_symbols = symbol_values(&our_bytes);
8103
+    let baseline_text = output_section(&baseline_bytes, "__TEXT", "__text")
8104
+        .unwrap()
8105
+        .1;
8106
+    let our_text = output_section(&our_bytes, "__TEXT", "__text").unwrap().1;
8107
+
8108
+    assert_ne!(
8109
+        baseline_symbols.get("_wrapper1"),
8110
+        baseline_symbols.get("_wrapper2"),
8111
+        "baseline link should keep distinct wrappers separate"
8112
+    );
8113
+    assert_ne!(
8114
+        our_symbols.get("_wrapper1"),
8115
+        our_symbols.get("_wrapper2"),
8116
+        "wrappers targeting different leaves must not fold under -icf=safe"
8117
+    );
49078118
     assert_eq!(
4908
-        canonical_data_in_code(&our_bytes),
4909
-        canonical_data_in_code(&apple_bytes)
8119
+        Command::new(&our_out).status().unwrap().code(),
8120
+        Some(8),
8121
+        "distinct branch-target executable should preserve runtime behavior"
49108122
     );
49118123
     assert_eq!(
4912
-        canonical_data_in_code(&our_bytes),
4913
-        vec![DataInCodeRecord {
4914
-            offset: 28,
4915
-            length: 8,
4916
-            kind: DICE_KIND_JUMP_TABLE32,
4917
-        }]
8124
+        our_text.len(),
8125
+        baseline_text.len(),
8126
+        "distinct branch-target wrappers should not shrink under -icf=safe"
49188127
     );
49198128
 
49208129
     let _ = fs::remove_file(obj);
8130
+    let _ = fs::remove_file(baseline_out);
49218131
     let _ = fs::remove_file(our_out);
4922
-    let _ = fs::remove_file(apple_out);
49238132
 }
49248133
 
49258134
 #[test]
4926
-fn linker_run_dedups_output_strtab_like_ld() {
8135
+fn linker_run_icf_safe_folds_identical_private_const_data() {
49278136
     if !have_xcrun() {
49288137
         eprintln!("skipping: xcrun unavailable");
49298138
         return;
4930
-    }
4931
-    let Some(sdk) = sdk_path() else {
4932
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
4933
-        return;
4934
-    };
4935
-    let Some(sdk_ver) = sdk_version() else {
4936
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
4937
-        return;
49388139
     };
4939
-    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4940
-    if !tbd.exists() {
4941
-        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4942
-        return;
4943
-    }
49448140
 
4945
-    let obj = scratch("strtab-dedup.o");
4946
-    let our_out = scratch("strtab-dedup-ours.out");
4947
-    let apple_out = scratch("strtab-dedup-apple.out");
4948
-    let mut asm =
4949
-        String::from("        .text\n        .globl _afs_array_sum\n        .globl _main\n");
4950
-    for idx in 0..20 {
4951
-        let symbol = format!("_pad_symbol_{idx:02}");
4952
-        asm.push_str(&format!("        .globl {symbol}\n"));
4953
-    }
4954
-    asm.push_str("        .p2align 2\n");
4955
-    asm.push_str("    _array_sum:\n        ret\n");
4956
-    asm.push_str("    _afs_array_sum:\n        ret\n");
4957
-    for idx in 0..20 {
4958
-        let symbol = format!("_pad_symbol_{idx:02}");
4959
-        asm.push_str(&format!("    {symbol}:\n        ret\n"));
4960
-    }
4961
-    asm.push_str("    _main:\n        bl _afs_array_sum\n        ret\n");
4962
-    asm.push_str("        .subsections_via_symbols\n");
4963
-    if let Err(e) = assemble(&asm, &obj) {
8141
+    let obj = scratch("icf-const-fold.o");
8142
+    let baseline_out = scratch("icf-const-fold-baseline.out");
8143
+    let our_out = scratch("icf-const-fold-ours.out");
8144
+    let src = r#"
8145
+        .section __TEXT,__text,regular,pure_instructions
8146
+        .globl _main
8147
+        _main:
8148
+            mov w0, #0
8149
+            ret
8150
+
8151
+        .section __TEXT,__const
8152
+        .p2align 3
8153
+        .private_extern _const1
8154
+        _const1:
8155
+            .quad 0x1122334455667788
8156
+        .p2align 3
8157
+        .private_extern _const2
8158
+        _const2:
8159
+            .quad 0x1122334455667788
8160
+        .subsections_via_symbols
8161
+    "#;
8162
+    if let Err(e) = assemble(src, &obj) {
49648163
         eprintln!("skipping: assemble failed: {e}");
49658164
         return;
49668165
     }
49678166
 
8167
+    let baseline_opts = LinkOptions {
8168
+        inputs: vec![obj.clone()],
8169
+        output: Some(baseline_out.clone()),
8170
+        kind: OutputKind::Executable,
8171
+        ..LinkOptions::default()
8172
+    };
8173
+    Linker::run(&baseline_opts).unwrap();
8174
+
49688175
     let opts = LinkOptions {
4969
-        inputs: vec![obj.clone(), tbd],
8176
+        inputs: vec![obj.clone()],
49708177
         output: Some(our_out.clone()),
49718178
         kind: OutputKind::Executable,
8179
+        icf_mode: afs_ld::IcfMode::Safe,
49728180
         ..LinkOptions::default()
49738181
     };
49748182
     Linker::run(&opts).unwrap();
4975
-    apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
49768183
 
8184
+    let baseline_bytes = fs::read(&baseline_out).unwrap();
49778185
     let our_bytes = fs::read(&our_out).unwrap();
4978
-    let apple_bytes = fs::read(&apple_out).unwrap();
8186
+    let baseline_symbols = symbol_values(&baseline_bytes);
8187
+    let our_symbols = symbol_values(&our_bytes);
8188
+    let baseline_const = output_section(&baseline_bytes, "__TEXT", "__const")
8189
+        .unwrap()
8190
+        .1;
8191
+    let our_const = output_section(&our_bytes, "__TEXT", "__const").unwrap().1;
8192
+
8193
+    assert_ne!(
8194
+        baseline_symbols.get("_const1"),
8195
+        baseline_symbols.get("_const2"),
8196
+        "baseline link should keep identical private const atoms separate"
8197
+    );
49798198
     assert_eq!(
4980
-        canonical_symbol_records(&our_bytes),
4981
-        canonical_symbol_records(&apple_bytes)
8199
+        our_symbols.get("_const1"),
8200
+        our_symbols.get("_const2"),
8201
+        "expected afs-ld -icf=safe to coalesce identical private const atoms"
49828202
     );
4983
-    let our_strtab = raw_string_table(&our_bytes);
4984
-    let apple_strtab = raw_string_table(&apple_bytes);
4985
-    assert_strtab_within_five_percent(&our_strtab, &apple_strtab);
49868203
     assert!(
4987
-        our_strtab.len() <= apple_strtab.len(),
4988
-        "suffix dedup should not grow the output string table: ours={} apple={}",
4989
-        our_strtab.len(),
4990
-        apple_strtab.len()
8204
+        our_const.len() < baseline_const.len(),
8205
+        "expected -icf=safe to reduce const section size on identical atoms"
49918206
     );
49928207
 
4993
-    let offsets = symbol_name_offsets(&our_bytes);
4994
-    assert_eq!(offsets["_array_sum"], offsets["_afs_array_sum"] + 4);
4995
-
49968208
     let _ = fs::remove_file(obj);
8209
+    let _ = fs::remove_file(baseline_out);
49978210
     let _ = fs::remove_file(our_out);
4998
-    let _ = fs::remove_file(apple_out);
49998211
 }
50008212
 
50018213
 #[test]
5002
-fn linker_run_launches_with_classic_lazy_dylib_import() {
5003
-    if !have_xcrun() || !have_tool("codesign") {
5004
-        eprintln!("skipping: xcrun clang or codesign unavailable");
5005
-        return;
5006
-    }
5007
-    let Some(sdk) = sdk_path() else {
5008
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
8214
+fn linker_run_icf_safe_folds_identical_private_cstrings() {
8215
+    if !have_xcrun() {
8216
+        eprintln!("skipping: xcrun unavailable");
50098217
         return;
50108218
     };
5011
-    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5012
-    if !tbd.exists() {
5013
-        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5014
-        return;
5015
-    }
50168219
 
5017
-    let dylib = scratch("lazy-runtime.dylib");
5018
-    let obj = scratch("lazy-runtime.o");
5019
-    let out = scratch("lazy-runtime.out");
8220
+    let obj = scratch("icf-cstring-fold.o");
8221
+    let baseline_out = scratch("icf-cstring-fold-baseline.out");
8222
+    let our_out = scratch("icf-cstring-fold-ours.out");
8223
+    let src = r#"
8224
+        .section __TEXT,__text,regular,pure_instructions
8225
+        .globl _main
8226
+        _main:
8227
+            mov w0, #0
8228
+            ret
50208229
 
5021
-    let dylib_src = r#"
5022
-        int ext_fn(void) { return 7; }
8230
+        .section __TEXT,__cstring,cstring_literals
8231
+        .private_extern _str1
8232
+        _str1:
8233
+            .asciz "fold me"
8234
+        .private_extern _str2
8235
+        _str2:
8236
+            .asciz "fold me"
8237
+        .subsections_via_symbols
50238238
     "#;
5024
-    if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
5025
-        eprintln!("skipping: dylib compile failed: {e}");
8239
+    if let Err(e) = assemble(src, &obj) {
8240
+        eprintln!("skipping: assemble failed: {e}");
50268241
         return;
50278242
     }
50288243
 
5029
-    let main_src = r#"
5030
-        int ext_fn(void);
5031
-        int main(void) { return ext_fn() == 7 ? 0 : 1; }
5032
-    "#;
5033
-    if let Err(e) = compile_c(main_src, &obj) {
5034
-        eprintln!("skipping: compile failed: {e}");
5035
-        return;
5036
-    }
8244
+    let baseline_opts = LinkOptions {
8245
+        inputs: vec![obj.clone()],
8246
+        output: Some(baseline_out.clone()),
8247
+        kind: OutputKind::Executable,
8248
+        ..LinkOptions::default()
8249
+    };
8250
+    Linker::run(&baseline_opts).unwrap();
50378251
 
50388252
     let opts = LinkOptions {
5039
-        inputs: vec![obj.clone(), tbd, dylib.clone()],
5040
-        output: Some(out.clone()),
8253
+        inputs: vec![obj.clone()],
8254
+        output: Some(our_out.clone()),
50418255
         kind: OutputKind::Executable,
8256
+        icf_mode: afs_ld::IcfMode::Safe,
50428257
         ..LinkOptions::default()
50438258
     };
50448259
     Linker::run(&opts).unwrap();
50458260
 
5046
-    let verify = Command::new("codesign")
5047
-        .arg("-v")
5048
-        .arg(&out)
5049
-        .output()
5050
-        .unwrap();
5051
-    assert!(
5052
-        verify.status.success(),
5053
-        "codesign verify failed: {}",
5054
-        String::from_utf8_lossy(&verify.stderr)
8261
+    let baseline_bytes = fs::read(&baseline_out).unwrap();
8262
+    let our_bytes = fs::read(&our_out).unwrap();
8263
+    let baseline_symbols = symbol_values(&baseline_bytes);
8264
+    let our_symbols = symbol_values(&our_bytes);
8265
+    let baseline_cstrings = output_section(&baseline_bytes, "__TEXT", "__cstring")
8266
+        .unwrap()
8267
+        .1;
8268
+    let our_cstrings = output_section(&our_bytes, "__TEXT", "__cstring").unwrap().1;
8269
+
8270
+    assert_ne!(
8271
+        baseline_symbols.get("_str1"),
8272
+        baseline_symbols.get("_str2"),
8273
+        "baseline link should keep identical private cstrings separate"
50558274
     );
5056
-    let status = Command::new(&out).status().unwrap();
50578275
     assert_eq!(
5058
-        status.code(),
5059
-        Some(0),
5060
-        "expected dylib-import executable to exit 0"
8276
+        our_symbols.get("_str1"),
8277
+        our_symbols.get("_str2"),
8278
+        "expected afs-ld -icf=safe to coalesce identical private cstrings"
8279
+    );
8280
+    assert!(
8281
+        our_cstrings.len() < baseline_cstrings.len(),
8282
+        "expected -icf=safe to reduce cstring section size on identical literals"
50618283
     );
50628284
 
5063
-    let _ = fs::remove_file(dylib);
50648285
     let _ = fs::remove_file(obj);
5065
-    let _ = fs::remove_file(out);
8286
+    let _ = fs::remove_file(baseline_out);
8287
+    let _ = fs::remove_file(our_out);
50668288
 }
50678289
 
5068
-#[test]
5069
-fn linker_run_handles_local_tlv_descriptors() {
5070
-    if !have_xcrun() || !have_tool("codesign") {
5071
-        eprintln!("skipping: xcrun clang or codesign unavailable");
5072
-        return;
5073
-    }
5074
-    let Some(sdk) = sdk_path() else {
5075
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
8290
+#[test]
8291
+fn linker_run_icf_safe_folds_identical_private_literal16() {
8292
+    if !have_xcrun() {
8293
+        eprintln!("skipping: xcrun unavailable");
50768294
         return;
50778295
     };
5078
-    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5079
-    if !tbd.exists() {
5080
-        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5081
-        return;
5082
-    }
50838296
 
5084
-    let obj = scratch("tlvp-local.o");
5085
-    let out = scratch("tlvp-local.out");
8297
+    let obj = scratch("icf-literal16-fold.o");
8298
+    let baseline_out = scratch("icf-literal16-fold-baseline.out");
8299
+    let our_out = scratch("icf-literal16-fold-ours.out");
50868300
     let src = r#"
5087
-        __thread long tls_a = 7;
5088
-        __thread long tls_b;
5089
-
5090
-        static long tls_sum(void) {
5091
-            return tls_a + tls_b;
5092
-        }
8301
+        .section __TEXT,__text,regular,pure_instructions
8302
+        .globl _main
8303
+        _main:
8304
+            mov w0, #0
8305
+            ret
50938306
 
5094
-        int main(void) {
5095
-            return tls_sum() == 7 ? 0 : 1;
5096
-        }
8307
+        .section __TEXT,__literal16,16byte_literals
8308
+        .private_extern _lit1
8309
+        _lit1:
8310
+            .quad 0x1122334455667788
8311
+            .quad 0x99aabbccddeeff00
8312
+        .private_extern _lit2
8313
+        _lit2:
8314
+            .quad 0x1122334455667788
8315
+            .quad 0x99aabbccddeeff00
8316
+        .subsections_via_symbols
50978317
     "#;
5098
-    if let Err(e) = compile_c(src, &obj) {
5099
-        eprintln!("skipping: compile failed: {e}");
8318
+    if let Err(e) = assemble(src, &obj) {
8319
+        eprintln!("skipping: assemble failed: {e}");
51008320
         return;
51018321
     }
51028322
 
8323
+    let baseline_opts = LinkOptions {
8324
+        inputs: vec![obj.clone()],
8325
+        output: Some(baseline_out.clone()),
8326
+        kind: OutputKind::Executable,
8327
+        ..LinkOptions::default()
8328
+    };
8329
+    Linker::run(&baseline_opts).unwrap();
8330
+
51038331
     let opts = LinkOptions {
5104
-        inputs: vec![obj.clone(), tbd],
5105
-        output: Some(out.clone()),
8332
+        inputs: vec![obj.clone()],
8333
+        output: Some(our_out.clone()),
51068334
         kind: OutputKind::Executable,
8335
+        icf_mode: afs_ld::IcfMode::Safe,
51078336
         ..LinkOptions::default()
51088337
     };
51098338
     Linker::run(&opts).unwrap();
51108339
 
5111
-    let bytes = fs::read(&out).unwrap();
5112
-    let (_, thread_vars) = output_section(&bytes, "__DATA", "__thread_vars").unwrap();
5113
-    let (_, thread_data) = output_section(&bytes, "__DATA", "__thread_data").unwrap();
5114
-    assert!(output_section(&bytes, "__DATA", "__thread_ptrs").is_none());
5115
-    assert_eq!(thread_vars.len(), 48);
5116
-    assert_eq!(thread_data.len(), 8);
5117
-    assert_eq!(
5118
-        u64::from_le_bytes(thread_vars[16..24].try_into().unwrap()),
5119
-        0
8340
+    let baseline_bytes = fs::read(&baseline_out).unwrap();
8341
+    let our_bytes = fs::read(&our_out).unwrap();
8342
+    let baseline_symbols = symbol_values(&baseline_bytes);
8343
+    let our_symbols = symbol_values(&our_bytes);
8344
+    let baseline_literals = output_section(&baseline_bytes, "__TEXT", "__literal16")
8345
+        .unwrap()
8346
+        .1;
8347
+    let our_literals = output_section(&our_bytes, "__TEXT", "__literal16")
8348
+        .unwrap()
8349
+        .1;
8350
+
8351
+    assert_ne!(
8352
+        baseline_symbols.get("_lit1"),
8353
+        baseline_symbols.get("_lit2"),
8354
+        "baseline link should keep identical private literal16 atoms separate"
51208355
     );
51218356
     assert_eq!(
5122
-        u64::from_le_bytes(thread_vars[40..48].try_into().unwrap()),
5123
-        8
8357
+        our_symbols.get("_lit1"),
8358
+        our_symbols.get("_lit2"),
8359
+        "expected afs-ld -icf=safe to coalesce identical private literal16 atoms"
51248360
     );
5125
-
5126
-    let binds = decode_bind_records(&bytes, false).unwrap();
5127
-    let mut tlv_binds: Vec<_> = binds
5128
-        .into_iter()
5129
-        .filter(|record| record.section == "__thread_vars" && record.symbol == "__tlv_bootstrap")
5130
-        .collect();
5131
-    tlv_binds.sort_by_key(|record| record.section_offset);
5132
-    assert_eq!(tlv_binds.len(), 2);
5133
-    assert_eq!(tlv_binds[0].section_offset, 0);
5134
-    assert_eq!(tlv_binds[1].section_offset, 24);
5135
-
5136
-    let header = parse_header(&bytes).unwrap();
5137
-    let commands = parse_commands(&header, &bytes).unwrap();
5138
-    let symtab = commands
5139
-        .iter()
5140
-        .find_map(|cmd| match cmd {
5141
-            LoadCommand::Symtab(cmd) => Some(*cmd),
5142
-            _ => None,
5143
-        })
5144
-        .unwrap();
5145
-    let symbols = parse_nlist_table(&bytes, symtab.symoff, symtab.nsyms).unwrap();
5146
-    let strings = StringTable::from_file(&bytes, symtab.stroff, symtab.strsize).unwrap();
5147
-    let symbol_names: Vec<&str> = symbols
5148
-        .iter()
5149
-        .map(|symbol| strings.get(symbol.strx()).unwrap())
5150
-        .collect();
5151
-    assert!(symbol_names.contains(&"__tlv_bootstrap"));
5152
-
5153
-    let verify = Command::new("codesign")
5154
-        .arg("-v")
5155
-        .arg(&out)
5156
-        .output()
5157
-        .unwrap();
51588361
     assert!(
5159
-        verify.status.success(),
5160
-        "codesign verify failed: {}",
5161
-        String::from_utf8_lossy(&verify.stderr)
8362
+        our_literals.len() < baseline_literals.len(),
8363
+        "expected -icf=safe to reduce literal16 section size on identical atoms"
51628364
     );
5163
-    let status = Command::new(&out).status().unwrap();
5164
-    assert_eq!(status.code(), Some(0), "expected TLV executable to exit 0");
51658365
 
51668366
     let _ = fs::remove_file(obj);
5167
-    let _ = fs::remove_file(out);
8367
+    let _ = fs::remove_file(baseline_out);
8368
+    let _ = fs::remove_file(our_out);
51688369
 }
51698370
 
51708371
 #[test]
5171
-fn linker_run_routes_imported_tlv_through_got() {
5172
-    if !have_xcrun() || !have_tool("codesign") {
5173
-        eprintln!("skipping: xcrun clang or codesign unavailable");
5174
-        return;
5175
-    }
5176
-    let Some(sdk) = sdk_path() else {
5177
-        eprintln!("skipping: xcrun --show-sdk-path unavailable");
5178
-        return;
5179
-    };
5180
-    let Some(sdk_ver) = sdk_version() else {
5181
-        eprintln!("skipping: xcrun --show-sdk-version unavailable");
8372
+fn linker_run_icf_safe_folds_identical_private_data_const_atoms() {
8373
+    if !have_xcrun() {
8374
+        eprintln!("skipping: xcrun unavailable");
51828375
         return;
51838376
     };
5184
-    let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5185
-    if !tbd.exists() {
5186
-        eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5187
-        return;
5188
-    }
51898377
 
5190
-    let dylib = scratch("libtlvprobe.dylib");
5191
-    let obj = scratch("imported-tlv.o");
5192
-    let our_out = scratch("imported-tlv-ours.out");
5193
-    let apple_out = scratch("imported-tlv-apple.out");
8378
+    let obj = scratch("icf-data-const-fold.o");
8379
+    let baseline_out = scratch("icf-data-const-fold-baseline.out");
8380
+    let our_out = scratch("icf-data-const-fold-ours.out");
8381
+    let src = r#"
8382
+        .section __TEXT,__text,regular,pure_instructions
8383
+        .globl _main
8384
+        _main:
8385
+            mov w0, #0
8386
+            ret
51948387
 
5195
-    let dylib_src = r#"
5196
-        __thread long ext_tls = 5;
5197
-        long read_lib_tls(void) { return ext_tls; }
8388
+        .section __DATA_CONST,__const
8389
+        .p2align 3
8390
+        .private_extern _const1
8391
+        _const1:
8392
+            .quad 0x0123456789abcdef
8393
+        .p2align 3
8394
+        .private_extern _const2
8395
+        _const2:
8396
+            .quad 0x0123456789abcdef
8397
+        .subsections_via_symbols
51988398
     "#;
5199
-    if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
5200
-        eprintln!("skipping: dylib compile failed: {e}");
8399
+    if let Err(e) = assemble(src, &obj) {
8400
+        eprintln!("skipping: assemble failed: {e}");
52018401
         return;
52028402
     }
52038403
 
5204
-    let main_src = r#"
5205
-        extern __thread long ext_tls;
5206
-        int main(void) { return ext_tls == 5 ? 0 : 1; }
5207
-    "#;
5208
-    if let Err(e) = compile_c(main_src, &obj) {
5209
-        eprintln!("skipping: compile failed: {e}");
5210
-        return;
5211
-    }
8404
+    let baseline_opts = LinkOptions {
8405
+        inputs: vec![obj.clone()],
8406
+        output: Some(baseline_out.clone()),
8407
+        kind: OutputKind::Executable,
8408
+        ..LinkOptions::default()
8409
+    };
8410
+    Linker::run(&baseline_opts).unwrap();
52128411
 
52138412
     let opts = LinkOptions {
5214
-        inputs: vec![obj.clone(), tbd.clone(), dylib.clone()],
8413
+        inputs: vec![obj.clone()],
52158414
         output: Some(our_out.clone()),
52168415
         kind: OutputKind::Executable,
8416
+        icf_mode: afs_ld::IcfMode::Safe,
52178417
         ..LinkOptions::default()
52188418
     };
52198419
     Linker::run(&opts).unwrap();
52208420
 
5221
-    let apple = Command::new("xcrun")
5222
-        .args([
5223
-            "ld",
5224
-            "-arch",
5225
-            "arm64",
5226
-            "-platform_version",
5227
-            "macos",
5228
-            &sdk_ver,
5229
-            &sdk_ver,
5230
-            "-syslibroot",
5231
-            &sdk,
5232
-            "-no_fixup_chains",
5233
-            "-lSystem",
5234
-            "-e",
5235
-            "_main",
5236
-            "-o",
5237
-        ])
5238
-        .arg(&apple_out)
5239
-        .arg(&obj)
5240
-        .arg(&dylib)
5241
-        .output()
5242
-        .unwrap();
5243
-    assert!(
5244
-        apple.status.success(),
5245
-        "xcrun ld failed: {}",
5246
-        String::from_utf8_lossy(&apple.stderr)
5247
-    );
5248
-
8421
+    let baseline_bytes = fs::read(&baseline_out).unwrap();
52498422
     let our_bytes = fs::read(&our_out).unwrap();
5250
-    let apple_bytes = fs::read(&apple_out).unwrap();
5251
-    let (our_text_addr, our_text) = output_section(&our_bytes, "__TEXT", "__text").unwrap();
5252
-    let (apple_text_addr, apple_text) = output_section(&apple_bytes, "__TEXT", "__text").unwrap();
5253
-    let (our_got_addr, our_got) = output_section(&our_bytes, "__DATA_CONST", "__got").unwrap();
5254
-    let (apple_got_addr, apple_got) =
5255
-        output_section(&apple_bytes, "__DATA_CONST", "__got").unwrap();
8423
+    let baseline_symbols = symbol_values(&baseline_bytes);
8424
+    let our_symbols = symbol_values(&our_bytes);
8425
+    let baseline_const = output_section(&baseline_bytes, "__DATA_CONST", "__const")
8426
+        .unwrap()
8427
+        .1;
8428
+    let our_const = output_section(&our_bytes, "__DATA_CONST", "__const")
8429
+        .unwrap()
8430
+        .1;
52568431
 
5257
-    assert!(output_section(&our_bytes, "__DATA", "__thread_ptrs").is_none());
5258
-    assert!(output_section(&apple_bytes, "__DATA", "__thread_ptrs").is_none());
5259
-    assert_eq!(our_got.len(), 8);
5260
-    assert_eq!(our_got, apple_got);
5261
-    assert_eq!(
5262
-        decode_page_reference(&our_text, our_text_addr, 20, &PageRefKind::Load).unwrap(),
5263
-        our_got_addr
5264
-    );
5265
-    assert_eq!(
5266
-        decode_page_reference(&apple_text, apple_text_addr, 20, &PageRefKind::Load).unwrap(),
5267
-        apple_got_addr
5268
-    );
5269
-    assert_eq!(our_text, apple_text);
5270
-    assert_eq!(read_insn(&our_text, 24).unwrap(), 0xf9400000);
5271
-    assert_eq!(read_insn(&our_text, 28).unwrap(), 0xf9400008);
5272
-    assert_eq!(read_insn(&our_text, 32).unwrap(), 0xd63f0100);
5273
-    assert_eq!(
5274
-        decode_bind_records(&our_bytes, false).unwrap(),
5275
-        decode_bind_records(&apple_bytes, false).unwrap()
8432
+    assert_ne!(
8433
+        baseline_symbols.get("_const1"),
8434
+        baseline_symbols.get("_const2"),
8435
+        "baseline link should keep identical private __DATA_CONST atoms separate"
52768436
     );
52778437
     assert_eq!(
5278
-        load_dylib_names(&our_bytes).unwrap(),
5279
-        load_dylib_names(&apple_bytes).unwrap()
8438
+        our_symbols.get("_const1"),
8439
+        our_symbols.get("_const2"),
8440
+        "expected afs-ld -icf=safe to coalesce identical private __DATA_CONST atoms"
52808441
     );
5281
-    let verify = Command::new("codesign")
5282
-        .arg("-v")
5283
-        .arg(&our_out)
5284
-        .output()
5285
-        .unwrap();
52868442
     assert!(
5287
-        verify.status.success(),
5288
-        "codesign verify failed: {}",
5289
-        String::from_utf8_lossy(&verify.stderr)
5290
-    );
5291
-    let status = Command::new(&our_out).status().unwrap();
5292
-    assert_eq!(
5293
-        status.code(),
5294
-        Some(0),
5295
-        "expected imported TLV executable to exit 0"
8443
+        our_const.len() < baseline_const.len(),
8444
+        "expected -icf=safe to reduce __DATA_CONST,__const size on identical atoms"
52968445
     );
52978446
 
5298
-    let _ = fs::remove_file(dylib);
52998447
     let _ = fs::remove_file(obj);
8448
+    let _ = fs::remove_file(baseline_out);
53008449
     let _ = fs::remove_file(our_out);
5301
-    let _ = fs::remove_file(apple_out);
53028450
 }
53038451
 
53048452
 #[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");
8453
+fn linker_run_icf_safe_reaches_fixed_point_through_folded_targets() {
8454
+    if !have_xcrun() {
8455
+        eprintln!("skipping: xcrun unavailable");
53208456
         return;
53218457
     };
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
-    }
53278458
 
5328
-    let obj = scratch("runtime-hello.o");
5329
-    let out = scratch("runtime-hello.out");
5330
-    let apple_out = scratch("runtime-hello-apple.out");
8459
+    let obj = scratch("icf-fixed-point.o");
8460
+    let baseline_out = scratch("icf-fixed-point-baseline.out");
8461
+    let our_out = scratch("icf-fixed-point-ours.out");
53318462
     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);
8463
+        .section __TEXT,__text,regular,pure_instructions
8464
+        .globl _main
8465
+        _main:
8466
+            stp x29, x30, [sp, #-32]!
8467
+            mov x29, sp
8468
+            bl _wrapper1
8469
+            str w0, [sp, #16]
8470
+            bl _wrapper2
8471
+            ldr w8, [sp, #16]
8472
+            add w0, w8, w0
8473
+            ldp x29, x30, [sp], #32
8474
+            ret
8475
+
8476
+        .private_extern _wrapper1
8477
+        _wrapper1:
8478
+            b _leaf1
8479
+
8480
+        .private_extern _wrapper2
8481
+        _wrapper2:
8482
+            b _leaf2
8483
+
8484
+        .private_extern _leaf1
8485
+        _leaf1:
8486
+            mov w0, #6
8487
+            ret
53368488
 
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
-        }
8489
+        .private_extern _leaf2
8490
+        _leaf2:
8491
+            mov w0, #6
8492
+            ret
8493
+        .subsections_via_symbols
53448494
     "#;
5345
-    if let Err(e) = compile_c(src, &obj) {
5346
-        eprintln!("skipping: compile failed: {e}");
8495
+    if let Err(e) = assemble(src, &obj) {
8496
+        eprintln!("skipping: assemble failed: {e}");
53478497
         return;
53488498
     }
53498499
 
8500
+    let baseline_opts = LinkOptions {
8501
+        inputs: vec![obj.clone()],
8502
+        output: Some(baseline_out.clone()),
8503
+        kind: OutputKind::Executable,
8504
+        ..LinkOptions::default()
8505
+    };
8506
+    Linker::run(&baseline_opts).unwrap();
8507
+
53508508
     let opts = LinkOptions {
5351
-        inputs: vec![obj.clone(), runtime.clone(), tbd],
5352
-        output: Some(out.clone()),
8509
+        inputs: vec![obj.clone()],
8510
+        output: Some(our_out.clone()),
53538511
         kind: OutputKind::Executable,
8512
+        icf_mode: afs_ld::IcfMode::Safe,
53548513
         ..LinkOptions::default()
53558514
     };
53568515
     Linker::run(&opts).unwrap();
53578516
 
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)
8517
+    let baseline_bytes = fs::read(&baseline_out).unwrap();
8518
+    let our_bytes = fs::read(&our_out).unwrap();
8519
+    let baseline_symbols = symbol_values(&baseline_bytes);
8520
+    let our_symbols = symbol_values(&our_bytes);
8521
+    let baseline_text = output_section(&baseline_bytes, "__TEXT", "__text")
54128522
         .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"));
8523
+        .1;
8524
+    let our_text = output_section(&our_bytes, "__TEXT", "__text").unwrap().1;
54248525
 
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"
8526
+    assert_ne!(
8527
+        baseline_symbols.get("_leaf1"),
8528
+        baseline_symbols.get("_leaf2"),
8529
+        "baseline link should keep equivalent leaves separate"
54348530
     );
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"
8531
+    assert_ne!(
8532
+        baseline_symbols.get("_wrapper1"),
8533
+        baseline_symbols.get("_wrapper2"),
8534
+        "baseline link should keep wrappers separate"
54398535
     );
54408536
     assert_eq!(
5441
-        indirect_symbol_identities(&bytes),
5442
-        indirect_symbol_identities(&apple_bytes),
5443
-        "runtime hello indirect symbol identities diverged from Apple ld"
8537
+        our_symbols.get("_leaf1"),
8538
+        our_symbols.get("_leaf2"),
8539
+        "equivalent leaves should fold under -icf=safe"
54448540
     );
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();
54678541
     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)
8542
+        our_symbols.get("_wrapper1"),
8543
+        our_symbols.get("_wrapper2"),
8544
+        "wrappers should fold once their targets converge to the same winner"
54728545
     );
54738546
     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)
8547
+        Command::new(&our_out).status().unwrap().code(),
8548
+        Some(12),
8549
+        "fixed-point folded executable should preserve runtime behavior"
54788550
     );
5479
-    assert_eq!(
5480
-        String::from_utf8_lossy(&output.stdout),
5481
-        String::from_utf8_lossy(&apple_output.stdout)
8551
+    assert!(
8552
+        our_text.len() < baseline_text.len(),
8553
+        "expected fixed-point folding to reduce text size"
54828554
     );
54838555
 
54848556
     let _ = fs::remove_file(obj);
5485
-    let _ = fs::remove_file(out);
5486
-    let _ = fs::remove_file(apple_out);
8557
+    let _ = fs::remove_file(baseline_out);
8558
+    let _ = fs::remove_file(our_out);
54878559
 }
54888560
 
54898561
 #[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");
8562
+fn linker_run_icf_safe_prefers_earlier_input_order_winner() {
8563
+    if !have_xcrun() {
8564
+        eprintln!("skipping: xcrun unavailable");
55018565
         return;
55028566
     };
5503
-    let Some(sdk_ver) = sdk_version() else {
5504
-        eprintln!("skipping: no macOS SDK version");
8567
+
8568
+    let main_obj = scratch("icf-order-main.o");
8569
+    let first_obj = scratch("icf-order-first.o");
8570
+    let second_obj = scratch("icf-order-second.o");
8571
+    let our_out = scratch("icf-order-ours.out");
8572
+    let map = scratch("icf-order.map");
8573
+    let main_src = r#"
8574
+        .section __TEXT,__text,regular,pure_instructions
8575
+        .globl _main
8576
+        _main:
8577
+            stp x29, x30, [sp, #-32]!
8578
+            mov x29, sp
8579
+            bl _helper_a
8580
+            str w0, [sp, #16]
8581
+            bl _helper_b
8582
+            ldr w8, [sp, #16]
8583
+            add w0, w8, w0
8584
+            ldp x29, x30, [sp], #32
8585
+            ret
8586
+        .subsections_via_symbols
8587
+    "#;
8588
+    let helper_a_src = r#"
8589
+        .section __TEXT,__text,regular,pure_instructions
8590
+        .private_extern _helper_a
8591
+        .globl _helper_a
8592
+        _helper_a:
8593
+            mov w0, #4
8594
+            ret
8595
+        .subsections_via_symbols
8596
+    "#;
8597
+    let helper_b_src = r#"
8598
+        .section __TEXT,__text,regular,pure_instructions
8599
+        .private_extern _helper_b
8600
+        .globl _helper_b
8601
+        _helper_b:
8602
+            mov w0, #4
8603
+            ret
8604
+        .subsections_via_symbols
8605
+    "#;
8606
+    if let Err(e) = assemble(main_src, &main_obj) {
8607
+        eprintln!("skipping: assemble failed: {e}");
55058608
         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());
8609
+    }
8610
+    if let Err(e) = assemble(helper_a_src, &first_obj) {
8611
+        eprintln!("skipping: assemble failed: {e}");
8612
+        let _ = fs::remove_file(main_obj);
55108613
         return;
55118614
     }
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}");
8615
+    if let Err(e) = assemble(helper_b_src, &second_obj) {
8616
+        eprintln!("skipping: assemble failed: {e}");
8617
+        let _ = fs::remove_file(main_obj);
8618
+        let _ = fs::remove_file(first_obj);
55268619
         return;
55278620
     }
55288621
 
55298622
     let opts = LinkOptions {
5530
-        inputs: vec![obj.clone(), runtime.clone(), tbd],
8623
+        inputs: vec![main_obj.clone(), first_obj.clone(), second_obj.clone()],
55318624
         output: Some(our_out.clone()),
8625
+        map: Some(map.clone()),
55328626
         kind: OutputKind::Executable,
8627
+        icf_mode: afs_ld::IcfMode::Safe,
55338628
         ..LinkOptions::default()
55348629
     };
55358630
     Linker::run(&opts).unwrap();
55368631
 
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
-
55658632
     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();
8633
+    let our_symbols = symbol_values(&our_bytes);
8634
+    let map_text = fs::read_to_string(&map).unwrap();
8635
+
55698636
     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"
8637
+        our_symbols.get("_helper_a"),
8638
+        our_symbols.get("_helper_b"),
8639
+        "equivalent helpers should fold to the same winner"
8640
+    );
8641
+    assert!(
8642
+        map_text.contains("_helper_b folded to _helper_a"),
8643
+        "earlier input should win safe-ICF ties:\n{map_text}"
55798644
     );
55808645
     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"
8646
+        Command::new(&our_out).status().unwrap().code(),
8647
+        Some(8),
8648
+        "input-order folded executable should preserve runtime behavior"
55908649
     );
55918650
 
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);
8651
+    let _ = fs::remove_file(main_obj);
8652
+    let _ = fs::remove_file(first_obj);
8653
+    let _ = fs::remove_file(second_obj);
55988654
     let _ = fs::remove_file(our_out);
5599
-    let _ = fs::remove_file(apple_out);
8655
+    let _ = fs::remove_file(map);
56008656
 }
tests/parity_canary.rsadded
43 lines changed — click to load
@@ -0,0 +1,43 @@
1
+//! Intentional-regression guardrails for Sprint 27.
2
+
3
+mod common;
4
+
5
+use std::path::PathBuf;
6
+
7
+use common::harness::{
8
+    diff_macho, have_xcrun, have_xcrun_tool, link_both, load_corpus, output_section,
9
+};
10
+
11
+#[test]
12
+fn mutated_text_byte_is_not_tolerated() {
13
+    if !have_xcrun() || !have_xcrun_tool("ld") {
14
+        eprintln!("skipping: xcrun as/ld unavailable");
15
+        return;
16
+    }
17
+
18
+    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
19
+        .join("tests")
20
+        .join("parity_corpus");
21
+    let case = load_corpus(&root)
22
+        .expect("load parity corpus")
23
+        .into_iter()
24
+        .find(|case| case.name == "hello_classic")
25
+        .expect("hello_classic parity case");
26
+
27
+    let outputs = link_both(&case).expect("link hello_classic with both linkers");
28
+    let (_, mut our_text) =
29
+        output_section(&outputs.ours, "__TEXT", "__text").expect("afs-ld __TEXT,__text");
30
+    let (_, their_text) =
31
+        output_section(&outputs.theirs, "__TEXT", "__text").expect("Apple __TEXT,__text");
32
+
33
+    our_text[0] ^= 0x1;
34
+    let report = diff_macho(&our_text, &their_text);
35
+    assert!(
36
+        !report.is_clean(),
37
+        "mutated text byte should be reported as critical: {report:#?}"
38
+    );
39
+    assert!(
40
+        !report.critical.is_empty(),
41
+        "expected at least one critical diff after mutation: {report:#?}"
42
+    );
43
+}
tests/parity_corpus/archive_order_exec/args.txtadded
17 lines changed — click to load
@@ -0,0 +1,17 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
16
+@ARTIFACT:liba.a@
17
+@ARTIFACT:libb.a@
tests/parity_corpus/archive_order_exec/artifacts.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+clang_archive a.c liba.a
2
+clang_archive b.c libb.a
tests/parity_corpus/archive_order_exec/command_checks.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+build_version
2
+load_dylib_names
tests/parity_corpus/archive_order_exec/inputs/a.cadded
5 lines changed — click to load
@@ -0,0 +1,5 @@
1
+int mid(void);
2
+
3
+int top(void) {
4
+    return mid();
5
+}
tests/parity_corpus/archive_order_exec/inputs/b.cadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+int mid(void) {
2
+    return 17;
3
+}
tests/parity_corpus/archive_order_exec/inputs/main.cadded
5 lines changed — click to load
@@ -0,0 +1,5 @@
1
+int top(void);
2
+
3
+int main(void) {
4
+    return top() == 17 ? 0 : 1;
5
+}
tests/parity_corpus/archive_order_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Multi-archive resolution parity case. `main` pulls `top` from the first archive,
2
+and that object in turn requires `mid` from the second archive, so archive
3
+fetch order must match Apple `ld`.
tests/parity_corpus/archive_order_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/archive_order_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text
tests/parity_corpus/backtrace_metadata_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/backtrace_metadata_exec/command_checks.txtadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+build_version
2
+load_dylib_names
3
+rebased_unwind_bytes
4
+normalized_function_starts
tests/parity_corpus/backtrace_metadata_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/backtrace_metadata_exec/inputs/main.cadded
18 lines changed — click to load
@@ -0,0 +1,18 @@
1
+#include <unwind.h>
2
+
3
+static _Unwind_Reason_Code cb(struct _Unwind_Context* ctx, void* arg) {
4
+    (void)ctx;
5
+    int* count = (int*)arg;
6
+    (*count)++;
7
+    return *count >= 8 ? _URC_END_OF_STACK : _URC_NO_REASON;
8
+}
9
+
10
+__attribute__((noinline)) int helper(void) {
11
+    int count = 0;
12
+    _Unwind_Backtrace(cb, &count);
13
+    return count;
14
+}
15
+
16
+int main(void) {
17
+    return helper() > 1 ? 0 : 1;
18
+}
tests/parity_corpus/backtrace_metadata_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Backtrace-metadata parity case lifted from the existing Apple `ld` probe. This
2
+keeps the Sprint 27 corpus honest about unwind bytes and normalized function
3
+starts for a real `_Unwind_Backtrace` executable.
tests/parity_corpus/backtrace_metadata_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/classic_lazy_batched_got_calls/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/classic_lazy_batched_got_calls/command_checks.txtadded
6 lines changed — click to load
@@ -0,0 +1,6 @@
1
+build_version
2
+load_dylib_names
3
+dyld_info_rebase
4
+dyld_info_bind
5
+dyld_info_weak_bind
6
+dyld_info_lazy_bind
tests/parity_corpus/classic_lazy_batched_got_calls/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/classic_lazy_batched_got_calls/inputs/main.sadded
14 lines changed — click to load
@@ -0,0 +1,14 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _main
3
+        _main:
4
+            adrp x0, _write@GOTPAGE
5
+            ldr x0, [x0, _write@GOTPAGEOFF]
6
+            bl _write
7
+            adrp x1, _close@GOTPAGE
8
+            ldr x1, [x1, _close@GOTPAGEOFF]
9
+            bl _close
10
+            adrp x2, _read@GOTPAGE
11
+            ldr x2, [x2, _read@GOTPAGEOFF]
12
+            bl _read
13
+            ret
14
+        .subsections_via_symbols
tests/parity_corpus/classic_lazy_batched_got_calls/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Classic lazy-binding multi-import parity case lifted from the existing
2
+`linker_run` matrix. This extends the Sprint 27 corpus around multiple distinct
3
+lazy imports in one executable while keeping the Apple-parity dyld-info and
4
+stub-surface checks in the dedicated harness.
tests/parity_corpus/classic_lazy_batched_got_calls/sections.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+__TEXT __stubs
2
+__TEXT __stub_helper
tests/parity_corpus/classic_lazy_branch_calls/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/classic_lazy_branch_calls/command_checks.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+build_version
2
+load_dylib_names
tests/parity_corpus/classic_lazy_branch_calls/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/classic_lazy_branch_calls/inputs/main.sadded
8 lines changed — click to load
@@ -0,0 +1,8 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _main
3
+_main:
4
+    bl _write
5
+    bl _close
6
+    bl _read
7
+    ret
8
+.subsections_via_symbols
tests/parity_corpus/classic_lazy_branch_calls/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Classic lazy-binding executable parity case pulled from the existing
2
+`linker_run` matrix. This verifies the dedicated Sprint 27 harness can compare
3
+import-stub surfaces and dylib load-command contents, not just minimal hello
4
+worlds.
tests/parity_corpus/classic_lazy_branch_calls/sections.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+__TEXT __stubs
2
+__TEXT __stub_helper
tests/parity_corpus/classic_lazy_branch_only_calls/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/classic_lazy_branch_only_calls/command_checks.txtadded
6 lines changed — click to load
@@ -0,0 +1,6 @@
1
+build_version
2
+load_dylib_names
3
+dyld_info_rebase
4
+dyld_info_bind
5
+dyld_info_weak_bind
6
+dyld_info_lazy_bind
tests/parity_corpus/classic_lazy_branch_only_calls/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/classic_lazy_branch_only_calls/inputs/main.sadded
8 lines changed — click to load
@@ -0,0 +1,8 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _main
3
+        _main:
4
+            bl _write
5
+            bl _close
6
+            bl _read
7
+            ret
8
+        .subsections_via_symbols
tests/parity_corpus/classic_lazy_branch_only_calls/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Classic lazy-binding branch-only parity case lifted from the existing
2
+`linker_run` matrix. This rounds out the Sprint 27 classic-lazy corpus by
3
+covering pure branch-driven imports without explicit GOT loads, while keeping
4
+the Apple-parity dyld-info and stub-surface checks in the dedicated harness.
tests/parity_corpus/classic_lazy_branch_only_calls/sections.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+__TEXT __stubs
2
+__TEXT __stub_helper
tests/parity_corpus/classic_lazy_deduped_import/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/classic_lazy_deduped_import/command_checks.txtadded
6 lines changed — click to load
@@ -0,0 +1,6 @@
1
+build_version
2
+load_dylib_names
3
+dyld_info_rebase
4
+dyld_info_bind
5
+dyld_info_weak_bind
6
+dyld_info_lazy_bind
tests/parity_corpus/classic_lazy_deduped_import/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/classic_lazy_deduped_import/inputs/main.sadded
11 lines changed — click to load
@@ -0,0 +1,11 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _main
3
+        _main:
4
+            adrp x0, _write@GOTPAGE
5
+            ldr x0, [x0, _write@GOTPAGEOFF]
6
+            bl _write
7
+            bl _write
8
+            adrp x1, _write@GOTPAGE
9
+            ldr x1, [x1, _write@GOTPAGEOFF]
10
+            ret
11
+        .subsections_via_symbols
tests/parity_corpus/classic_lazy_deduped_import/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Classic lazy-binding dedupe parity case lifted from the existing `linker_run`
2
+matrix. This strengthens the Sprint 27 corpus around repeated imports of the
3
+same symbol, while still comparing the Apple-parity dyld-info streams and stub
4
+surfaces instead of only command IDs.
tests/parity_corpus/classic_lazy_deduped_import/sections.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+__TEXT __stubs
2
+__TEXT __stub_helper
tests/parity_corpus/common_symbol_promotion_exec/args.txtadded
17 lines changed — click to load
@@ -0,0 +1,17 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
16
+@INPUT:a.o@
17
+@INPUT:b.o@
tests/parity_corpus/common_symbol_promotion_exec/command_checks.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+build_version
2
+load_dylib_names
tests/parity_corpus/common_symbol_promotion_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_SEGMENT_64
tests/parity_corpus/common_symbol_promotion_exec/inputs/a.sadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+.comm _shared,8,3
tests/parity_corpus/common_symbol_promotion_exec/inputs/b.sadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+.comm _shared,8,3
tests/parity_corpus/common_symbol_promotion_exec/inputs/main.sadded
6 lines changed — click to load
@@ -0,0 +1,6 @@
1
+.text
2
+.globl _main
3
+.p2align 2
4
+_main:
5
+    mov w0, #0
6
+    ret
tests/parity_corpus/common_symbol_promotion_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Common-symbol promotion parity case. Two objects contribute the same `.comm`
2
+symbol, and the linked executable should promote that storage into the final
3
+image and record it the same way Apple `ld` does.
tests/parity_corpus/common_symbol_promotion_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/common_symbol_promotion_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text
tests/parity_corpus/data_in_code_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/data_in_code_exec/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+load_dylib_names
3
+data_in_code
tests/parity_corpus/data_in_code_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/data_in_code_exec/inputs/main.sadded
23 lines changed — click to load
@@ -0,0 +1,23 @@
1
+.text
2
+.globl _main
3
+.p2align 2
4
+_main:
5
+    mov w0, #0
6
+    b Ldispatch
7
+    .p2align 2
8
+Ltable:
9
+    .data_region jt32
10
+    .long Lcase0-Ltable
11
+    .long Lcase1-Ltable
12
+    .end_data_region
13
+Ldispatch:
14
+    cmp w0, #0
15
+    b.eq Lcase0
16
+    b Lcase1
17
+Lcase0:
18
+    mov w0, #1
19
+    ret
20
+Lcase1:
21
+    mov w0, #2
22
+    ret
23
+.subsections_via_symbols
tests/parity_corpus/data_in_code_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Data-in-code parity case lifted from the existing Apple `ld` probe. This keeps
2
+the Sprint 27 corpus honest about remapping a jump-table record in the primary
3
+text section.
tests/parity_corpus/data_in_code_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/data_in_code_large_first_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/data_in_code_large_first_exec/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+load_dylib_names
3
+data_in_code
tests/parity_corpus/data_in_code_large_first_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/data_in_code_large_first_exec/inputs/main.sadded
28 lines changed — click to load
@@ -0,0 +1,28 @@
1
+.text
2
+.globl _main
3
+.p2align 2
4
+_main:
5
+    nop
6
+    nop
7
+    nop
8
+    nop
9
+    nop
10
+    ret
11
+
12
+.section __TEXT,__text2,regular,pure_instructions
13
+.globl _helper
14
+_helper:
15
+    b Ldispatch
16
+    .p2align 2
17
+Ltable:
18
+    .data_region jt32
19
+    .long Lcase0-Ltable
20
+    .long Lcase1-Ltable
21
+    .end_data_region
22
+Ldispatch:
23
+    ret
24
+Lcase0:
25
+    ret
26
+Lcase1:
27
+    ret
28
+.subsections_via_symbols
tests/parity_corpus/data_in_code_large_first_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Data-in-code parity case lifted from the existing Apple `ld` probe with a large
2
+first text section. This keeps the Sprint 27 corpus honest about remapping
3
+later jump-table records after earlier text growth.
tests/parity_corpus/data_in_code_large_first_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/data_in_code_late_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/data_in_code_late_exec/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+load_dylib_names
3
+data_in_code
tests/parity_corpus/data_in_code_late_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/data_in_code_late_exec/inputs/main.sadded
24 lines changed — click to load
@@ -0,0 +1,24 @@
1
+.text
2
+.globl _main
3
+.p2align 2
4
+_main:
5
+    ret
6
+
7
+.section __TEXT,__text2,regular,pure_instructions
8
+.globl _helper
9
+.p2align 2
10
+_helper:
11
+    b Ldispatch
12
+    .p2align 2
13
+Ltable:
14
+    .data_region jt32
15
+    .long Lcase0-Ltable
16
+    .long Lcase1-Ltable
17
+    .end_data_region
18
+Ldispatch:
19
+    ret
20
+Lcase0:
21
+    ret
22
+Lcase1:
23
+    ret
24
+.subsections_via_symbols
tests/parity_corpus/data_in_code_late_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Data-in-code parity case lifted from the existing Apple `ld` probe for a later
2
+text section. This keeps the Sprint 27 corpus honest about remapping jump-table
3
+records outside the primary text section.
tests/parity_corpus/data_in_code_late_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/dead_strip_import_exec/absent_sections.txtadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+__TEXT __stubs
2
+__TEXT __stub_helper
3
+__DATA __la_symbol_ptr
4
+__DATA_CONST __got
tests/parity_corpus/dead_strip_import_exec/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-dead_strip
13
+-no_fixup_chains
14
+-o
15
+@OUT@
16
+@INPUT:main.o@
tests/parity_corpus/dead_strip_import_exec/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+load_dylib_names
3
+dyld_info_bind
tests/parity_corpus/dead_strip_import_exec/inputs/main.sadded
12 lines changed — click to load
@@ -0,0 +1,12 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _main
3
+_main:
4
+    mov w0, #0
5
+    ret
6
+
7
+.globl _unused
8
+_unused:
9
+    bl _puts
10
+    mov w0, #0
11
+    ret
12
+.subsections_via_symbols
tests/parity_corpus/dead_strip_import_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Dead-strip synthetic-import parity case lifted from the Sprint 23 closeout
2
+work. This keeps the Sprint 27 corpus honest about pruning unused synthetic
3
+import sections when the importing atom is dead-stripped.
tests/parity_corpus/dead_strip_import_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/dead_strip_import_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/direct_bind_data/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-no_fixup_chains
10
+-e
11
+_main
12
+-o
13
+@OUT@
14
+@INPUT:main.o@
15
+@SDK_TBD:usr/lib/libSystem.tbd@
16
+@ARTIFACT:libdirect.dylib@
tests/parity_corpus/direct_bind_data/artifacts.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+clang_dylib libdirect.c libdirect.dylib
tests/parity_corpus/direct_bind_data/command_checks.txtadded
6 lines changed — click to load
@@ -0,0 +1,6 @@
1
+build_version
2
+load_dylib_names
3
+dyld_info_rebase
4
+dyld_info_bind
5
+dyld_info_weak_bind
6
+dyld_info_lazy_bind
tests/parity_corpus/direct_bind_data/inputs/libdirect.cadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+int ext_data = 5;
tests/parity_corpus/direct_bind_data/inputs/main.cadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+extern int ext_data;
2
+int *p = &ext_data;
3
+int main(void) { return *p == 5 ? 0 : 1; }
tests/parity_corpus/direct_bind_data/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Direct-bind executable parity case lifted from the standalone data-import probe.
2
+This keeps the Sprint 27 corpus honest about direct imported-data pointers even
3
+when there is no paired imported function call.
tests/parity_corpus/direct_bind_data/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/direct_bind_data/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/direct_bind_deduped/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-no_fixup_chains
10
+-e
11
+_main
12
+-o
13
+@OUT@
14
+@INPUT:main.o@
15
+@SDK_TBD:usr/lib/libSystem.tbd@
16
+@ARTIFACT:libdirect.dylib@
tests/parity_corpus/direct_bind_deduped/artifacts.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+clang_dylib libdirect.c libdirect.dylib
tests/parity_corpus/direct_bind_deduped/command_checks.txtadded
6 lines changed — click to load
@@ -0,0 +1,6 @@
1
+build_version
2
+load_dylib_names
3
+dyld_info_rebase
4
+dyld_info_bind
5
+dyld_info_weak_bind
6
+dyld_info_lazy_bind
tests/parity_corpus/direct_bind_deduped/inputs/libdirect.cadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+int ext_data = 5;
tests/parity_corpus/direct_bind_deduped/inputs/main.cadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+extern int ext_data;
2
+int *p = &ext_data;
3
+int *q = &ext_data;
4
+int main(void) { return (*p == 5 && *q == 5) ? 0 : 1; }
tests/parity_corpus/direct_bind_deduped/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Direct-bind executable parity case lifted from the direct-bind fixture matrix.
2
+This keeps the Sprint 27 corpus honest about repeated direct references to the
3
+same imported data symbol.
tests/parity_corpus/direct_bind_deduped/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/direct_bind_deduped/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/direct_bind_mixed/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-no_fixup_chains
10
+-e
11
+_main
12
+-o
13
+@OUT@
14
+@INPUT:main.o@
15
+@SDK_TBD:usr/lib/libSystem.tbd@
16
+@ARTIFACT:libdirect.dylib@
tests/parity_corpus/direct_bind_mixed/artifacts.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+clang_dylib libdirect.c libdirect.dylib
tests/parity_corpus/direct_bind_mixed/command_checks.txtadded
6 lines changed — click to load
@@ -0,0 +1,6 @@
1
+build_version
2
+load_dylib_names
3
+dyld_info_rebase
4
+dyld_info_bind
5
+dyld_info_weak_bind
6
+dyld_info_lazy_bind
tests/parity_corpus/direct_bind_mixed/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/direct_bind_mixed/inputs/libdirect.cadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+int ext_data = 5;
2
+int ext_fn(void) { return ext_data + 1; }
tests/parity_corpus/direct_bind_mixed/inputs/main.cadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+extern int ext_data;
2
+extern int ext_fn(void);
3
+int *p = &ext_data;
4
+int main(void) { return *p + ext_fn() == 11 ? 0 : 1; }
tests/parity_corpus/direct_bind_mixed/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Direct-bind executable parity case lifted from the existing fixture matrix.
2
+This extends the Sprint 27 corpus to handle compiled dylib side artifacts and
3
+checks the classic dyld-info streams Apple `ld` emits for direct imports.
tests/parity_corpus/direct_bind_mixed/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/direct_bind_mixed/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/direct_bind_multi_data/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-no_fixup_chains
10
+-e
11
+_main
12
+-o
13
+@OUT@
14
+@INPUT:main.o@
15
+@SDK_TBD:usr/lib/libSystem.tbd@
16
+@ARTIFACT:libdirect.dylib@
tests/parity_corpus/direct_bind_multi_data/artifacts.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+clang_dylib libdirect.c libdirect.dylib
tests/parity_corpus/direct_bind_multi_data/command_checks.txtadded
6 lines changed — click to load
@@ -0,0 +1,6 @@
1
+build_version
2
+load_dylib_names
3
+dyld_info_rebase
4
+dyld_info_bind
5
+dyld_info_weak_bind
6
+dyld_info_lazy_bind
tests/parity_corpus/direct_bind_multi_data/inputs/libdirect.cadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+int ext_data = 5;
2
+int more_data = 9;
tests/parity_corpus/direct_bind_multi_data/inputs/main.cadded
5 lines changed — click to load
@@ -0,0 +1,5 @@
1
+extern int ext_data;
2
+extern int more_data;
3
+int *p = &ext_data;
4
+int *q = &more_data;
5
+int main(void) { return (*p == 5 && *q == 9) ? 0 : 1; }
tests/parity_corpus/direct_bind_multi_data/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Direct-bind executable parity case lifted from the direct-bind fixture matrix.
2
+This keeps the Sprint 27 corpus honest about multiple imported data pointers in
3
+one executable.
tests/parity_corpus/direct_bind_multi_data/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/direct_bind_multi_data/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_bss_dylib/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-dylib
2
+-arch
3
+arm64
4
+-platform_version
5
+macos
6
+@SDK_VERSION@
7
+@SDK_VERSION@
8
+-syslibroot
9
+@SDK_PATH@
10
+-lSystem
11
+-install_name
12
+@rpath/export-bss.dylib
13
+-no_fixup_chains
14
+-o
15
+@OUT@
16
+@INPUT:main.o@
tests/parity_corpus/export_bss_dylib/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+export_records
3
+symbol_record_map
tests/parity_corpus/export_bss_dylib/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_bss_dylib/inputs/main.sadded
6 lines changed — click to load
@@ -0,0 +1,6 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _touch
3
+        _touch:
4
+            ret
5
+        .zerofill __DATA,__bss,_global_bss,16,3
6
+        .subsections_via_symbols
tests/parity_corpus/export_bss_dylib/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Exported-BSS dylib parity case lifted from the existing `linker_run` export
2
+matrix. This broadens the Sprint 27 corpus into zerofill/export-trie behavior,
3
+so the dedicated parity harness checks Apple agreement when an exported symbol
4
+lives in `__DATA,__bss` instead of only text or initialized data.
tests/parity_corpus/export_bss_dylib/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_filter_dylib/args.txtadded
20 lines changed — click to load
@@ -0,0 +1,20 @@
1
+-dylib
2
+-arch
3
+arm64
4
+-platform_version
5
+macos
6
+@SDK_VERSION@
7
+@SDK_VERSION@
8
+-syslibroot
9
+@SDK_PATH@
10
+-lSystem
11
+-install_name
12
+@rpath/export-filter.dylib
13
+-no_fixup_chains
14
+-exported_symbol
15
+_alpha
16
+-exported_symbols_list
17
+@FILE:exports.txt@
18
+-o
19
+@OUT@
20
+@INPUT:main.o@
tests/parity_corpus/export_filter_dylib/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+export_records
3
+symbol_record_map
tests/parity_corpus/export_filter_dylib/files/exports.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+_bet?
tests/parity_corpus/export_filter_dylib/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_filter_dylib/inputs/main.sadded
11 lines changed — click to load
@@ -0,0 +1,11 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _alpha
3
+.globl _beta
4
+.globl _gamma
5
+_alpha:
6
+    ret
7
+_beta:
8
+    ret
9
+_gamma:
10
+    ret
11
+.subsections_via_symbols
tests/parity_corpus/export_filter_dylib/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Export-filter dylib parity case lifted from the existing `linker_run` coverage.
2
+This extends the Sprint 27 corpus to handle sidecar list files and verifies
3
+that exported-symbol filtering matches Apple `ld` in both the export trie and
4
+the final symtab surface.
tests/parity_corpus/export_filter_dylib/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_ordering_dylib/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-dylib
2
+-arch
3
+arm64
4
+-platform_version
5
+macos
6
+@SDK_VERSION@
7
+@SDK_VERSION@
8
+-syslibroot
9
+@SDK_PATH@
10
+-lSystem
11
+-install_name
12
+@rpath/export-ordering.dylib
13
+-no_fixup_chains
14
+-o
15
+@OUT@
16
+@INPUT:main.o@
tests/parity_corpus/export_ordering_dylib/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+export_records
3
+symbol_record_map
tests/parity_corpus/export_ordering_dylib/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_ordering_dylib/inputs/main.sadded
11 lines changed — click to load
@@ -0,0 +1,11 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _zeta
3
+        _zeta:
4
+            ret
5
+        .globl _alpha
6
+        _alpha:
7
+            ret
8
+        .globl _middle
9
+        _middle:
10
+            ret
11
+        .subsections_via_symbols
tests/parity_corpus/export_ordering_dylib/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Export-ordering dylib parity case lifted from the existing `linker_run`
2
+export matrix. This gives the Sprint 27 corpus a direct check that export trie
3
+and final symtab ordering semantics stay in Apple-parity agreement even when
4
+symbols are declared out of lexical order.
tests/parity_corpus/export_ordering_dylib/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_prefix_fanout_dylib/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-dylib
2
+-arch
3
+arm64
4
+-platform_version
5
+macos
6
+@SDK_VERSION@
7
+@SDK_VERSION@
8
+-syslibroot
9
+@SDK_PATH@
10
+-lSystem
11
+-install_name
12
+@rpath/export-prefix-fanout.dylib
13
+-no_fixup_chains
14
+-o
15
+@OUT@
16
+@INPUT:main.o@
tests/parity_corpus/export_prefix_fanout_dylib/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+export_records
3
+symbol_record_map
tests/parity_corpus/export_prefix_fanout_dylib/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_prefix_fanout_dylib/inputs/main.sadded
14 lines changed — click to load
@@ -0,0 +1,14 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _pre
3
+        _pre:
4
+            ret
5
+        .globl _prefix
6
+        _prefix:
7
+            ret
8
+        .globl _prefix_long
9
+        _prefix_long:
10
+            ret
11
+        .globl _prefix_lone
12
+        _prefix_lone:
13
+            ret
14
+        .subsections_via_symbols
tests/parity_corpus/export_prefix_fanout_dylib/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Shared-prefix dylib export parity case lifted from the existing `linker_run`
2
+export matrix. This gives the Sprint 27 corpus a real export-trie fanout shape
3
+where multiple exported symbols share a common prefix but diverge later,
4
+without relying on export filtering to create the prefix structure.
tests/parity_corpus/export_prefix_fanout_dylib/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_shared_data_prefix_dylib/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-dylib
2
+-arch
3
+arm64
4
+-platform_version
5
+macos
6
+@SDK_VERSION@
7
+@SDK_VERSION@
8
+-syslibroot
9
+@SDK_PATH@
10
+-lSystem
11
+-install_name
12
+@rpath/export-shared-data-prefix.dylib
13
+-no_fixup_chains
14
+-o
15
+@OUT@
16
+@INPUT:main.o@
tests/parity_corpus/export_shared_data_prefix_dylib/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+export_records
3
+symbol_record_map
tests/parity_corpus/export_shared_data_prefix_dylib/inputs/main.sadded
12 lines changed — click to load
@@ -0,0 +1,12 @@
1
+        .section __DATA,__data
2
+        .p2align 3
3
+        .globl _alpha_data
4
+        _alpha_data:
5
+            .quad 1
6
+        .globl _alphabet_data
7
+        _alphabet_data:
8
+            .quad 2
9
+        .globl _alphanumeric_data
10
+        _alphanumeric_data:
11
+            .quad 3
12
+        .subsections_via_symbols
tests/parity_corpus/export_shared_data_prefix_dylib/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Shared-prefix data-only dylib export parity case lifted from the existing
2
+`linker_run` export matrix. This closes the remaining obvious export-matrix gap
3
+by checking Apple parity when all exported symbols live in `__DATA,__data` and
4
+share a long common prefix.
tests/parity_corpus/export_shared_data_prefix_dylib/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_text_const_dylib/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-dylib
2
+-arch
3
+arm64
4
+-platform_version
5
+macos
6
+@SDK_VERSION@
7
+@SDK_VERSION@
8
+-syslibroot
9
+@SDK_PATH@
10
+-lSystem
11
+-install_name
12
+@rpath/export-text-const.dylib
13
+-no_fixup_chains
14
+-o
15
+@OUT@
16
+@INPUT:main.o@
tests/parity_corpus/export_text_const_dylib/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+export_records
3
+symbol_record_map
tests/parity_corpus/export_text_const_dylib/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_text_const_dylib/inputs/main.sadded
10 lines changed — click to load
@@ -0,0 +1,10 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _entry
3
+        _entry:
4
+            ret
5
+        .section __TEXT,__const
6
+        .p2align 3
7
+        .globl _ro_value
8
+        _ro_value:
9
+            .quad 0xfeedface
10
+        .subsections_via_symbols
tests/parity_corpus/export_text_const_dylib/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Mixed text-plus-const dylib export parity case lifted from the existing
2
+`linker_run` export matrix. This expands the Sprint 27 corpus to cover export
3
+surfaces where one exported symbol lives in `__TEXT,__const` rather than only
4
+code, initialized data, or BSS.
tests/parity_corpus/export_text_const_dylib/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_text_data_dylib/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-dylib
2
+-arch
3
+arm64
4
+-platform_version
5
+macos
6
+@SDK_VERSION@
7
+@SDK_VERSION@
8
+-syslibroot
9
+@SDK_PATH@
10
+-lSystem
11
+-install_name
12
+@rpath/export-text-data.dylib
13
+-no_fixup_chains
14
+-o
15
+@OUT@
16
+@INPUT:main.o@
tests/parity_corpus/export_text_data_dylib/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+export_records
3
+symbol_record_map
tests/parity_corpus/export_text_data_dylib/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/export_text_data_dylib/inputs/main.sadded
13 lines changed — click to load
@@ -0,0 +1,13 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _code_symbol
3
+        _code_symbol:
4
+            ret
5
+        .section __DATA,__data
6
+        .p2align 3
7
+        .globl _data_symbol
8
+        _data_symbol:
9
+            .quad 0x1234
10
+        .globl _more_data
11
+        _more_data:
12
+            .long 7
13
+        .subsections_via_symbols
tests/parity_corpus/export_text_data_dylib/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Mixed text-plus-data dylib export parity case lifted from the existing
2
+`linker_run` export matrix. This broadens the Sprint 27 corpus beyond
3
+text-only dylibs and checks that both the export trie and final symtab stay in
4
+Apple-parity agreement when exported symbols live in different sections.
tests/parity_corpus/export_text_data_dylib/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/function_starts_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/function_starts_exec/command_checks.txtadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+build_version
2
+load_dylib_names
3
+normalized_function_starts
4
+data_in_code
tests/parity_corpus/function_starts_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/function_starts_exec/inputs/main.sadded
9 lines changed — click to load
@@ -0,0 +1,9 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _main
3
+.p2align 2
4
+_main:
5
+    adrp x0, _write@GOTPAGE
6
+    ldr x0, [x0, _write@GOTPAGEOFF]
7
+    bl _write
8
+    ret
9
+.subsections_via_symbols
tests/parity_corpus/function_starts_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Function-starts parity case lifted from the existing Apple `ld` probe. This
2
+keeps the Sprint 27 corpus honest about `LC_FUNCTION_STARTS` and the adjacent
3
+data-in-code payload for a classic lazy-link executable.
tests/parity_corpus/function_starts_exec/sections.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+__TEXT __stubs
2
+__TEXT __stub_helper
tests/parity_corpus/function_starts_textcoal_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/function_starts_textcoal_exec/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+load_dylib_names
3
+normalized_function_starts
tests/parity_corpus/function_starts_textcoal_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/function_starts_textcoal_exec/inputs/main.sadded
12 lines changed — click to load
@@ -0,0 +1,12 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _main
3
+.p2align 2
4
+_main:
5
+    ret
6
+
7
+.section __TEXT,__textcoal_nt,regular,pure_instructions
8
+.globl _helper
9
+.p2align 2
10
+_helper:
11
+    ret
12
+.subsections_via_symbols
tests/parity_corpus/function_starts_textcoal_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Function-starts parity case lifted from the existing Apple `ld` probe for a
2
+second text section. This keeps the Sprint 27 corpus honest about recording
3
+starts outside the primary `__TEXT,__text` section too.
tests/parity_corpus/function_starts_textcoal_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/hello_classic/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/hello_classic/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/hello_classic/inputs/main.sadded
6 lines changed — click to load
@@ -0,0 +1,6 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _main
3
+_main:
4
+    mov x0, #0
5
+    ret
6
+.subsections_via_symbols
tests/parity_corpus/hello_classic/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Classic executable hello-world seed case for the Sprint 27 differential
2
+matrix. This starts the reusable on-disk corpus with the simplest runnable
3
+shape we already expect to match Apple `ld`.
tests/parity_corpus/hello_classic/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/hello_classic/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text
tests/parity_corpus/hello_dead_strip/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-dead_strip
14
+-o
15
+@OUT@
16
+@INPUT:main.o@
tests/parity_corpus/hello_dead_strip/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/hello_dead_strip/inputs/main.sadded
6 lines changed — click to load
@@ -0,0 +1,6 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _main
3
+_main:
4
+    mov x0, #0
5
+    ret
6
+.subsections_via_symbols
tests/parity_corpus/hello_dead_strip/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Dead-strip executable seed case for the Sprint 27 differential matrix. This
2
+exercises the new corpus path on a real post-Sprint-23 behavior knob without
3
+dragging in a large multi-object fixture yet.
tests/parity_corpus/hello_dead_strip/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/hello_dead_strip/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text
tests/parity_corpus/hidden_got_exec/absent_sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__DATA_CONST __got
tests/parity_corpus/hidden_got_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/hidden_got_exec/command_checks.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+build_version
2
+load_dylib_names
tests/parity_corpus/hidden_got_exec/inputs/main.sadded
14 lines changed — click to load
@@ -0,0 +1,14 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _main
3
+_main:
4
+    adrp x8, _value@GOTPAGE
5
+    ldr x8, [x8, _value@GOTPAGEOFF]
6
+    ldr w0, [x8]
7
+    ret
8
+
9
+.private_extern _value
10
+.section __DATA,__data
11
+.p2align 2
12
+_value:
13
+    .long 7
14
+.subsections_via_symbols
tests/parity_corpus/hidden_got_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Hidden-GOT relaxation parity case lifted from the existing Apple `ld` probe.
2
+This keeps the Sprint 27 corpus honest about relaxing hidden GOT references to
3
+direct page references and omitting the final `__got` section.
tests/parity_corpus/hidden_got_exec/page_refs.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text 0 add _value
tests/parity_corpus/hidden_got_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/hidden_got_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text
tests/parity_corpus/imported_tlv_exec/absent_sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__DATA __thread_ptrs
tests/parity_corpus/imported_tlv_exec/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-no_fixup_chains
10
+-lSystem
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
16
+@ARTIFACT:libtlvprobe.dylib@
tests/parity_corpus/imported_tlv_exec/artifacts.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+clang_dylib libtlvprobe.c libtlvprobe.dylib
tests/parity_corpus/imported_tlv_exec/command_checks.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+build_version
tests/parity_corpus/imported_tlv_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/imported_tlv_exec/inputs/libtlvprobe.cadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+__thread long ext_tls = 5;
2
+long read_lib_tls(void) { return ext_tls; }
tests/parity_corpus/imported_tlv_exec/inputs/main.cadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+extern __thread long ext_tls;
2
+int main(void) { return ext_tls == 5 ? 0 : 1; }
tests/parity_corpus/imported_tlv_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Imported TLV executable parity case lifted from the existing Apple `ld` probe.
2
+This keeps the Sprint 27 corpus honest about routing imported TLV access
3
+through the GOT while matching Apple in text bytes, GOT contents, and runtime.
tests/parity_corpus/imported_tlv_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/imported_tlv_exec/sections.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+__TEXT __text
2
+__DATA_CONST __got
tests/parity_corpus/local_got_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/local_got_exec/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+load_dylib_names
3
+dyld_info_bind
tests/parity_corpus/local_got_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_SEGMENT_64
tests/parity_corpus/local_got_exec/inputs/main.sadded
14 lines changed — click to load
@@ -0,0 +1,14 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _main
3
+_main:
4
+    adrp x8, _value@GOTPAGE
5
+    ldr x8, [x8, _value@GOTPAGEOFF]
6
+    ldr w0, [x8]
7
+    ret
8
+
9
+.section __DATA,__data
10
+.globl _value
11
+.p2align 2
12
+_value:
13
+    .long 7
14
+.subsections_via_symbols
tests/parity_corpus/local_got_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Local GOT executable parity case lifted from the existing Apple `ld` probe.
2
+This keeps the Sprint 27 corpus honest about classic local-GOT routing through
3
+rebased slots while preserving runtime behavior.
tests/parity_corpus/local_got_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/local_got_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/local_rebase_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/local_rebase_exec/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+load_dylib_names
3
+dyld_info_rebase
tests/parity_corpus/local_rebase_exec/inputs/main.cadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+int ext = 7;
2
+int *p = &ext;
3
+int main(void) { return *p == 7 ? 0 : 1; }
tests/parity_corpus/local_rebase_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Local absolute-pointer executable parity case lifted from the existing Apple
2
+`ld` probe. This keeps the Sprint 27 corpus honest about local pointer rebases
3
+and the resulting runtime behavior.
tests/parity_corpus/local_rebase_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/local_rebase_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/loh_adrp_add_dylib/absent_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LINKER_OPTIMIZATION_HINT
tests/parity_corpus/loh_adrp_add_dylib/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-dylib
2
+-arch
3
+arm64
4
+-platform_version
5
+macos
6
+@SDK_VERSION@
7
+@SDK_VERSION@
8
+-syslibroot
9
+@SDK_PATH@
10
+-lSystem
11
+-install_name
12
+@rpath/liblohprobe.dylib
13
+-no_fixup_chains
14
+-o
15
+@OUT@
16
+@INPUT:main.o@
tests/parity_corpus/loh_adrp_add_dylib/command_checks.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+build_version
tests/parity_corpus/loh_adrp_add_dylib/inputs/main.sadded
13 lines changed — click to load
@@ -0,0 +1,13 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _loh_probe
3
+.globl _target
4
+_loh_probe:
5
+Lloh0:
6
+    adrp x0, _target@PAGE
7
+Lloh1:
8
+    add x0, x0, _target@PAGEOFF
9
+    ret
10
+_target:
11
+    ret
12
+.loh AdrpAdd Lloh0, Lloh1
13
+.subsections_via_symbols
tests/parity_corpus/loh_adrp_add_dylib/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+LOH Apple-parity dylib case lifted from the Sprint 25 parity probes. This
2
+keeps the dedicated matrix honest about the current Apple `ld` behavior for
3
+dylib outputs too: preserve `ADRP+ADD` text and omit
4
+`LC_LINKER_OPTIMIZATION_HINT`.
tests/parity_corpus/loh_adrp_add_dylib/page_refs.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text 0 add _target
tests/parity_corpus/loh_adrp_add_dylib/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/loh_adrp_add_exec/absent_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LINKER_OPTIMIZATION_HINT
tests/parity_corpus/loh_adrp_add_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/loh_adrp_add_exec/command_checks.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+build_version
tests/parity_corpus/loh_adrp_add_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/loh_adrp_add_exec/inputs/main.sadded
14 lines changed — click to load
@@ -0,0 +1,14 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _main
3
+.globl _target
4
+_main:
5
+Lloh0:
6
+    adrp x0, _target@PAGE
7
+Lloh1:
8
+    add x0, x0, _target@PAGEOFF
9
+    mov w0, #0
10
+    ret
11
+_target:
12
+    ret
13
+.loh AdrpAdd Lloh0, Lloh1
14
+.subsections_via_symbols
tests/parity_corpus/loh_adrp_add_exec/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+LOH Apple-parity executable case pulled from the Sprint 25 parity probes. This
2
+keeps the dedicated matrix honest about the agreed Apple `ld` direction:
3
+preserve `ADRP+ADD` text and omit `LC_LINKER_OPTIMIZATION_HINT` from the final
4
+binary.
tests/parity_corpus/loh_adrp_add_exec/page_refs.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text 0 add _target
tests/parity_corpus/loh_adrp_add_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/loh_adrp_add_exec/sections.txtadded
0 lines changed — click to load
tests/parity_corpus/loh_adrp_ldr_exec/absent_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LINKER_OPTIMIZATION_HINT
tests/parity_corpus/loh_adrp_ldr_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/loh_adrp_ldr_exec/command_checks.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+build_version
tests/parity_corpus/loh_adrp_ldr_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/loh_adrp_ldr_exec/inputs/main.sadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _main
3
+.globl _target
4
+_main:
5
+Lloh0:
6
+    adrp x0, _target@PAGE
7
+Lloh1:
8
+    ldr x1, [x0, _target@PAGEOFF]
9
+    mov w0, #0
10
+    ret
11
+    .p2align 3
12
+_target:
13
+    .quad 0x1122334455667788
14
+.loh AdrpLdr Lloh0, Lloh1
15
+.subsections_via_symbols
tests/parity_corpus/loh_adrp_ldr_exec/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+LOH Apple-parity executable case lifted from the Sprint 25 parity probes.
2
+This keeps the dedicated matrix honest about the current Apple `ld` behavior:
3
+preserve `ADRP+LDR` text and omit `LC_LINKER_OPTIMIZATION_HINT` from the final
4
+binary.
tests/parity_corpus/loh_adrp_ldr_exec/page_refs.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text 0 load _target
tests/parity_corpus/loh_adrp_ldr_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/loh_adrp_ldr_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/loh_adrp_ldr_got_ldr_exec/absent_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LINKER_OPTIMIZATION_HINT
tests/parity_corpus/loh_adrp_ldr_got_ldr_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/loh_adrp_ldr_got_ldr_exec/command_checks.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+build_version
tests/parity_corpus/loh_adrp_ldr_got_ldr_exec/ignored_load_commands.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+LC_SEGMENT_64
2
+LC_LOAD_DYLIB
tests/parity_corpus/loh_adrp_ldr_got_ldr_exec/inputs/main.sadded
18 lines changed — click to load
@@ -0,0 +1,18 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _main
3
+.globl _value
4
+_main:
5
+Lloh0:
6
+    adrp x8, _value@GOTPAGE
7
+Lloh1:
8
+    ldr x8, [x8, _value@GOTPAGEOFF]
9
+Lloh2:
10
+    ldr w0, [x8]
11
+    ret
12
+
13
+.section __DATA,__data
14
+.p2align 2
15
+_value:
16
+    .long 7
17
+.loh AdrpLdrGotLdr Lloh0, Lloh1, Lloh2
18
+.subsections_via_symbols
tests/parity_corpus/loh_adrp_ldr_got_ldr_exec/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+LOH Apple-parity executable case lifted from the Sprint 25 parity probes.
2
+This keeps the dedicated matrix honest about the agreed Apple direction for
3
+local GOT-resolved `AdrpLdrGotLdr`: preserve the resolved `ADRP+ADD+LDR` shape
4
+and omit `LC_LINKER_OPTIMIZATION_HINT`.
tests/parity_corpus/loh_adrp_ldr_got_ldr_exec/page_refs.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text 0 add _value
tests/parity_corpus/loh_adrp_ldr_got_ldr_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/loh_adrp_ldr_got_ldr_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/reexport_chain_exec/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
16
+@ARTIFACT:libmid.dylib@
tests/parity_corpus/reexport_chain_exec/artifacts.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+clang_dylib leaf.c libleaf.dylib
2
+clang_reexport_dylib mid.c libmid.dylib libleaf.dylib
tests/parity_corpus/reexport_chain_exec/command_checks.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+build_version
tests/parity_corpus/reexport_chain_exec/inputs/leaf.cadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+int leaf_value(void) {
2
+    return 29;
3
+}
tests/parity_corpus/reexport_chain_exec/inputs/main.cadded
5 lines changed — click to load
@@ -0,0 +1,5 @@
1
+int mid_anchor(void);
2
+
3
+int main(void) {
4
+    return mid_anchor() == 0 ? 0 : 1;
5
+}
tests/parity_corpus/reexport_chain_exec/inputs/mid.cadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+int mid_anchor(void) {
2
+    return 0;
3
+}
tests/parity_corpus/reexport_chain_exec/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Dylib reexport-chain parity case. The executable links only the middle dylib,
2
+which itself reexports a leaf dylib. The executable resolves a direct symbol
3
+from the middle dylib while carrying the same dependency-chain surface Apple
4
+`ld` produces.
tests/parity_corpus/reexport_chain_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/reexport_chain_exec/sections.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+__TEXT __text
2
+__TEXT __stubs
tests/parity_corpus/reloc_adrp_add_backward/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/reloc_adrp_add_backward/inputs/main.sadded
10 lines changed — click to load
@@ -0,0 +1,10 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        _target:
3
+            .quad 0x55
4
+        .space 0x4ff8
5
+        .globl _main
6
+        _main:
7
+            adrp x0, _target@PAGE
8
+            add x0, x0, _target@PAGEOFF
9
+            ret
10
+        .subsections_via_symbols
tests/parity_corpus/reloc_adrp_add_backward/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Backward `adrp/add` page-reference parity case lifted from the legacy
2
+relocation matrix. This keeps the shared corpus honest about negative-offset
3
+page references as well as forward ones.
tests/parity_corpus/reloc_adrp_add_backward/page_refs.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text 0x5000 add _target
tests/parity_corpus/reloc_adrp_add_backward/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/reloc_adrp_add_forward/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/reloc_adrp_add_forward/inputs/main.sadded
10 lines changed — click to load
@@ -0,0 +1,10 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _main
3
+        _main:
4
+            adrp x0, _target@PAGE
5
+            add x0, x0, _target@PAGEOFF
6
+            ret
7
+        .space 0x4ff4
8
+        _target:
9
+            .quad 0
10
+        .subsections_via_symbols
tests/parity_corpus/reloc_adrp_add_forward/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Forward `adrp/add` page-reference parity case lifted from the legacy relocation
2
+matrix. This checks that the shared Sprint 27 corpus can validate semantic page
3
+references, not just exact section bytes.
tests/parity_corpus/reloc_adrp_add_forward/page_refs.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text 0 add _target
tests/parity_corpus/reloc_adrp_add_forward/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/reloc_adrp_ldr_w/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/reloc_adrp_ldr_w/inputs/main.sadded
10 lines changed — click to load
@@ -0,0 +1,10 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _main
3
+        _main:
4
+            adrp x0, _target@PAGE
5
+            ldr w1, [x0, _target@PAGEOFF]
6
+            ret
7
+        .space 0x2f4
8
+        _target:
9
+            .long 0x11223344
10
+        .subsections_via_symbols
tests/parity_corpus/reloc_adrp_ldr_w/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+`adrp/ldr w` page-reference parity case lifted from the legacy relocation
2
+matrix. This keeps the shared Sprint 27 corpus growing across narrower load
3
+widths, not just 64-bit loads.
tests/parity_corpus/reloc_adrp_ldr_w/page_refs.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text 0 load _target
tests/parity_corpus/reloc_adrp_ldr_w/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/reloc_adrp_ldr_x/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/reloc_adrp_ldr_x/inputs/main.sadded
10 lines changed — click to load
@@ -0,0 +1,10 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _main
3
+        _main:
4
+            adrp x0, _target@PAGE
5
+            ldr x1, [x0, _target@PAGEOFF]
6
+            ret
7
+        .space 0x3f4
8
+        _target:
9
+            .quad 0x1122334455667788
10
+        .subsections_via_symbols
tests/parity_corpus/reloc_adrp_ldr_x/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+`adrp/ldr x` page-reference parity case lifted from the legacy relocation
2
+matrix. This expands the shared Sprint 27 corpus from branch and add-based
3
+relocations into load-addressing semantics too.
tests/parity_corpus/reloc_adrp_ldr_x/page_refs.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text 0 load _target
tests/parity_corpus/reloc_adrp_ldr_x/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/reloc_adrp_ldrb/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/reloc_adrp_ldrb/inputs/main.sadded
10 lines changed — click to load
@@ -0,0 +1,10 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _main
3
+        _main:
4
+            adrp x0, _target@PAGE
5
+            ldrb w1, [x0, _target@PAGEOFF]
6
+            ret
7
+        .space 0xf4
8
+        _target:
9
+            .byte 0x44
10
+        .subsections_via_symbols
tests/parity_corpus/reloc_adrp_ldrb/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+`adrp/ldrb` page-reference parity case lifted from the legacy relocation
2
+matrix. This completes the byte-width side of the current page-reference corpus
3
+coverage.
tests/parity_corpus/reloc_adrp_ldrb/page_refs.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text 0 load _target
tests/parity_corpus/reloc_adrp_ldrb/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/reloc_adrp_ldrh/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/reloc_adrp_ldrh/inputs/main.sadded
10 lines changed — click to load
@@ -0,0 +1,10 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _main
3
+        _main:
4
+            adrp x0, _target@PAGE
5
+            ldrh w1, [x0, _target@PAGEOFF]
6
+            ret
7
+        .space 0x1f4
8
+        _target:
9
+            .hword 0x3344
10
+        .subsections_via_symbols
tests/parity_corpus/reloc_adrp_ldrh/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+`adrp/ldrh` page-reference parity case lifted from the legacy relocation
2
+matrix. This keeps the corpus honest for halfword loads too, not only word and
3
+doubleword forms.
tests/parity_corpus/reloc_adrp_ldrh/page_refs.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text 0 load _target
tests/parity_corpus/reloc_adrp_ldrh/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/reloc_branch_and_subtractor/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/reloc_branch_and_subtractor/inputs/main.sadded
13 lines changed — click to load
@@ -0,0 +1,13 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _helper
3
+        _helper:
4
+            ret
5
+        .globl _main
6
+        _main:
7
+            bl _helper
8
+            ret
9
+        .section __TEXT,__const
10
+        .p2align 3
11
+        _delta:
12
+            .quad _main - _helper
13
+        .subsections_via_symbols
tests/parity_corpus/reloc_branch_and_subtractor/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Combined branch plus subtractor relocation parity case lifted from the legacy
2
+matrix. This finishes the current subtractor trio in the shared Sprint 27
3
+corpus and checks both `__TEXT,__text` and `__TEXT,__const` parity together.
tests/parity_corpus/reloc_branch_and_subtractor/sections.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+__TEXT __text
2
+__TEXT __const
tests/parity_corpus/reloc_branch_backward/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/reloc_branch_backward/inputs/main.sadded
9 lines changed — click to load
@@ -0,0 +1,9 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _helper
3
+        _helper:
4
+            ret
5
+        .globl _main
6
+        _main:
7
+            bl _helper
8
+            ret
9
+        .subsections_via_symbols
tests/parity_corpus/reloc_branch_backward/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Backward branch relocation parity case lifted from the existing
2
+`relocated_sections_match_apple_ld_across_fixture_matrix` coverage. This keeps
3
+the shared Sprint 27 corpus growing on the relocation side with a simple exact
4
+`__TEXT,__text` byte-parity check after reloc application.
tests/parity_corpus/reloc_branch_backward/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text
tests/parity_corpus/reloc_branch_forward/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/reloc_branch_forward/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/reloc_branch_forward/inputs/main.sadded
8 lines changed — click to load
@@ -0,0 +1,8 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _main
3
+        _main:
4
+            bl _helper
5
+            ret
6
+        _helper:
7
+            ret
8
+        .subsections_via_symbols
tests/parity_corpus/reloc_branch_forward/notes.mdadded
5 lines changed — click to load
@@ -0,0 +1,5 @@
1
+Forward branch relocation parity case lifted from the existing
2
+`relocated_sections_match_apple_ld_across_fixture_matrix` coverage. This is the
3
+first explicit relocation-matrix migration into the Sprint 27 corpus and keeps
4
+the check intentionally simple: exact `__TEXT,__text` byte parity after reloc
5
+application.
tests/parity_corpus/reloc_branch_forward/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text
tests/parity_corpus/reloc_mixed_branch_adrp_text/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/reloc_mixed_branch_adrp_text/inputs/main.sadded
14 lines changed — click to load
@@ -0,0 +1,14 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _main
3
+        .globl _helper
4
+        _main:
5
+            adrp x0, _target@PAGE
6
+            add x0, x0, _target@PAGEOFF
7
+            bl _helper
8
+            ret
9
+        _helper:
10
+            ret
11
+        .space 0xff0
12
+        _target:
13
+            .quad 0x99
14
+        .subsections_via_symbols
tests/parity_corpus/reloc_mixed_branch_adrp_text/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Mixed branch-plus-`adrp/add` parity case lifted from the legacy relocation
2
+matrix. This gives the shared Sprint 27 corpus one executable that exercises
3
+both branch and page-reference relocation semantics together.
tests/parity_corpus/reloc_mixed_branch_adrp_text/page_refs.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text 0 add _target
tests/parity_corpus/reloc_mixed_branch_adrp_text/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/reloc_subtractor_negative/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/reloc_subtractor_negative/inputs/main.sadded
12 lines changed — click to load
@@ -0,0 +1,12 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _helper
3
+        _helper:
4
+            ret
5
+        .globl _main
6
+        _main:
7
+            ret
8
+        .section __TEXT,__const
9
+        .p2align 3
10
+        _delta:
11
+            .quad _main - _helper
12
+        .subsections_via_symbols
tests/parity_corpus/reloc_subtractor_negative/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Negative subtractor relocation parity case lifted from the legacy relocation
2
+matrix. This keeps the corpus honest on signed subtractor results as well as
3
+positive ones.
tests/parity_corpus/reloc_subtractor_negative/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __const
tests/parity_corpus/reloc_subtractor_positive/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/reloc_subtractor_positive/inputs/main.sadded
13 lines changed — click to load
@@ -0,0 +1,13 @@
1
+        .section __TEXT,__text,regular,pure_instructions
2
+        .globl _helper
3
+        _helper:
4
+            ret
5
+        .globl _main
6
+        _main:
7
+            bl _helper
8
+            ret
9
+        .section __TEXT,__const
10
+        .p2align 3
11
+        _delta:
12
+            .quad _helper - _main
13
+        .subsections_via_symbols
tests/parity_corpus/reloc_subtractor_positive/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Positive subtractor relocation parity case lifted from the legacy relocation
2
+matrix. This extends the shared corpus beyond code relocs into absolute
3
+subtractor expressions materialized in `__TEXT,__const`.
tests/parity_corpus/reloc_subtractor_positive/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __const
tests/parity_corpus/runtime_fortran_three_func_exec/args.txtadded
16 lines changed — click to load
@@ -0,0 +1,16 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
16
+@FILE:libarmfortas_rt.a@
tests/parity_corpus/runtime_fortran_three_func_exec/command_checks.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+build_version
2
+load_dylib_names
tests/parity_corpus/runtime_fortran_three_func_exec/files/libarmfortas_rt.aadded
Binary file changed.
tests/parity_corpus/runtime_fortran_three_func_exec/files/source.f90added
18 lines changed — click to load
@@ -0,0 +1,18 @@
1
+program main
2
+  integer :: v
3
+  v = add3(7)
4
+contains
5
+  integer function add3(x)
6
+    integer, intent(in) :: x
7
+    add3 = twice(x) + one()
8
+  end function add3
9
+
10
+  integer function twice(x)
11
+    integer, intent(in) :: x
12
+    twice = x + x
13
+  end function twice
14
+
15
+  integer function one()
16
+    one = 8
17
+  end function one
18
+end program main
tests/parity_corpus/runtime_fortran_three_func_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_SEGMENT_64
tests/parity_corpus/runtime_fortran_three_func_exec/inputs/main.oadded
Binary file changed.
tests/parity_corpus/runtime_fortran_three_func_exec/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Minimal runtime parity case for a three-function Fortran program. The input
2
+object is compiled with `armfortas`, and the sidecar `libarmfortas_rt.a` is a
3
+real archive copied from the runtime build so this scenario stays standalone
4
+inside the `afs-ld` repo.
tests/parity_corpus/runtime_fortran_three_func_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/runtime_fortran_three_func_exec/sections.txtadded
0 lines changed — click to load
tests/parity_corpus/strip_locals_exec/args.txtadded
10 lines changed — click to load
@@ -0,0 +1,10 @@
1
+-arch
2
+arm64
3
+-x
4
+-no_fixup_chains
5
+-e
6
+_main
7
+-o
8
+@OUT@
9
+@INPUT:main.o@
10
+@ARTIFACT:libsymtab.dylib@
tests/parity_corpus/strip_locals_exec/artifacts.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+clang_dylib libsymtab.c libsymtab.dylib
tests/parity_corpus/strip_locals_exec/command_checks.txtadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+load_dylib_names
2
+symbol_record_map
3
+symbol_partition_names
4
+string_table_near_parity
tests/parity_corpus/strip_locals_exec/inputs/libsymtab.cadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+int ext_data = 5;
tests/parity_corpus/strip_locals_exec/inputs/main.sadded
17 lines changed — click to load
@@ -0,0 +1,17 @@
1
+.text
2
+.private_extern _hidden
3
+.globl _visible
4
+.globl _main
5
+.p2align 2
6
+_local:
7
+    ret
8
+_hidden:
9
+    ret
10
+_visible:
11
+    ret
12
+_main:
13
+    ret
14
+
15
+.data
16
+.quad _ext_data
17
+.subsections_via_symbols
tests/parity_corpus/strip_locals_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+`-x` symtab parity case lifted from the existing Apple `ld` probe. This keeps
2
+the Sprint 27 corpus honest about stripping locals while preserving the
3
+remaining external and undefined symbol partitions.
tests/parity_corpus/strip_locals_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/symtab_partition_exec/args.txtadded
9 lines changed — click to load
@@ -0,0 +1,9 @@
1
+-arch
2
+arm64
3
+-no_fixup_chains
4
+-e
5
+_main
6
+-o
7
+@OUT@
8
+@INPUT:main.o@
9
+@ARTIFACT:libsymtab.dylib@
tests/parity_corpus/symtab_partition_exec/artifacts.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+clang_dylib libsymtab.c libsymtab.dylib
tests/parity_corpus/symtab_partition_exec/command_checks.txtadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+load_dylib_names
2
+symbol_record_map
3
+symbol_partition_names
4
+string_table_near_parity
tests/parity_corpus/symtab_partition_exec/inputs/libsymtab.cadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+int ext_data = 5;
tests/parity_corpus/symtab_partition_exec/inputs/main.sadded
17 lines changed — click to load
@@ -0,0 +1,17 @@
1
+.text
2
+.private_extern _hidden
3
+.globl _visible
4
+.globl _main
5
+.p2align 2
6
+_local:
7
+    ret
8
+_hidden:
9
+    ret
10
+_visible:
11
+    ret
12
+_main:
13
+    ret
14
+
15
+.data
16
+.quad _ext_data
17
+.subsections_via_symbols
tests/parity_corpus/symtab_partition_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Symtab partition parity case lifted from the existing Apple `ld` probe. This
2
+keeps the Sprint 27 corpus honest about local/extdef/undef partitioning and
3
+string-table size staying near Apple `ld` on a dylib-import executable.
tests/parity_corpus/symtab_partition_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/synthetic_import_classic_lazy/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/synthetic_import_classic_lazy/command_checks.txtadded
7 lines changed — click to load
@@ -0,0 +1,7 @@
1
+build_version
2
+load_dylib_names
3
+indirect_symbol_identities
4
+dyld_info_rebase
5
+dyld_info_bind
6
+dyld_info_weak_bind
7
+dyld_info_lazy_bind
tests/parity_corpus/synthetic_import_classic_lazy/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/synthetic_import_classic_lazy/inputs/main.sadded
8 lines changed — click to load
@@ -0,0 +1,8 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _main
3
+_main:
4
+    adrp x0, _write@GOTPAGE
5
+    ldr x0, [x0, _write@GOTPAGEOFF]
6
+    bl _write
7
+    ret
8
+.subsections_via_symbols
tests/parity_corpus/synthetic_import_classic_lazy/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Synthetic-import classic-lazy parity case lifted from the dedicated Apple `ld`
2
+probe. This keeps the Sprint 27 corpus honest about stub surfaces, dyld-info
3
+streams, and indirect-symbol identities for the synthetic import path.
tests/parity_corpus/synthetic_import_classic_lazy/sections.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+__TEXT __stubs
2
+__TEXT __stub_helper
tests/parity_corpus/unexport_filter_dylib/args.txtadded
18 lines changed — click to load
@@ -0,0 +1,18 @@
1
+-dylib
2
+-arch
3
+arm64
4
+-platform_version
5
+macos
6
+@SDK_VERSION@
7
+@SDK_VERSION@
8
+-syslibroot
9
+@SDK_PATH@
10
+-lSystem
11
+-no_fixup_chains
12
+-unexported_symbol
13
+_gamma
14
+-unexported_symbols_list
15
+@FILE:hidden.txt@
16
+-o
17
+@OUT@
18
+@INPUT:main.o@
tests/parity_corpus/unexport_filter_dylib/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+export_records
3
+symbol_record_map
tests/parity_corpus/unexport_filter_dylib/files/hidden.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+_bet?
tests/parity_corpus/unexport_filter_dylib/inputs/main.sadded
11 lines changed — click to load
@@ -0,0 +1,11 @@
1
+.section __TEXT,__text,regular,pure_instructions
2
+.globl _alpha
3
+.globl _beta
4
+.globl _gamma
5
+_alpha:
6
+    ret
7
+_beta:
8
+    ret
9
+_gamma:
10
+    ret
11
+.subsections_via_symbols
tests/parity_corpus/unexport_filter_dylib/notes.mdadded
4 lines changed — click to load
@@ -0,0 +1,4 @@
1
+Unexport-filter dylib parity case lifted from the existing `linker_run`
2
+coverage. This keeps the Sprint 27 corpus honest about `-unexported_symbol`
3
+and `-unexported_symbols_list` matching Apple `ld` in both the export trie and
4
+the final symtab surface.
tests/parity_corpus/unexport_filter_dylib/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/unwind_leaf_exec/absent_sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__LD __compact_unwind
tests/parity_corpus/unwind_leaf_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/unwind_leaf_exec/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+load_dylib_names
3
+rebased_unwind_bytes
tests/parity_corpus/unwind_leaf_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/unwind_leaf_exec/inputs/main.cadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+int main(void) {
2
+    return 0;
3
+}
tests/parity_corpus/unwind_leaf_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Leaf unwind-info parity case lifted from the existing Apple `ld` probe. This
2
+keeps the Sprint 27 corpus honest about rebased `__unwind_info` bytes for the
3
+smallest executable shape.
tests/parity_corpus/unwind_leaf_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/unwind_multi_exec/absent_sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__LD __compact_unwind
tests/parity_corpus/unwind_multi_exec/args.txtadded
15 lines changed — click to load
@@ -0,0 +1,15 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-no_fixup_chains
11
+-e
12
+_main
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
tests/parity_corpus/unwind_multi_exec/command_checks.txtadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+build_version
2
+load_dylib_names
3
+rebased_unwind_bytes
tests/parity_corpus/unwind_multi_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_LOAD_DYLIB
tests/parity_corpus/unwind_multi_exec/inputs/main.cadded
7 lines changed — click to load
@@ -0,0 +1,7 @@
1
+int helper(void) {
2
+    return 1;
3
+}
4
+
5
+int main(void) {
6
+    return helper();
7
+}
tests/parity_corpus/unwind_multi_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Multi-function unwind-info parity case lifted from the existing Apple `ld`
2
+probe. This keeps the Sprint 27 corpus honest about rebased `__unwind_info`
3
+bytes when more than one function contributes unwind metadata.
tests/parity_corpus/unwind_multi_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/weak_def_coalescing_exec/args.txtadded
17 lines changed — click to load
@@ -0,0 +1,17 @@
1
+-arch
2
+arm64
3
+-platform_version
4
+macos
5
+@SDK_VERSION@
6
+@SDK_VERSION@
7
+-syslibroot
8
+@SDK_PATH@
9
+-lSystem
10
+-e
11
+_main
12
+-no_fixup_chains
13
+-o
14
+@OUT@
15
+@INPUT:main.o@
16
+@ARTIFACT:libweak_a.dylib@
17
+@ARTIFACT:libweak_b.dylib@
tests/parity_corpus/weak_def_coalescing_exec/artifacts.txtadded
2 lines changed — click to load
@@ -0,0 +1,2 @@
1
+clang_dylib weak_a.c libweak_a.dylib
2
+clang_dylib weak_b.c libweak_b.dylib
tests/parity_corpus/weak_def_coalescing_exec/command_checks.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+build_version
tests/parity_corpus/weak_def_coalescing_exec/ignored_load_commands.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+LC_SEGMENT_64
tests/parity_corpus/weak_def_coalescing_exec/inputs/main.cadded
5 lines changed — click to load
@@ -0,0 +1,5 @@
1
+int shared(void);
2
+
3
+int main(void) {
4
+    return shared() == 17 ? 0 : 1;
5
+}
tests/parity_corpus/weak_def_coalescing_exec/inputs/weak_a.cadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+__attribute__((weak)) int shared(void) {
2
+    return 17;
3
+}
tests/parity_corpus/weak_def_coalescing_exec/inputs/weak_b.cadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+__attribute__((weak)) int shared(void) {
2
+    return 23;
3
+}
tests/parity_corpus/weak_def_coalescing_exec/notes.mdadded
3 lines changed — click to load
@@ -0,0 +1,3 @@
1
+Weak-definition coalescing parity case. The executable links two dylibs that
2
+both export the same weak definition, and the final image plus runtime
3
+resolution should follow the same winner Apple `ld` chooses.
tests/parity_corpus/weak_def_coalescing_exec/runtime.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+
tests/parity_corpus/weak_def_coalescing_exec/sections.txtadded
1 lines changed — click to load
@@ -0,0 +1,1 @@
1
+__TEXT __text
tests/parity_harness.rsadded
66 lines changed — click to load
@@ -0,0 +1,66 @@
1
+//! Focused tests for Sprint 27 harness glue.
2
+
3
+mod common;
4
+
5
+use common::harness::{
6
+    apply_section_tolerances, diff_macho, parse_case_tolerances, string_table_within_five_percent,
7
+};
8
+
9
+#[test]
10
+fn notes_tolerance_block_parses_section_range() {
11
+    let notes = r#"
12
+tolerated:
13
+  - region: __TEXT,__text bytes 0x1-0x3 reason: "known padding drift"
14
+"#;
15
+    let tolerances = parse_case_tolerances(Some(notes)).expect("parse case tolerances");
16
+    assert_eq!(tolerances.len(), 1);
17
+    assert_eq!(tolerances[0].reason, "known padding drift");
18
+}
19
+
20
+#[test]
21
+fn notes_tolerance_can_hide_section_byte_diff() {
22
+    let notes = r#"
23
+tolerated:
24
+  - region: __TEXT,__text bytes 0x1-0x1 reason: "known one-byte drift"
25
+"#;
26
+    let tolerances = parse_case_tolerances(Some(notes)).expect("parse case tolerances");
27
+    let diff = diff_macho(b"abc", b"adc");
28
+    let filtered = apply_section_tolerances(diff, "__TEXT", "__text", &tolerances);
29
+    assert!(
30
+        filtered.is_clean(),
31
+        "expected tolerance to absorb diff: {filtered:#?}"
32
+    );
33
+    assert_eq!(filtered.tolerated.len(), 1);
34
+}
35
+
36
+#[test]
37
+fn notes_tolerance_does_not_hide_other_sections() {
38
+    let notes = r#"
39
+tolerated:
40
+  - region: __TEXT,__text bytes 0x1-0x1 reason: "known one-byte drift"
41
+"#;
42
+    let tolerances = parse_case_tolerances(Some(notes)).expect("parse case tolerances");
43
+    let diff = diff_macho(b"abc", b"adc");
44
+    let filtered = apply_section_tolerances(diff, "__DATA", "__data", &tolerances);
45
+    assert!(
46
+        !filtered.is_clean(),
47
+        "unexpectedly tolerated unrelated diff: {filtered:#?}"
48
+    );
49
+    assert_eq!(filtered.critical.len(), 1);
50
+}
51
+
52
+#[test]
53
+fn string_table_near_parity_accepts_small_suffix_dedup_drift() {
54
+    assert!(
55
+        string_table_within_five_percent(101, 100),
56
+        "1% string-table drift should stay within the Sprint 27 allowance"
57
+    );
58
+}
59
+
60
+#[test]
61
+fn string_table_near_parity_rejects_large_suffix_dedup_drift() {
62
+    assert!(
63
+        !string_table_within_five_percent(120, 100),
64
+        "20% string-table drift should fail the Sprint 27 allowance"
65
+    );
66
+}
tests/parity_matrix.rsadded
286 lines changed — click to load
@@ -0,0 +1,286 @@
1
+//! Differential parity matrix against Apple `ld`.
2
+//!
3
+//! Sprint 27 starts with a tiny executable-only corpus so the reusable harness,
4
+//! on-disk case format, and runtime parity path all exist before we scale up to
5
+//! the full corpus promised by the sprint doc.
6
+
7
+mod common;
8
+
9
+use std::fs;
10
+use std::path::{Path, PathBuf};
11
+use std::time::{Duration, Instant};
12
+
13
+use common::harness::{
14
+    compare_command_details, compare_command_ids, compare_page_refs, compare_runtime,
15
+    compare_sections, ensure_absent_load_commands, ensure_absent_sections, have_xcrun,
16
+    have_xcrun_tool, link_both, load_corpus, LinkCase,
17
+};
18
+
19
+#[test]
20
+fn parity_corpus() {
21
+    if !have_xcrun() || !have_xcrun_tool("ld") {
22
+        eprintln!("skipping: xcrun as/ld unavailable");
23
+        return;
24
+    }
25
+    let started = Instant::now();
26
+
27
+    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
28
+        .join("tests")
29
+        .join("parity_corpus");
30
+    let cases = load_corpus(&root).expect("load parity corpus");
31
+    assert!(
32
+        cases.len() >= 50,
33
+        "expected at least 50 parity corpus cases under {}, found {}",
34
+        root.display(),
35
+        cases.len()
36
+    );
37
+
38
+    let artifact_dir = std::env::var_os("PARITY_MATRIX_ARTIFACT_DIR").map(PathBuf::from);
39
+    if let Some(dir) = artifact_dir.as_ref() {
40
+        fs::create_dir_all(dir).expect("create parity artifact dir");
41
+    }
42
+
43
+    let mut case_reports = Vec::new();
44
+    let mut failures = Vec::new();
45
+
46
+    for case in cases {
47
+        let report = run_case(&case);
48
+        if let Some(dir) = artifact_dir.as_ref() {
49
+            write_case_artifact(dir, &case, &report).expect("write case artifact");
50
+        }
51
+        if let Some(error) = report.error_message() {
52
+            failures.push(error);
53
+        }
54
+        case_reports.push((case, report));
55
+    }
56
+
57
+    if let Some(dir) = artifact_dir.as_ref() {
58
+        write_index_artifact(dir, &case_reports).expect("write parity index");
59
+    }
60
+
61
+    assert!(
62
+        failures.is_empty(),
63
+        "Parity matrix failures ({} cases):\n{}",
64
+        failures.len(),
65
+        failures.join("\n\n")
66
+    );
67
+
68
+    if let Some(limit) = parity_matrix_time_limit() {
69
+        let elapsed = started.elapsed();
70
+        assert!(
71
+            elapsed <= limit,
72
+            "parity matrix exceeded scale budget: {:?} > {:?}",
73
+            elapsed,
74
+            limit
75
+        );
76
+    }
77
+}
78
+
79
+#[derive(Debug)]
80
+struct CaseStep {
81
+    name: &'static str,
82
+    error: Option<String>,
83
+}
84
+
85
+#[derive(Debug, Default)]
86
+struct CaseReport {
87
+    steps: Vec<CaseStep>,
88
+}
89
+
90
+impl CaseReport {
91
+    fn push(&mut self, name: &'static str, result: Result<(), String>) -> bool {
92
+        match result {
93
+            Ok(()) => {
94
+                self.steps.push(CaseStep { name, error: None });
95
+                true
96
+            }
97
+            Err(error) => {
98
+                self.steps.push(CaseStep {
99
+                    name,
100
+                    error: Some(error),
101
+                });
102
+                false
103
+            }
104
+        }
105
+    }
106
+
107
+    fn passed(&self) -> bool {
108
+        self.steps.iter().all(|step| step.error.is_none())
109
+    }
110
+
111
+    fn error_message(&self) -> Option<String> {
112
+        self.steps.iter().find_map(|step| {
113
+            step.error
114
+                .as_ref()
115
+                .map(|error| format!("{} failed:\n{}", step.name, error))
116
+        })
117
+    }
118
+}
119
+
120
+fn run_case(case: &LinkCase) -> CaseReport {
121
+    let mut report = CaseReport::default();
122
+    let outputs = match link_both(case) {
123
+        Ok(outputs) => {
124
+            report.push("link", Ok(()));
125
+            outputs
126
+        }
127
+        Err(error) => {
128
+            report.push(
129
+                "link",
130
+                Err(format!(
131
+                    "failed to link parity case from {}:\n{}",
132
+                    case.dir.display(),
133
+                    error
134
+                )),
135
+            );
136
+            return report;
137
+        }
138
+    };
139
+
140
+    if !report.push(
141
+        "load-command ids",
142
+        compare_command_ids(&outputs.ours, &outputs.theirs, &case.ignored_load_commands),
143
+    ) {
144
+        return report;
145
+    }
146
+    if !report.push(
147
+        "command details",
148
+        compare_command_details(&outputs.ours, &outputs.theirs, &case.command_checks),
149
+    ) {
150
+        return report;
151
+    }
152
+    if !report.push(
153
+        "afs-ld absent commands",
154
+        ensure_absent_load_commands(&outputs.ours, &case.absent_load_commands, "afs-ld"),
155
+    ) {
156
+        return report;
157
+    }
158
+    if !report.push(
159
+        "Apple absent commands",
160
+        ensure_absent_load_commands(&outputs.theirs, &case.absent_load_commands, "Apple ld"),
161
+    ) {
162
+        return report;
163
+    }
164
+    if !report.push(
165
+        "afs-ld absent sections",
166
+        ensure_absent_sections(&outputs.ours, &case.absent_sections, "afs-ld"),
167
+    ) {
168
+        return report;
169
+    }
170
+    if !report.push(
171
+        "Apple absent sections",
172
+        ensure_absent_sections(&outputs.theirs, &case.absent_sections, "Apple ld"),
173
+    ) {
174
+        return report;
175
+    }
176
+    if !report.push(
177
+        "section parity",
178
+        compare_sections(
179
+            &outputs.ours,
180
+            &outputs.theirs,
181
+            &case.section_checks,
182
+            &case.case_tolerances,
183
+        ),
184
+    ) {
185
+        return report;
186
+    }
187
+    if !report.push(
188
+        "page-ref parity",
189
+        compare_page_refs(&outputs.ours, &outputs.theirs, &case.page_ref_checks),
190
+    ) {
191
+        return report;
192
+    }
193
+    if !case.runtime_args.is_empty() || case.dir.join("runtime.txt").exists() {
194
+        report.push(
195
+            "runtime parity",
196
+            compare_runtime(&outputs.our_path, &outputs.their_path, &case.runtime_args),
197
+        );
198
+    }
199
+
200
+    report
201
+}
202
+
203
+fn write_case_artifact(dir: &Path, case: &LinkCase, report: &CaseReport) -> Result<(), String> {
204
+    let path = dir.join(format!("{}.html", slug(&case.name)));
205
+    let mut html = String::new();
206
+    html.push_str("<!doctype html><html><head><meta charset=\"utf-8\">");
207
+    html.push_str(&format!(
208
+        "<title>{}</title><style>body{{font-family:ui-monospace,Menlo,monospace;padding:2rem;}} .ok{{color:#0a0;}} .fail{{color:#a00;}} pre{{background:#f6f8fa;padding:1rem;white-space:pre-wrap;}}</style></head><body>",
209
+        escape_html(&case.name)
210
+    ));
211
+    html.push_str(&format!("<h1>{}</h1>", escape_html(&case.name)));
212
+    html.push_str(&format!(
213
+        "<p>Status: <strong class=\"{}\">{}</strong></p>",
214
+        if report.passed() { "ok" } else { "fail" },
215
+        if report.passed() { "PASS" } else { "FAIL" }
216
+    ));
217
+    html.push_str("<h2>Steps</h2><ul>");
218
+    for step in &report.steps {
219
+        match &step.error {
220
+            None => html.push_str(&format!(
221
+                "<li><span class=\"ok\">PASS</span> {}</li>",
222
+                escape_html(step.name)
223
+            )),
224
+            Some(error) => html.push_str(&format!(
225
+                "<li><span class=\"fail\">FAIL</span> {}<pre>{}</pre></li>",
226
+                escape_html(step.name),
227
+                escape_html(error)
228
+            )),
229
+        }
230
+    }
231
+    html.push_str("</ul>");
232
+    html.push_str("<h2>Args</h2><pre>");
233
+    html.push_str(&escape_html(&case.args.join("\n")));
234
+    html.push_str("</pre>");
235
+    if let Some(notes) = &case.notes {
236
+        html.push_str("<h2>Notes</h2><pre>");
237
+        html.push_str(&escape_html(notes));
238
+        html.push_str("</pre>");
239
+    }
240
+    html.push_str("</body></html>");
241
+    fs::write(&path, html).map_err(|e| format!("write {}: {e}", path.display()))
242
+}
243
+
244
+fn write_index_artifact(dir: &Path, cases: &[(LinkCase, CaseReport)]) -> Result<(), String> {
245
+    let mut html = String::new();
246
+    html.push_str("<!doctype html><html><head><meta charset=\"utf-8\">");
247
+    html.push_str("<title>Parity Matrix</title><style>body{font-family:ui-monospace,Menlo,monospace;padding:2rem;} .ok{color:#0a0;} .fail{color:#a00;}</style></head><body>");
248
+    html.push_str("<h1>Parity Matrix</h1><ul>");
249
+    for (case, report) in cases {
250
+        let slug = slug(&case.name);
251
+        html.push_str(&format!(
252
+            "<li><a href=\"{}.html\">{}</a> <strong class=\"{}\">{}</strong></li>",
253
+            slug,
254
+            escape_html(&case.name),
255
+            if report.passed() { "ok" } else { "fail" },
256
+            if report.passed() { "PASS" } else { "FAIL" }
257
+        ));
258
+    }
259
+    html.push_str("</ul></body></html>");
260
+    let path = dir.join("index.html");
261
+    fs::write(&path, html).map_err(|e| format!("write {}: {e}", path.display()))
262
+}
263
+
264
+fn slug(name: &str) -> String {
265
+    name.chars()
266
+        .map(|ch| {
267
+            if ch.is_ascii_alphanumeric() {
268
+                ch.to_ascii_lowercase()
269
+            } else {
270
+                '-'
271
+            }
272
+        })
273
+        .collect()
274
+}
275
+
276
+fn parity_matrix_time_limit() -> Option<Duration> {
277
+    let raw = std::env::var("PARITY_MATRIX_MAX_SECONDS").ok()?;
278
+    let seconds = raw.parse::<u64>().ok()?;
279
+    Some(Duration::from_secs(seconds))
280
+}
281
+
282
+fn escape_html(text: &str) -> String {
283
+    text.replace('&', "&amp;")
284
+        .replace('<', "&lt;")
285
+        .replace('>', "&gt;")
286
+}
tests/perf_baseline.rsadded
170 lines changed — click to load
@@ -0,0 +1,170 @@
1
+use std::path::{Path, PathBuf};
2
+use std::time::Duration;
3
+
4
+mod common;
5
+
6
+use afs_ld::{LinkOptions, LinkProfile, Linker};
7
+use common::harness::{assemble, have_xcrun, have_xcrun_tool, scratch, sdk_path, sdk_version};
8
+
9
+fn find_runtime_archive() -> Option<PathBuf> {
10
+    let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
11
+    for profile in ["debug", "release"] {
12
+        let candidate = workspace
13
+            .join("target")
14
+            .join(profile)
15
+            .join("libarmfortas_rt.a");
16
+        if candidate.is_file() {
17
+            return Some(candidate);
18
+        }
19
+    }
20
+    None
21
+}
22
+
23
+fn executable_opts(inputs: Vec<PathBuf>, output: PathBuf) -> LinkOptions {
24
+    LinkOptions {
25
+        inputs,
26
+        output: Some(output),
27
+        syslibroot: sdk_path().map(PathBuf::from),
28
+        platform_version: sdk_version().map(|v| {
29
+            let parsed = afs_ld::macho::tbd::parse_version(&v);
30
+            afs_ld::PlatformVersion {
31
+                minos: parsed,
32
+                sdk: parsed,
33
+            }
34
+        }),
35
+        library_names: vec!["System".into()],
36
+        ..LinkOptions::default()
37
+    }
38
+}
39
+
40
+fn assert_profile_basics(name: &str, profile: &LinkProfile) {
41
+    eprintln!(
42
+        "{name}: total={:?} parse={:?} resolve={:?} atomize={:?} layout={:?} synth={:?} (linkedit={:?}: symbols={:?} [locals={:?} globals={:?} strtab={:?}] dyld={:?} metadata={:?} codesig={:?}; unwind={:?}) reloc={:?} write={:?}",
43
+        profile.total_wall,
44
+        profile.phases.input_parsing,
45
+        profile.phases.symbol_resolution,
46
+        profile.phases.atomization,
47
+        profile.phases.layout,
48
+        profile.phases.synth_sections,
49
+        profile.phases.synth_linkedit_finalize,
50
+        profile.phases.synth_linkedit_symbol_plan,
51
+        profile.phases.synth_linkedit_symbol_plan_locals,
52
+        profile.phases.synth_linkedit_symbol_plan_globals,
53
+        profile.phases.synth_linkedit_symbol_plan_strtab,
54
+        profile.phases.synth_linkedit_dyld_info,
55
+        profile.phases.synth_linkedit_metadata_tables,
56
+        profile.phases.synth_linkedit_code_signature,
57
+        profile.phases.synth_unwind,
58
+        profile.phases.reloc_apply,
59
+        profile.phases.write_output,
60
+    );
61
+    assert!(profile.output.is_file(), "{name}: output file missing");
62
+    assert!(
63
+        profile.total_wall >= profile.phases.accounted_total(),
64
+        "{name}: accounted phases exceeded total wall time"
65
+    );
66
+    assert!(
67
+        profile.phases.accounted_total() > Duration::ZERO,
68
+        "{name}: all phase timings were zero"
69
+    );
70
+    assert!(
71
+        profile.phases.synth_sections
72
+            >= profile.phases.synth_linkedit_finalize + profile.phases.synth_unwind,
73
+        "{name}: synth subphases exceeded synth total"
74
+    );
75
+    assert!(
76
+        profile.phases.synth_linkedit_finalize
77
+            >= profile.phases.synth_linkedit_symbol_plan
78
+                + profile.phases.synth_linkedit_dyld_info
79
+                + profile.phases.synth_linkedit_metadata_tables
80
+                + profile.phases.synth_linkedit_code_signature,
81
+        "{name}: linkedit subphases exceeded linkedit total"
82
+    );
83
+    assert!(
84
+        profile.phases.synth_linkedit_symbol_plan
85
+            >= profile.phases.synth_linkedit_symbol_plan_locals
86
+                + profile.phases.synth_linkedit_symbol_plan_globals
87
+                + profile.phases.synth_linkedit_symbol_plan_strtab,
88
+        "{name}: symbol-plan subphases exceeded symbol-plan total"
89
+    );
90
+}
91
+
92
+#[test]
93
+fn hello_world_profile_reports_baseline_timings() {
94
+    if !have_xcrun() || !have_xcrun_tool("ld") {
95
+        eprintln!("skipping: xcrun as/ld unavailable");
96
+        return;
97
+    }
98
+
99
+    let obj = scratch("perf-hello.o");
100
+    let out = scratch("perf-hello.out");
101
+    assemble(
102
+        "\
103
+        .text\n\
104
+        .globl _main\n\
105
+        .p2align 2\n\
106
+        _main:\n\
107
+            mov w0, #0\n\
108
+            ret\n",
109
+        &obj,
110
+    )
111
+    .expect("assemble hello");
112
+
113
+    let profile = Linker::run_profiled(&executable_opts(vec![obj], out)).expect("profile hello");
114
+    assert_profile_basics("hello", &profile);
115
+
116
+    if let Ok(limit_ms) = std::env::var("AFS_LD_HELLO_BUDGET_MS") {
117
+        let limit = Duration::from_millis(limit_ms.parse().expect("parse hello budget"));
118
+        assert!(
119
+            profile.total_wall <= limit,
120
+            "hello baseline exceeded budget: {:?} > {:?}",
121
+            profile.total_wall,
122
+            limit
123
+        );
124
+    }
125
+}
126
+
127
+#[test]
128
+fn runtime_link_profile_reports_baseline_timings() {
129
+    if !have_xcrun() || !have_xcrun_tool("ld") {
130
+        eprintln!("skipping: xcrun as/ld unavailable");
131
+        return;
132
+    }
133
+    let Some(runtime) = find_runtime_archive() else {
134
+        eprintln!("skipping: libarmfortas_rt.a not built");
135
+        return;
136
+    };
137
+
138
+    let obj = scratch("perf-runtime.o");
139
+    let out = scratch("perf-runtime.out");
140
+    assemble(
141
+        "\
142
+        .text\n\
143
+        .globl _main\n\
144
+        .p2align 2\n\
145
+        _main:\n\
146
+            stp x29, x30, [sp, #-16]!\n\
147
+            mov x29, sp\n\
148
+            bl _afs_program_init\n\
149
+            bl _afs_program_finalize\n\
150
+            mov w0, #0\n\
151
+            ldp x29, x30, [sp], #16\n\
152
+            ret\n",
153
+        &obj,
154
+    )
155
+    .expect("assemble runtime");
156
+
157
+    let profile = Linker::run_profiled(&executable_opts(vec![obj, runtime], out))
158
+        .expect("profile runtime link");
159
+    assert_profile_basics("runtime", &profile);
160
+
161
+    if let Ok(limit_ms) = std::env::var("AFS_LD_RUNTIME_BUDGET_MS") {
162
+        let limit = Duration::from_millis(limit_ms.parse().expect("parse runtime budget"));
163
+        assert!(
164
+            profile.total_wall <= limit,
165
+            "runtime baseline exceeded budget: {:?} > {:?}",
166
+            profile.total_wall,
167
+            limit
168
+        );
169
+    }
170
+}
tests/snapshots/help.txtadded
49 lines changed — click to load
@@ -0,0 +1,49 @@
1
+Usage: afs-ld [options] <inputs...>
2
+
3
+Options:
4
+  -o <path>                       Write output to <path>
5
+  -dylib                          Emit a dylib instead of an executable
6
+  -e <symbol>                     Set the entry symbol
7
+  -arch arm64                     Select the arm64 target
8
+  -map <path>                     Emit text link map
9
+  -why_live <symbol>              Print a reachability chain for <symbol>
10
+  -l<name> / -l <name>            Search for library
11
+  -L <dir>                        Add library search path
12
+  -framework <name>               Link framework
13
+  -weak_framework <name>          Link weak framework
14
+  -ObjC                           Objective-C archive loading mode (currently a no-op warning)
15
+  -syslibroot <path>              Prefix SDK search roots
16
+  -platform_version macos <min> <sdk>
17
+                                  Set LC_BUILD_VERSION payload
18
+  -r                              Relocatable output (deferred; errors)
19
+  -bundle                         Bundle output (deferred; errors)
20
+  -undefined <error|warning|suppress|dynamic_lookup>
21
+                                  Control unresolved-symbol treatment
22
+  -rpath <path>                   Add LC_RPATH
23
+  -install_name <path>            Override dylib install name
24
+  -current_version <v>            Override dylib current version
25
+  -compatibility_version <v>      Override dylib compatibility version
26
+  -exported_symbols_list <file>   Export only symbols matching file patterns
27
+  -unexported_symbols_list <file> Hide symbols matching file patterns
28
+  -exported_symbol <sym>          Export one symbol/pattern
29
+  -unexported_symbol <sym>        Hide one symbol/pattern
30
+  -x                              Strip local symbols
31
+  -S                              Strip debug symbols (currently a no-op warning)
32
+  -no_uuid                        Omit LC_UUID
33
+  -no_loh                         Accepted for compatibility (currently warns; no effect)
34
+  -thunks=<none|safe|all>         Configure branch thunks
35
+  -dead_strip                     Dead-strip unreferenced code/data
36
+  -icf=safe | -icf=none | -icf=all
37
+                                  Configure identical code folding (`all` currently errors)
38
+  -fixup_chains | -no_fixup_chains
39
+                                  Select chained fixups vs classic dyld info
40
+  -all_load                       Force-load every archive member
41
+  -force_load <archive>           Force-load one archive
42
+  -Wl,<arg,arg,...>               Normalize comma-separated driver flags
43
+  --dump <path>                   Dump a Mach-O file summary
44
+  --dump-archive <path>           Dump an archive summary
45
+  --dump-dylib <path>             Dump a dylib summary
46
+  --dump-tbd <path>               Dump a TBD summary
47
+  -t, -trace                      Print input paths as they are loaded
48
+  -h, --help                      Show this help
49
+  -v, --version                   Show afs-ld version
Diff truncated: 393 files; expand each to load its hunks.