Rust · 243744 bytes Raw Blame History
1 //! End-to-end `Linker::run` coverage for Sprint 10's newly wired pipeline.
2
3 use std::collections::HashMap;
4 use std::fs;
5 use std::os::unix::fs::PermissionsExt;
6 use std::path::PathBuf;
7 use std::process::Command;
8
9 mod common;
10
11 use afs_ld::leb::read_uleb;
12 use afs_ld::macho::constants::{
13 BIND_IMMEDIATE_MASK, BIND_OPCODE_ADD_ADDR_ULEB, BIND_OPCODE_DONE, BIND_OPCODE_DO_BIND,
14 BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED, BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB,
15 BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB, BIND_OPCODE_MASK,
16 BIND_OPCODE_SET_DYLIB_ORDINAL_IMM, BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB,
17 BIND_OPCODE_SET_DYLIB_SPECIAL_IMM, BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB,
18 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM, BIND_OPCODE_SET_TYPE_IMM,
19 BIND_SYMBOL_FLAGS_WEAK_IMPORT, DICE_KIND_JUMP_TABLE32, INDIRECT_SYMBOL_ABS,
20 INDIRECT_SYMBOL_LOCAL, LC_BUILD_VERSION, LC_DATA_IN_CODE, LC_DYLD_INFO_ONLY, LC_DYSYMTAB,
21 LC_FUNCTION_STARTS, LC_SEGMENT_64, LC_SYMTAB, N_PEXT, REBASE_IMMEDIATE_MASK,
22 REBASE_OPCODE_ADD_ADDR_IMM_SCALED, REBASE_OPCODE_ADD_ADDR_ULEB, REBASE_OPCODE_DONE,
23 REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB, REBASE_OPCODE_DO_REBASE_IMM_TIMES,
24 REBASE_OPCODE_DO_REBASE_ULEB_TIMES, REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB,
25 REBASE_OPCODE_MASK, REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB, REBASE_OPCODE_SET_TYPE_IMM,
26 REBASE_TYPE_POINTER, SG_READ_ONLY,
27 };
28 use afs_ld::macho::dylib::DylibFile;
29 use afs_ld::macho::exports::{ExportKind, Exports};
30 use afs_ld::macho::reader::{parse_commands, parse_header, u32_le, LoadCommand, Section64Header};
31 use afs_ld::string_table::StringTable;
32 use afs_ld::symbol::{parse_nlist_table, SymKind};
33 use afs_ld::synth::unwind::decode_unwind_info;
34 use afs_ld::{FrameworkSpec, LinkError, LinkOptions, Linker, OutputKind};
35 use common::harness::diff_macho;
36
37 fn have_xcrun() -> bool {
38 Command::new("xcrun")
39 .arg("-f")
40 .arg("as")
41 .output()
42 .map(|o| o.status.success())
43 .unwrap_or(false)
44 }
45
46 fn sdk_path() -> Option<String> {
47 let out = Command::new("xcrun")
48 .args(["--sdk", "macosx", "--show-sdk-path"])
49 .output()
50 .ok()?;
51 if !out.status.success() {
52 return None;
53 }
54 Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
55 }
56
57 fn sdk_version() -> Option<String> {
58 let out = Command::new("xcrun")
59 .args(["--sdk", "macosx", "--show-sdk-version"])
60 .output()
61 .ok()?;
62 if !out.status.success() {
63 return None;
64 }
65 Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
66 }
67
68 fn find_runtime_archive() -> Option<PathBuf> {
69 let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
70 for profile in ["debug", "release"] {
71 let candidate = workspace
72 .join("target")
73 .join(profile)
74 .join("libarmfortas_rt.a");
75 if candidate.is_file() {
76 return Some(candidate);
77 }
78 }
79 None
80 }
81
82 fn have_xcrun_tool(tool: &str) -> bool {
83 Command::new("xcrun")
84 .arg("-f")
85 .arg(tool)
86 .output()
87 .map(|o| o.status.success())
88 .unwrap_or(false)
89 }
90
91 fn have_tool(tool: &str) -> bool {
92 Command::new(tool)
93 .arg("--version")
94 .output()
95 .map(|o| o.status.success() || !o.stderr.is_empty())
96 .unwrap_or(false)
97 }
98
99 fn assemble(src: &str, out: &PathBuf) -> Result<(), String> {
100 let tmp = std::env::temp_dir().join(format!(
101 "afs-ld-linker-run-{}-{}.s",
102 std::process::id(),
103 out.file_stem().and_then(|s| s.to_str()).unwrap_or("t")
104 ));
105 fs::write(&tmp, src).map_err(|e| format!("write: {e}"))?;
106 let output = Command::new("xcrun")
107 .args(["--sdk", "macosx", "as", "-arch", "arm64"])
108 .arg(&tmp)
109 .arg("-o")
110 .arg(out)
111 .output()
112 .map_err(|e| format!("spawn xcrun as: {e}"))?;
113 let _ = fs::remove_file(&tmp);
114 if !output.status.success() {
115 return Err(format!(
116 "xcrun as failed: {}",
117 String::from_utf8_lossy(&output.stderr)
118 ));
119 }
120 Ok(())
121 }
122
123 fn compile_c(src: &str, out: &PathBuf) -> Result<(), String> {
124 let tmp = std::env::temp_dir().join(format!(
125 "afs-ld-linker-run-{}-{}.c",
126 std::process::id(),
127 out.file_stem().and_then(|s| s.to_str()).unwrap_or("t")
128 ));
129 fs::write(&tmp, src).map_err(|e| format!("write: {e}"))?;
130 let output = Command::new("xcrun")
131 .args(["--sdk", "macosx", "clang", "-arch", "arm64", "-c"])
132 .arg(&tmp)
133 .arg("-o")
134 .arg(out)
135 .output()
136 .map_err(|e| format!("spawn xcrun clang: {e}"))?;
137 let _ = fs::remove_file(&tmp);
138 if !output.status.success() {
139 return Err(format!(
140 "xcrun clang failed: {}",
141 String::from_utf8_lossy(&output.stderr)
142 ));
143 }
144 Ok(())
145 }
146
147 fn compile_cxx(src: &str, out: &PathBuf) -> Result<(), String> {
148 let tmp = std::env::temp_dir().join(format!(
149 "afs-ld-linker-run-{}-{}.cc",
150 std::process::id(),
151 out.file_stem().and_then(|s| s.to_str()).unwrap_or("t")
152 ));
153 fs::write(&tmp, src).map_err(|e| format!("write: {e}"))?;
154 let output = Command::new("xcrun")
155 .args(["--sdk", "macosx", "clang++", "-arch", "arm64", "-c"])
156 .arg(&tmp)
157 .arg("-o")
158 .arg(out)
159 .output()
160 .map_err(|e| format!("spawn xcrun clang++: {e}"))?;
161 let _ = fs::remove_file(&tmp);
162 if !output.status.success() {
163 return Err(format!(
164 "xcrun clang++ failed: {}",
165 String::from_utf8_lossy(&output.stderr)
166 ));
167 }
168 Ok(())
169 }
170
171 fn compile_dylib_c(src: &str, out: &PathBuf) -> Result<(), String> {
172 let tmp = std::env::temp_dir().join(format!(
173 "afs-ld-linker-run-{}-{}.c",
174 std::process::id(),
175 out.file_stem().and_then(|s| s.to_str()).unwrap_or("lib")
176 ));
177 fs::write(&tmp, src).map_err(|e| format!("write: {e}"))?;
178 let install_name = out.to_string_lossy().to_string();
179 let output = Command::new("xcrun")
180 .args(["--sdk", "macosx", "clang", "-arch", "arm64", "-dynamiclib"])
181 .arg(&tmp)
182 .arg(format!("-Wl,-install_name,{install_name}"))
183 .arg("-o")
184 .arg(out)
185 .output()
186 .map_err(|e| format!("spawn xcrun clang dylib: {e}"))?;
187 let _ = fs::remove_file(&tmp);
188 if !output.status.success() {
189 return Err(format!(
190 "xcrun clang dylib failed: {}",
191 String::from_utf8_lossy(&output.stderr)
192 ));
193 }
194 Ok(())
195 }
196
197 fn scratch(name: &str) -> PathBuf {
198 std::env::temp_dir().join(format!("afs-ld-linker-run-{}-{name}", std::process::id()))
199 }
200
201 fn output_section(bytes: &[u8], segname: &str, sectname: &str) -> Option<(u64, Vec<u8>)> {
202 let header = parse_header(bytes).ok()?;
203 let commands = parse_commands(&header, bytes).ok()?;
204 for cmd in commands {
205 if let LoadCommand::Segment64(seg) = cmd {
206 for section in seg.sections {
207 if section.segname_str() == segname && section.sectname_str() == sectname {
208 let data = if section.offset == 0 {
209 Vec::new()
210 } else {
211 let start = section.offset as usize;
212 let end = start + section.size as usize;
213 bytes.get(start..end)?.to_vec()
214 };
215 return Some((section.addr, data));
216 }
217 }
218 }
219 }
220 None
221 }
222
223 fn output_section_header(bytes: &[u8], segname: &str, sectname: &str) -> Option<Section64Header> {
224 let header = parse_header(bytes).ok()?;
225 let commands = parse_commands(&header, bytes).ok()?;
226 for cmd in commands {
227 if let LoadCommand::Segment64(seg) = cmd {
228 for section in seg.sections {
229 if section.segname_str() == segname && section.sectname_str() == sectname {
230 return Some(section);
231 }
232 }
233 }
234 }
235 None
236 }
237
238 fn segment_flags(bytes: &[u8], segname: &str) -> Option<u32> {
239 let header = parse_header(bytes).ok()?;
240 let commands = parse_commands(&header, bytes).ok()?;
241 for cmd in commands {
242 if let LoadCommand::Segment64(seg) = cmd {
243 if seg.segname_str() == segname {
244 return Some(seg.flags);
245 }
246 }
247 }
248 None
249 }
250
251 fn segment_vmaddr(bytes: &[u8], segname: &str) -> Option<u64> {
252 let header = parse_header(bytes).ok()?;
253 let commands = parse_commands(&header, bytes).ok()?;
254 for cmd in commands {
255 if let LoadCommand::Segment64(seg) = cmd {
256 if seg.segname_str() == segname {
257 return Some(seg.vmaddr);
258 }
259 }
260 }
261 None
262 }
263
264 fn symbol_values(bytes: &[u8]) -> HashMap<String, u64> {
265 let header = parse_header(bytes).unwrap();
266 let commands = parse_commands(&header, bytes).unwrap();
267 let symtab = commands
268 .iter()
269 .find_map(|cmd| match cmd {
270 LoadCommand::Symtab(cmd) => Some(*cmd),
271 _ => None,
272 })
273 .unwrap();
274 let symbols = parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).unwrap();
275 let strings = StringTable::from_file(bytes, symtab.stroff, symtab.strsize).unwrap();
276 let mut out = HashMap::new();
277 for symbol in symbols {
278 let Ok(name) = strings.get(symbol.strx()) else {
279 continue;
280 };
281 out.insert(name.to_string(), symbol.value());
282 }
283 out
284 }
285
286 fn symtab_and_dysymtab(
287 bytes: &[u8],
288 ) -> (
289 afs_ld::macho::reader::SymtabCmd,
290 afs_ld::macho::reader::DysymtabCmd,
291 ) {
292 let header = parse_header(bytes).unwrap();
293 let commands = parse_commands(&header, bytes).unwrap();
294 let mut symtab = None;
295 let mut dysymtab = None;
296 for cmd in commands {
297 match cmd {
298 LoadCommand::Symtab(cmd) => symtab = Some(cmd),
299 LoadCommand::Dysymtab(cmd) => dysymtab = Some(cmd),
300 _ => {}
301 }
302 }
303 (symtab.unwrap(), dysymtab.unwrap())
304 }
305
306 fn symbol_partition_names(bytes: &[u8]) -> (Vec<String>, Vec<String>, Vec<String>) {
307 let (symtab, dysymtab) = symtab_and_dysymtab(bytes);
308 let symbols = parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).unwrap();
309 let strings = StringTable::from_file(bytes, symtab.stroff, symtab.strsize).unwrap();
310 let names_for = |start: u32, count: u32| -> Vec<String> {
311 symbols[start as usize..(start + count) as usize]
312 .iter()
313 .map(|symbol| strings.get(symbol.strx()).unwrap().to_string())
314 .collect()
315 };
316 (
317 names_for(dysymtab.ilocalsym, dysymtab.nlocalsym),
318 names_for(dysymtab.iextdefsym, dysymtab.nextdefsym),
319 names_for(dysymtab.iundefsym, dysymtab.nundefsym),
320 )
321 }
322
323 #[derive(Debug, Clone, PartialEq, Eq)]
324 struct CanonicalSymbolRecord {
325 name: String,
326 n_type: u8,
327 n_sect: u8,
328 n_desc: u16,
329 value: u64,
330 }
331
332 fn section_addrs(bytes: &[u8]) -> Vec<u64> {
333 let header = parse_header(bytes).unwrap();
334 let commands = parse_commands(&header, bytes).unwrap();
335 let mut out = Vec::new();
336 for cmd in commands {
337 if let LoadCommand::Segment64(seg) = cmd {
338 for section in seg.sections {
339 out.push(section.addr);
340 }
341 }
342 }
343 out
344 }
345
346 fn canonical_symbol_records(bytes: &[u8]) -> Vec<CanonicalSymbolRecord> {
347 let (symtab, _) = symtab_and_dysymtab(bytes);
348 let symbols = parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).unwrap();
349 let strings = StringTable::from_file(bytes, symtab.stroff, symtab.strsize).unwrap();
350 let section_addrs = section_addrs(bytes);
351 symbols
352 .iter()
353 .map(|symbol| {
354 let value = if symbol.kind() == SymKind::Sect && symbol.sect_idx() != 0 {
355 let section_addr = section_addrs[symbol.sect_idx() as usize - 1];
356 if symbol.value() >= section_addr {
357 symbol.value() - section_addr
358 } else {
359 symbol.value()
360 }
361 } else {
362 symbol.value()
363 };
364 CanonicalSymbolRecord {
365 name: strings.get(symbol.strx()).unwrap().to_string(),
366 n_type: symbol.raw.n_type,
367 n_sect: symbol.raw.n_sect,
368 n_desc: symbol.raw.n_desc,
369 value,
370 }
371 })
372 .collect()
373 }
374
375 fn canonical_symbol_record_map(bytes: &[u8]) -> HashMap<String, CanonicalSymbolRecord> {
376 canonical_symbol_records(bytes)
377 .into_iter()
378 .map(|record| (record.name.clone(), record))
379 .collect()
380 }
381
382 #[derive(Debug, Clone, PartialEq, Eq)]
383 enum CanonicalExportKind {
384 Regular(u64),
385 ThreadLocal(u64),
386 Absolute(u64),
387 Reexport { ordinal: u32, imported_name: String },
388 StubAndResolver { stub: u64, resolver: u64 },
389 }
390
391 #[derive(Debug, Clone, PartialEq, Eq)]
392 struct CanonicalExportRecord {
393 name: String,
394 flags: u64,
395 kind: CanonicalExportKind,
396 }
397
398 fn canonical_export_records(bytes: &[u8]) -> Vec<CanonicalExportRecord> {
399 let dylib = DylibFile::parse("/tmp/canonical.dylib", bytes).unwrap();
400 let symbol_values: HashMap<String, u64> = canonical_symbol_records(bytes)
401 .into_iter()
402 .map(|record| (record.name, record.value))
403 .collect();
404 let mut out = dylib
405 .exports
406 .entries()
407 .unwrap()
408 .into_iter()
409 .map(|entry| {
410 let kind = match entry.kind {
411 ExportKind::Regular { .. } => {
412 CanonicalExportKind::Regular(*symbol_values.get(&entry.name).unwrap())
413 }
414 ExportKind::ThreadLocal { .. } => {
415 CanonicalExportKind::ThreadLocal(*symbol_values.get(&entry.name).unwrap())
416 }
417 ExportKind::Absolute { .. } => {
418 CanonicalExportKind::Absolute(*symbol_values.get(&entry.name).unwrap())
419 }
420 ExportKind::Reexport {
421 ordinal,
422 imported_name,
423 } => CanonicalExportKind::Reexport {
424 ordinal,
425 imported_name,
426 },
427 ExportKind::StubAndResolver { stub, resolver } => {
428 CanonicalExportKind::StubAndResolver { stub, resolver }
429 }
430 };
431 CanonicalExportRecord {
432 name: entry.name,
433 flags: entry.flags,
434 kind,
435 }
436 })
437 .collect::<Vec<_>>();
438 out.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
439 out
440 }
441
442 fn dyld_info_export_names(bytes: &[u8]) -> Result<Vec<String>, String> {
443 let trie = dyld_info_stream(bytes, DyldInfoStreamKind::Export)?;
444 if trie.is_empty() {
445 return Ok(Vec::new());
446 }
447 let mut out = Exports::from_trie_bytes(&trie)
448 .entries()
449 .map_err(|e| format!("decode export trie: {e}"))?
450 .into_iter()
451 .map(|entry| entry.name)
452 .collect::<Vec<_>>();
453 out.sort();
454 Ok(out)
455 }
456
457 fn raw_string_table(bytes: &[u8]) -> Vec<u8> {
458 let (symtab, _) = symtab_and_dysymtab(bytes);
459 let start = symtab.stroff as usize;
460 let end = start + symtab.strsize as usize;
461 bytes[start..end].to_vec()
462 }
463
464 fn symbol_name_offsets(bytes: &[u8]) -> HashMap<String, u32> {
465 let (symtab, _) = symtab_and_dysymtab(bytes);
466 let symbols = parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).unwrap();
467 let strings = StringTable::from_file(bytes, symtab.stroff, symtab.strsize).unwrap();
468 symbols
469 .iter()
470 .map(|symbol| {
471 (
472 strings.get(symbol.strx()).unwrap().to_string(),
473 symbol.strx(),
474 )
475 })
476 .collect()
477 }
478
479 fn indirect_symbol_table(bytes: &[u8]) -> Vec<u32> {
480 let (_, dysymtab) = symtab_and_dysymtab(bytes);
481 if dysymtab.nindirectsyms == 0 {
482 return Vec::new();
483 }
484 let start = dysymtab.indirectsymoff as usize;
485 let end = start + dysymtab.nindirectsyms as usize * 4;
486 bytes[start..end]
487 .chunks_exact(4)
488 .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
489 .collect()
490 }
491
492 fn indirect_symbol_identities(bytes: &[u8]) -> Vec<String> {
493 let (symtab, _) = symtab_and_dysymtab(bytes);
494 let symbols = parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).unwrap();
495 let strings = StringTable::from_file(bytes, symtab.stroff, symtab.strsize).unwrap();
496 indirect_symbol_table(bytes)
497 .into_iter()
498 .map(|index| {
499 if index & INDIRECT_SYMBOL_LOCAL != 0 {
500 if index & INDIRECT_SYMBOL_ABS != 0 {
501 "<LOCAL|ABS>".to_string()
502 } else {
503 "<LOCAL>".to_string()
504 }
505 } else if index & INDIRECT_SYMBOL_ABS != 0 {
506 "<ABS>".to_string()
507 } else {
508 let symbol = &symbols[index as usize];
509 strings.get(symbol.strx()).unwrap().to_string()
510 }
511 })
512 .collect()
513 }
514
515 fn raw_linkedit_data_cmd(bytes: &[u8], expected_cmd: u32) -> (u32, u32) {
516 let header = parse_header(bytes).unwrap();
517 let commands = parse_commands(&header, bytes).unwrap();
518 for cmd in commands {
519 if let LoadCommand::Raw { cmd, data, .. } = cmd {
520 if cmd == expected_cmd {
521 return (u32_le(&data[0..4]), u32_le(&data[4..8]));
522 }
523 }
524 }
525 panic!("missing raw linkedit command 0x{expected_cmd:x}");
526 }
527
528 fn linkedit_payload(bytes: &[u8], cmd: u32) -> Vec<u8> {
529 let (dataoff, datasize) = raw_linkedit_data_cmd(bytes, cmd);
530 if datasize == 0 {
531 return Vec::new();
532 }
533 bytes[dataoff as usize..(dataoff + datasize) as usize].to_vec()
534 }
535
536 fn decode_function_starts(bytes: &[u8]) -> Vec<u64> {
537 let payload = linkedit_payload(bytes, LC_FUNCTION_STARTS);
538 let mut offsets = Vec::new();
539 let mut cursor = 0usize;
540 let mut current = 0u64;
541 while cursor < payload.len() {
542 let (delta, used) = read_uleb(&payload[cursor..]).unwrap();
543 cursor += used;
544 if delta == 0 {
545 break;
546 }
547 current += delta;
548 offsets.push(current);
549 }
550 offsets
551 }
552
553 fn command_ids(bytes: &[u8]) -> Vec<u32> {
554 let header = parse_header(bytes).unwrap();
555 let commands = parse_commands(&header, bytes).unwrap();
556 commands
557 .into_iter()
558 .map(|cmd| match cmd {
559 LoadCommand::Segment64(_) => LC_SEGMENT_64,
560 LoadCommand::Symtab(_) => LC_SYMTAB,
561 LoadCommand::Dysymtab(_) => LC_DYSYMTAB,
562 LoadCommand::BuildVersion(_) => LC_BUILD_VERSION,
563 LoadCommand::Dylib(d) => d.cmd,
564 LoadCommand::DyldInfoOnly(_) => LC_DYLD_INFO_ONLY,
565 LoadCommand::Raw { cmd, .. } => cmd,
566 other => panic!("unexpected load command in command_ids helper: {other:?}"),
567 })
568 .collect()
569 }
570
571 fn normalize_function_start_offsets(starts: &[u64]) -> Vec<u64> {
572 let Some(&base) = starts.first() else {
573 return Vec::new();
574 };
575 starts.iter().map(|offset| offset - base).collect()
576 }
577
578 #[derive(Debug, Clone, PartialEq, Eq)]
579 struct DataInCodeRecord {
580 offset: u32,
581 length: u16,
582 kind: u16,
583 }
584
585 fn rebased_unwind_bytes(bytes: &[u8]) -> Vec<u8> {
586 let header_base = segment_vmaddr(bytes, "__TEXT").unwrap_or(0);
587 let text_base = output_section(bytes, "__TEXT", "__text").unwrap().0 - header_base;
588 let got_range = output_section(bytes, "__DATA_CONST", "__got")
589 .map(|(addr, data)| (addr - header_base, addr - header_base + data.len() as u64));
590 let lsda_base =
591 output_section(bytes, "__TEXT", "__gcc_except_tab").map(|(addr, _)| addr - header_base);
592 let (_, unwind) = output_section(bytes, "__TEXT", "__unwind_info").unwrap();
593 let mut out = unwind;
594 if out.len() < 28 {
595 return out;
596 }
597
598 let personalities_offset = u32_le(&out[12..16]) as usize;
599 let personalities_count = u32_le(&out[16..20]) as usize;
600 let indices_offset = u32_le(&out[20..24]) as usize;
601 let indices_count = u32_le(&out[24..28]) as usize;
602
603 for idx in 0..personalities_count {
604 let off = personalities_offset + idx * 4;
605 let value = u32_le(&out[off..off + 4]) as u64;
606 let rebased = if let Some((got_start, got_end)) = got_range {
607 if got_start <= value && value < got_end {
608 value - got_start
609 } else if value >= text_base {
610 value - text_base
611 } else {
612 value
613 }
614 } else if value >= text_base {
615 value - text_base
616 } else {
617 value
618 };
619 out[off..off + 4].copy_from_slice(&(rebased as u32).to_le_bytes());
620 }
621
622 let mut lsda_offsets = Vec::with_capacity(indices_count);
623 for idx in 0..indices_count {
624 let entry_off = indices_offset + idx * 12;
625 let function_offset = u32_le(&out[entry_off..entry_off + 4]) as u64;
626 let rebased = function_offset.saturating_sub(text_base);
627 out[entry_off..entry_off + 4].copy_from_slice(&(rebased as u32).to_le_bytes());
628 lsda_offsets.push(u32_le(&out[entry_off + 8..entry_off + 12]) as usize);
629 }
630
631 if let (Some(lsda_base), Some(&start), Some(&end)) =
632 (lsda_base, lsda_offsets.first(), lsda_offsets.last())
633 {
634 let mut entry_off = start;
635 while entry_off < end {
636 let function_offset = u32_le(&out[entry_off..entry_off + 4]) as u64;
637 let lsda_offset = u32_le(&out[entry_off + 4..entry_off + 8]) as u64;
638 out[entry_off..entry_off + 4]
639 .copy_from_slice(&(function_offset.saturating_sub(text_base) as u32).to_le_bytes());
640 out[entry_off + 4..entry_off + 8]
641 .copy_from_slice(&(lsda_offset.saturating_sub(lsda_base) as u32).to_le_bytes());
642 entry_off += 8;
643 }
644 }
645
646 out
647 }
648
649 fn normalized_eh_frame_dump(path: &PathBuf, text_base: u64) -> Result<String, String> {
650 let output = Command::new("xcrun")
651 .args(["dwarfdump", "--eh-frame"])
652 .arg(path)
653 .output()
654 .map_err(|e| format!("spawn xcrun dwarfdump: {e}"))?;
655 if !output.status.success() {
656 return Err(format!(
657 "xcrun dwarfdump failed: {}",
658 String::from_utf8_lossy(&output.stderr)
659 ));
660 }
661 let mut normalized = Vec::new();
662 for line in String::from_utf8_lossy(&output.stdout).lines() {
663 let trimmed = line.trim();
664 if trimmed.starts_with("0x") && trimmed.contains(": CFA=") {
665 let (addr, rest) = trimmed.split_once(':').unwrap();
666 let value = u64::from_str_radix(addr.trim_start_matches("0x"), 16).unwrap();
667 normalized.push(format!("0x{:x}:{}", value - text_base, rest));
668 continue;
669 }
670 if let Some(pc_idx) = trimmed.find("pc=") {
671 let prefix = &trimmed[..pc_idx + 3];
672 let range = &trimmed[pc_idx + 3..];
673 if let Some((start, end)) = range.split_once("...") {
674 let start = u64::from_str_radix(start, 16).unwrap();
675 let end = u64::from_str_radix(end, 16).unwrap();
676 normalized.push(format!(
677 "{}0x{:x}...0x{:x}",
678 prefix,
679 start - text_base,
680 end - text_base
681 ));
682 continue;
683 }
684 }
685 if trimmed.is_empty()
686 || trimmed.starts_with(".debug_frame")
687 || trimmed.starts_with(".eh_frame")
688 || trimmed.ends_with("file format Mach-O arm64")
689 {
690 continue;
691 }
692 normalized.push(rebase_hex_addresses(trimmed, text_base));
693 }
694 Ok(normalized.join("\n"))
695 }
696
697 fn canonical_unwind_info(bytes: &[u8]) -> afs_ld::synth::unwind::DecodedUnwindInfo {
698 let (_, unwind) = output_section(bytes, "__TEXT", "__unwind_info").unwrap();
699 let mut decoded = decode_unwind_info(&unwind).unwrap();
700 let header_base = segment_vmaddr(bytes, "__TEXT").unwrap_or(0);
701 let text_base = output_section(bytes, "__TEXT", "__text").unwrap().0 - header_base;
702 for record in &mut decoded.records {
703 record.function_offset -= text_base as u32;
704 }
705 if let Some((lsda_addr, _)) = output_section(bytes, "__TEXT", "__gcc_except_tab") {
706 let lsda_base = lsda_addr - header_base;
707 for record in &mut decoded.lsdas {
708 record.function_offset -= text_base as u32;
709 record.lsda_offset -= lsda_base as u32;
710 }
711 }
712 if let Some((got_addr, got)) = output_section(bytes, "__DATA_CONST", "__got") {
713 let got_base = got_addr - header_base;
714 let got_end = got_base + got.len() as u64;
715 for personality in &mut decoded.personalities {
716 let offset = *personality as u64;
717 if got_base <= offset && offset < got_end {
718 *personality -= got_base as u32;
719 }
720 }
721 }
722 decoded
723 }
724
725 fn rebase_hex_addresses(line: &str, text_base: u64) -> String {
726 let bytes = line.as_bytes();
727 let mut out = String::new();
728 let mut idx = 0;
729 while idx < bytes.len() {
730 if idx + 2 <= bytes.len() && bytes[idx] == b'0' && bytes[idx + 1] == b'x' {
731 let mut end = idx + 2;
732 while end < bytes.len() && bytes[end].is_ascii_hexdigit() {
733 end += 1;
734 }
735 let token = &line[idx + 2..end];
736 let value = u64::from_str_radix(token, 16).unwrap();
737 if value >= text_base {
738 out.push_str(&format!("0x{:x}", value - text_base));
739 } else {
740 out.push_str(&line[idx..end]);
741 }
742 idx = end;
743 continue;
744 }
745 out.push(bytes[idx] as char);
746 idx += 1;
747 }
748 out
749 }
750
751 fn decode_data_in_code(bytes: &[u8]) -> Vec<DataInCodeRecord> {
752 let payload = linkedit_payload(bytes, LC_DATA_IN_CODE);
753 payload
754 .chunks_exact(8)
755 .map(|chunk| DataInCodeRecord {
756 offset: u32::from_le_bytes(chunk[0..4].try_into().unwrap()),
757 length: u16::from_le_bytes(chunk[4..6].try_into().unwrap()),
758 kind: u16::from_le_bytes(chunk[6..8].try_into().unwrap()),
759 })
760 .collect()
761 }
762
763 fn canonical_data_in_code(bytes: &[u8]) -> Vec<DataInCodeRecord> {
764 let text = output_section_header(bytes, "__TEXT", "__text").unwrap();
765 decode_data_in_code(bytes)
766 .into_iter()
767 .map(|record| DataInCodeRecord {
768 offset: record.offset - text.offset,
769 length: record.length,
770 kind: record.kind,
771 })
772 .collect()
773 }
774
775 fn assert_strtab_within_five_percent(ours: &[u8], apple: &[u8]) {
776 let delta = ours.len().abs_diff(apple.len());
777 assert!(
778 delta * 20 <= apple.len(),
779 "string table length drifted too far from Apple ld: ours={} apple={}",
780 ours.len(),
781 apple.len()
782 );
783 }
784
785 fn apple_link(
786 obj: &PathBuf,
787 out: &PathBuf,
788 entry: &str,
789 syslibroot: &str,
790 platform_version: &str,
791 ) -> Result<(), String> {
792 apple_link_with_args(obj, out, entry, syslibroot, platform_version, &[])
793 }
794
795 fn apple_link_with_args(
796 obj: &PathBuf,
797 out: &PathBuf,
798 entry: &str,
799 syslibroot: &str,
800 platform_version: &str,
801 extra_args: &[&str],
802 ) -> Result<(), String> {
803 let output = Command::new("xcrun")
804 .args([
805 "ld",
806 "-arch",
807 "arm64",
808 "-platform_version",
809 "macos",
810 platform_version,
811 platform_version,
812 "-syslibroot",
813 syslibroot,
814 "-lSystem",
815 "-e",
816 entry,
817 ])
818 .args(extra_args)
819 .arg("-o")
820 .arg(out)
821 .arg(obj)
822 .output()
823 .map_err(|e| format!("spawn xcrun ld: {e}"))?;
824 if !output.status.success() {
825 return Err(format!(
826 "xcrun ld failed: {}",
827 String::from_utf8_lossy(&output.stderr)
828 ));
829 }
830 Ok(())
831 }
832
833 fn apple_link_classic_lazy(
834 obj: &PathBuf,
835 out: &PathBuf,
836 entry: &str,
837 syslibroot: &str,
838 platform_version: &str,
839 ) -> Result<(), String> {
840 apple_link_with_args(
841 obj,
842 out,
843 entry,
844 syslibroot,
845 platform_version,
846 &["-no_fixup_chains"],
847 )
848 }
849
850 fn apple_link_dylib_classic(
851 obj: &PathBuf,
852 out: &PathBuf,
853 install_name: &str,
854 syslibroot: &str,
855 platform_version: &str,
856 ) -> Result<(), String> {
857 let output = Command::new("xcrun")
858 .args([
859 "ld",
860 "-dylib",
861 "-arch",
862 "arm64",
863 "-platform_version",
864 "macos",
865 platform_version,
866 platform_version,
867 "-syslibroot",
868 syslibroot,
869 "-lSystem",
870 "-install_name",
871 install_name,
872 "-no_fixup_chains",
873 ])
874 .arg("-o")
875 .arg(out)
876 .arg(obj)
877 .output()
878 .map_err(|e| format!("spawn xcrun ld -dylib: {e}"))?;
879 if !output.status.success() {
880 return Err(format!(
881 "xcrun ld -dylib failed: {}",
882 String::from_utf8_lossy(&output.stderr)
883 ));
884 }
885 Ok(())
886 }
887
888 fn apple_link_cxx_classic(obj: &PathBuf, out: &PathBuf) -> Result<(), String> {
889 let output = Command::new("xcrun")
890 .args([
891 "--sdk",
892 "macosx",
893 "clang++",
894 "-arch",
895 "arm64",
896 "-Wl,-no_fixup_chains",
897 "-o",
898 ])
899 .arg(out)
900 .arg(obj)
901 .output()
902 .map_err(|e| format!("spawn xcrun clang++ link: {e}"))?;
903 if !output.status.success() {
904 return Err(format!(
905 "xcrun clang++ link failed: {}",
906 String::from_utf8_lossy(&output.stderr)
907 ));
908 }
909 Ok(())
910 }
911
912 #[derive(Debug, Clone, PartialEq, Eq)]
913 struct RebaseRecord {
914 segment: String,
915 section: String,
916 section_offset: u64,
917 rebase_type: u8,
918 }
919
920 #[derive(Debug, Clone, PartialEq, Eq)]
921 struct BindRecord {
922 segment: String,
923 section: String,
924 section_offset: u64,
925 ordinal: u16,
926 symbol: String,
927 weak_import: bool,
928 }
929
930 #[derive(Debug, Clone)]
931 struct SegmentView {
932 name: String,
933 vm_addr: u64,
934 vm_size: u64,
935 sections: Vec<Section64Header>,
936 }
937
938 fn dyld_info_command(bytes: &[u8]) -> Result<afs_ld::macho::reader::DyldInfoCmd, String> {
939 let header = parse_header(bytes).map_err(|e| e.to_string())?;
940 let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
941 commands
942 .into_iter()
943 .find_map(|cmd| match cmd {
944 LoadCommand::DyldInfoOnly(cmd) => Some(cmd),
945 _ => None,
946 })
947 .ok_or_else(|| "missing LC_DYLD_INFO_ONLY".to_string())
948 }
949
950 #[derive(Clone, Copy)]
951 enum DyldInfoStreamKind {
952 Rebase,
953 Bind,
954 WeakBind,
955 LazyBind,
956 Export,
957 }
958
959 fn dyld_info_stream(bytes: &[u8], kind: DyldInfoStreamKind) -> Result<Vec<u8>, String> {
960 let dyld_info = dyld_info_command(bytes)?;
961 let (off, size) = match kind {
962 DyldInfoStreamKind::Rebase => (dyld_info.rebase_off, dyld_info.rebase_size),
963 DyldInfoStreamKind::Bind => (dyld_info.bind_off, dyld_info.bind_size),
964 DyldInfoStreamKind::WeakBind => (dyld_info.weak_bind_off, dyld_info.weak_bind_size),
965 DyldInfoStreamKind::LazyBind => (dyld_info.lazy_bind_off, dyld_info.lazy_bind_size),
966 DyldInfoStreamKind::Export => (dyld_info.export_off, dyld_info.export_size),
967 };
968 if size == 0 {
969 return Ok(Vec::new());
970 }
971 let start = off as usize;
972 let end = start + size as usize;
973 bytes
974 .get(start..end)
975 .map(|slice| slice.to_vec())
976 .ok_or_else(|| "dyld-info stream out of bounds".to_string())
977 }
978
979 fn canonical_lazy_bind_stream(bytes: &[u8]) -> Result<Vec<u8>, String> {
980 let mut stream = dyld_info_stream(bytes, DyldInfoStreamKind::LazyBind)?;
981 while stream.len() >= 2
982 && stream[stream.len() - 1] == BIND_OPCODE_DONE
983 && stream[stream.len() - 2] == BIND_OPCODE_DONE
984 {
985 stream.pop();
986 }
987 Ok(stream)
988 }
989
990 fn segment_views(bytes: &[u8]) -> Result<Vec<SegmentView>, String> {
991 let header = parse_header(bytes).map_err(|e| e.to_string())?;
992 let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
993 Ok(commands
994 .into_iter()
995 .filter_map(|cmd| match cmd {
996 LoadCommand::Segment64(seg) => Some(SegmentView {
997 name: seg.segname_str().to_string(),
998 vm_addr: seg.vmaddr,
999 vm_size: seg.vmsize,
1000 sections: seg.sections,
1001 }),
1002 _ => None,
1003 })
1004 .collect())
1005 }
1006
1007 fn read_cstr(bytes: &[u8], cursor: &mut usize) -> Result<String, String> {
1008 let start = *cursor;
1009 let end = bytes[start..]
1010 .iter()
1011 .position(|b| *b == 0)
1012 .map(|len| start + len)
1013 .ok_or_else(|| "unterminated dyld-info string".to_string())?;
1014 *cursor = end + 1;
1015 std::str::from_utf8(&bytes[start..end])
1016 .map(|s| s.to_string())
1017 .map_err(|e| format!("dyld-info string is not UTF-8: {e}"))
1018 }
1019
1020 fn locate_section(
1021 segments: &[SegmentView],
1022 segment_index: u8,
1023 segment_offset: u64,
1024 ) -> Result<(String, String, u64), String> {
1025 let segment = segments
1026 .get(segment_index as usize)
1027 .ok_or_else(|| format!("segment index {segment_index} out of range"))?;
1028 let addr = segment.vm_addr + segment_offset;
1029 for section in &segment.sections {
1030 if addr >= section.addr && addr < section.addr + section.size {
1031 return Ok((
1032 section.segname_str().to_string(),
1033 section.sectname_str().to_string(),
1034 addr - section.addr,
1035 ));
1036 }
1037 }
1038 if segment_offset <= segment.vm_size {
1039 return Ok((segment.name.clone(), String::new(), segment_offset));
1040 }
1041 Err(format!(
1042 "address 0x{addr:x} does not land in any section of {}",
1043 segment.name
1044 ))
1045 }
1046
1047 fn decode_rebase_records(bytes: &[u8]) -> Result<Vec<RebaseRecord>, String> {
1048 let dyld_info = dyld_info_command(bytes)?;
1049 if dyld_info.rebase_size == 0 {
1050 return Ok(Vec::new());
1051 }
1052 let segments = segment_views(bytes)?;
1053 let start = dyld_info.rebase_off as usize;
1054 let end = start + dyld_info.rebase_size as usize;
1055 let stream = bytes
1056 .get(start..end)
1057 .ok_or_else(|| "rebase stream out of bounds".to_string())?;
1058
1059 let mut out = Vec::new();
1060 let mut cursor = 0usize;
1061 let mut segment_index = 0u8;
1062 let mut segment_offset = 0u64;
1063 let mut rebase_type = 0u8;
1064 while cursor < stream.len() {
1065 let byte = stream[cursor];
1066 cursor += 1;
1067 let opcode = byte & REBASE_OPCODE_MASK;
1068 let imm = byte & REBASE_IMMEDIATE_MASK;
1069 match opcode {
1070 REBASE_OPCODE_DONE => break,
1071 REBASE_OPCODE_SET_TYPE_IMM => rebase_type = imm,
1072 REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB => {
1073 segment_index = imm;
1074 let (offset, len) =
1075 read_uleb(&stream[cursor..]).map_err(|e| format!("rebase ULEB: {e}"))?;
1076 cursor += len;
1077 segment_offset = offset;
1078 }
1079 REBASE_OPCODE_ADD_ADDR_ULEB => {
1080 let (delta, len) =
1081 read_uleb(&stream[cursor..]).map_err(|e| format!("rebase ULEB: {e}"))?;
1082 cursor += len;
1083 segment_offset += delta;
1084 }
1085 REBASE_OPCODE_ADD_ADDR_IMM_SCALED => {
1086 segment_offset += (imm as u64) * 8;
1087 }
1088 REBASE_OPCODE_DO_REBASE_IMM_TIMES => {
1089 for _ in 0..imm {
1090 let (segment, section, section_offset) =
1091 locate_section(&segments, segment_index, segment_offset)?;
1092 out.push(RebaseRecord {
1093 segment,
1094 section,
1095 section_offset,
1096 rebase_type,
1097 });
1098 segment_offset += 8;
1099 }
1100 }
1101 REBASE_OPCODE_DO_REBASE_ULEB_TIMES => {
1102 let (count, len) =
1103 read_uleb(&stream[cursor..]).map_err(|e| format!("rebase ULEB: {e}"))?;
1104 cursor += len;
1105 for _ in 0..count {
1106 let (segment, section, section_offset) =
1107 locate_section(&segments, segment_index, segment_offset)?;
1108 out.push(RebaseRecord {
1109 segment,
1110 section,
1111 section_offset,
1112 rebase_type,
1113 });
1114 segment_offset += 8;
1115 }
1116 }
1117 REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB => {
1118 let (segment, section, section_offset) =
1119 locate_section(&segments, segment_index, segment_offset)?;
1120 out.push(RebaseRecord {
1121 segment,
1122 section,
1123 section_offset,
1124 rebase_type,
1125 });
1126 segment_offset += 8;
1127 let (delta, len) =
1128 read_uleb(&stream[cursor..]).map_err(|e| format!("rebase ULEB: {e}"))?;
1129 cursor += len;
1130 segment_offset += delta;
1131 }
1132 REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB => {
1133 let (count, count_len) =
1134 read_uleb(&stream[cursor..]).map_err(|e| format!("rebase ULEB: {e}"))?;
1135 cursor += count_len;
1136 let (skip, skip_len) =
1137 read_uleb(&stream[cursor..]).map_err(|e| format!("rebase ULEB: {e}"))?;
1138 cursor += skip_len;
1139 for _ in 0..count {
1140 let (segment, section, section_offset) =
1141 locate_section(&segments, segment_index, segment_offset)?;
1142 out.push(RebaseRecord {
1143 segment,
1144 section,
1145 section_offset,
1146 rebase_type,
1147 });
1148 segment_offset += 8 + skip;
1149 }
1150 }
1151 _ => return Err(format!("unsupported rebase opcode 0x{byte:02x}")),
1152 }
1153 }
1154 Ok(out)
1155 }
1156
1157 fn decode_bind_records(bytes: &[u8], lazy: bool) -> Result<Vec<BindRecord>, String> {
1158 let dyld_info = dyld_info_command(bytes)?;
1159 let (off, size) = if lazy {
1160 (dyld_info.lazy_bind_off, dyld_info.lazy_bind_size)
1161 } else {
1162 (dyld_info.bind_off, dyld_info.bind_size)
1163 };
1164 if size == 0 {
1165 return Ok(Vec::new());
1166 }
1167 let segments = segment_views(bytes)?;
1168 let start = off as usize;
1169 let end = start + size as usize;
1170 let stream = bytes
1171 .get(start..end)
1172 .ok_or_else(|| "bind stream out of bounds".to_string())?;
1173
1174 let mut out = Vec::new();
1175 let mut cursor = 0usize;
1176 let mut segment_index = 0u8;
1177 let mut segment_offset = 0u64;
1178 let mut ordinal = 0u16;
1179 let mut symbol = String::new();
1180 let mut weak_import = false;
1181 while cursor < stream.len() {
1182 let byte = stream[cursor];
1183 cursor += 1;
1184 let opcode = byte & BIND_OPCODE_MASK;
1185 let imm = byte & BIND_IMMEDIATE_MASK;
1186 match opcode {
1187 BIND_OPCODE_DONE => {
1188 if lazy {
1189 symbol.clear();
1190 weak_import = false;
1191 } else {
1192 break;
1193 }
1194 }
1195 BIND_OPCODE_SET_DYLIB_ORDINAL_IMM => ordinal = imm as u16,
1196 BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB => {
1197 let (value, len) =
1198 read_uleb(&stream[cursor..]).map_err(|e| format!("bind ULEB: {e}"))?;
1199 cursor += len;
1200 ordinal = value as u16;
1201 }
1202 BIND_OPCODE_SET_DYLIB_SPECIAL_IMM => {
1203 let signed = ((imm as i8) << 4) >> 4;
1204 ordinal = signed as i16 as u16;
1205 }
1206 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM => {
1207 weak_import = (imm & BIND_SYMBOL_FLAGS_WEAK_IMPORT) != 0;
1208 symbol = read_cstr(stream, &mut cursor)?;
1209 }
1210 BIND_OPCODE_SET_TYPE_IMM => {}
1211 BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB => {
1212 segment_index = imm;
1213 let (offset, len) =
1214 read_uleb(&stream[cursor..]).map_err(|e| format!("bind ULEB: {e}"))?;
1215 cursor += len;
1216 segment_offset = offset;
1217 }
1218 BIND_OPCODE_ADD_ADDR_ULEB => {
1219 let (delta, len) =
1220 read_uleb(&stream[cursor..]).map_err(|e| format!("bind ULEB: {e}"))?;
1221 cursor += len;
1222 segment_offset += delta;
1223 }
1224 BIND_OPCODE_DO_BIND => {
1225 let (segment, section, section_offset) =
1226 locate_section(&segments, segment_index, segment_offset)?;
1227 out.push(BindRecord {
1228 segment,
1229 section,
1230 section_offset,
1231 ordinal,
1232 symbol: symbol.clone(),
1233 weak_import,
1234 });
1235 segment_offset += 8;
1236 }
1237 BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB => {
1238 let (segment, section, section_offset) =
1239 locate_section(&segments, segment_index, segment_offset)?;
1240 out.push(BindRecord {
1241 segment,
1242 section,
1243 section_offset,
1244 ordinal,
1245 symbol: symbol.clone(),
1246 weak_import,
1247 });
1248 segment_offset += 8;
1249 let (delta, len) =
1250 read_uleb(&stream[cursor..]).map_err(|e| format!("bind ULEB: {e}"))?;
1251 cursor += len;
1252 segment_offset += delta;
1253 }
1254 BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED => {
1255 let (segment, section, section_offset) =
1256 locate_section(&segments, segment_index, segment_offset)?;
1257 out.push(BindRecord {
1258 segment,
1259 section,
1260 section_offset,
1261 ordinal,
1262 symbol: symbol.clone(),
1263 weak_import,
1264 });
1265 segment_offset += 8 + (imm as u64) * 8;
1266 }
1267 BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB => {
1268 let (count, count_len) =
1269 read_uleb(&stream[cursor..]).map_err(|e| format!("bind ULEB: {e}"))?;
1270 cursor += count_len;
1271 let (skip, skip_len) =
1272 read_uleb(&stream[cursor..]).map_err(|e| format!("bind ULEB: {e}"))?;
1273 cursor += skip_len;
1274 for _ in 0..count {
1275 let (segment, section, section_offset) =
1276 locate_section(&segments, segment_index, segment_offset)?;
1277 out.push(BindRecord {
1278 segment,
1279 section,
1280 section_offset,
1281 ordinal,
1282 symbol: symbol.clone(),
1283 weak_import,
1284 });
1285 segment_offset += 8 + skip;
1286 }
1287 }
1288 _ => return Err(format!("unsupported bind opcode 0x{byte:02x}")),
1289 }
1290 }
1291 Ok(out)
1292 }
1293
1294 fn load_dylib_names(bytes: &[u8]) -> Result<Vec<String>, String> {
1295 let header = parse_header(bytes).map_err(|e| e.to_string())?;
1296 let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
1297 Ok(commands
1298 .into_iter()
1299 .filter_map(|cmd| match cmd {
1300 LoadCommand::Dylib(cmd) if cmd.cmd == afs_ld::macho::constants::LC_LOAD_DYLIB => {
1301 Some(cmd.name)
1302 }
1303 _ => None,
1304 })
1305 .collect())
1306 }
1307
1308 #[derive(Clone, Copy)]
1309 struct SectionCase {
1310 segname: &'static str,
1311 sectname: &'static str,
1312 }
1313
1314 #[derive(Clone, Copy)]
1315 enum PageRefKind {
1316 Add,
1317 Load,
1318 }
1319
1320 enum ParityCheck {
1321 ExactSections(&'static [SectionCase]),
1322 PageRef {
1323 section: SectionCase,
1324 site_offset: u64,
1325 target_offset: u64,
1326 kind: PageRefKind,
1327 },
1328 }
1329
1330 struct ParityCase {
1331 name: &'static str,
1332 src: &'static str,
1333 check: ParityCheck,
1334 }
1335
1336 struct ExportParityCase {
1337 name: &'static str,
1338 src: &'static str,
1339 }
1340
1341 struct ClassicLazyParityCase {
1342 name: &'static str,
1343 src: &'static str,
1344 }
1345
1346 struct DirectBindParityCase {
1347 name: &'static str,
1348 dylib_src: &'static str,
1349 main_src: &'static str,
1350 }
1351
1352 fn assert_case_matches_apple_ld(case: &ParityCase, sdk: &str, sdk_ver: &str) -> Result<(), String> {
1353 let obj = scratch(&format!("parity-{}.o", case.name));
1354 let our_out = scratch(&format!("parity-{}-ours.out", case.name));
1355 let apple_out = scratch(&format!("parity-{}-apple.out", case.name));
1356
1357 assemble(case.src, &obj)?;
1358
1359 let opts = LinkOptions {
1360 inputs: vec![obj.clone()],
1361 output: Some(our_out.clone()),
1362 kind: OutputKind::Executable,
1363 ..LinkOptions::default()
1364 };
1365 Linker::run(&opts).map_err(|e| format!("afs-ld link failed for {}: {e}", case.name))?;
1366 apple_link(&obj, &apple_out, "_main", sdk, sdk_ver)?;
1367
1368 let our_bytes = fs::read(&our_out).map_err(|e| format!("read our output: {e}"))?;
1369 let apple_bytes = fs::read(&apple_out).map_err(|e| format!("read apple output: {e}"))?;
1370
1371 match case.check {
1372 ParityCheck::ExactSections(sections) => {
1373 for section in sections {
1374 let (_, ours) = output_section(&our_bytes, section.segname, section.sectname)
1375 .ok_or_else(|| {
1376 format!(
1377 "missing our section {},{}",
1378 section.segname, section.sectname
1379 )
1380 })?;
1381 let (_, theirs) = output_section(&apple_bytes, section.segname, section.sectname)
1382 .ok_or_else(|| {
1383 format!(
1384 "missing apple section {},{}",
1385 section.segname, section.sectname
1386 )
1387 })?;
1388 let diff = diff_macho(&ours, &theirs);
1389 if !diff.is_clean() {
1390 return Err(format!(
1391 "{}: section {},{} diverged from Apple ld: {:#?}",
1392 case.name, section.segname, section.sectname, diff.critical
1393 ));
1394 }
1395 }
1396 }
1397 ParityCheck::PageRef {
1398 section,
1399 site_offset,
1400 target_offset,
1401 kind,
1402 } => {
1403 let (our_addr, our_bytes_sec) =
1404 output_section(&our_bytes, section.segname, section.sectname).ok_or_else(|| {
1405 format!(
1406 "missing our section {},{}",
1407 section.segname, section.sectname
1408 )
1409 })?;
1410 let (apple_addr, apple_bytes_sec) =
1411 output_section(&apple_bytes, section.segname, section.sectname).ok_or_else(
1412 || {
1413 format!(
1414 "missing apple section {},{}",
1415 section.segname, section.sectname
1416 )
1417 },
1418 )?;
1419 let our_target = decode_page_reference(&our_bytes_sec, our_addr, site_offset, &kind)?;
1420 let apple_target =
1421 decode_page_reference(&apple_bytes_sec, apple_addr, site_offset, &kind)?;
1422 let our_offset = our_target - our_addr;
1423 let apple_offset = apple_target - apple_addr;
1424 if our_offset != target_offset || apple_offset != target_offset {
1425 return Err(format!(
1426 "{}: decoded target offset mismatch (ours={our_offset:#x}, apple={apple_offset:#x}, expected={target_offset:#x})",
1427 case.name,
1428 ));
1429 }
1430 }
1431 }
1432
1433 let _ = fs::remove_file(obj);
1434 let _ = fs::remove_file(our_out);
1435 let _ = fs::remove_file(apple_out);
1436 Ok(())
1437 }
1438
1439 fn assert_dylib_export_case_matches_apple_ld(
1440 case: &ExportParityCase,
1441 sdk: &str,
1442 sdk_ver: &str,
1443 ) -> Result<(), String> {
1444 let obj = scratch(&format!("export-parity-{}.o", case.name));
1445 let our_out = scratch(&format!("export-parity-{}-ours.dylib", case.name));
1446 let apple_out = scratch(&format!("export-parity-{}-apple.dylib", case.name));
1447
1448 assemble(case.src, &obj)?;
1449
1450 let opts = LinkOptions {
1451 inputs: vec![obj.clone()],
1452 output: Some(our_out.clone()),
1453 kind: OutputKind::Dylib,
1454 ..LinkOptions::default()
1455 };
1456 Linker::run(&opts).map_err(|e| format!("afs-ld dylib link failed for {}: {e}", case.name))?;
1457 apple_link_dylib_classic(
1458 &obj,
1459 &apple_out,
1460 &format!("@rpath/{}.dylib", case.name),
1461 sdk,
1462 sdk_ver,
1463 )?;
1464
1465 let our_bytes = fs::read(&our_out).map_err(|e| format!("read our output: {e}"))?;
1466 let apple_bytes = fs::read(&apple_out).map_err(|e| format!("read apple output: {e}"))?;
1467 if canonical_export_records(&our_bytes) != canonical_export_records(&apple_bytes) {
1468 return Err(format!(
1469 "{}: canonical export records diverged:\nours={:#?}\napple={:#?}",
1470 case.name,
1471 canonical_export_records(&our_bytes),
1472 canonical_export_records(&apple_bytes)
1473 ));
1474 }
1475 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::WeakBind)
1476 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::WeakBind)
1477 {
1478 return Err(format!(
1479 "{}: weak-bind stream diverged from Apple ld",
1480 case.name
1481 ));
1482 }
1483 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::Export)
1484 .map_err(|e| format!("read our export stream: {e}"))?
1485 .is_empty()
1486 {
1487 return Err(format!("{}: expected non-empty export trie", case.name));
1488 }
1489
1490 let _ = fs::remove_file(obj);
1491 let _ = fs::remove_file(our_out);
1492 let _ = fs::remove_file(apple_out);
1493 Ok(())
1494 }
1495
1496 fn assert_classic_lazy_case_matches_apple_ld(
1497 case: &ClassicLazyParityCase,
1498 sdk: &str,
1499 sdk_ver: &str,
1500 ) -> Result<(), String> {
1501 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
1502 if !tbd.exists() {
1503 return Err(format!("no libSystem.tbd at {}", tbd.display()));
1504 }
1505
1506 let obj = scratch(&format!("classic-lazy-{}.o", case.name));
1507 let our_out = scratch(&format!("classic-lazy-{}-ours.out", case.name));
1508 let apple_out = scratch(&format!("classic-lazy-{}-apple.out", case.name));
1509
1510 assemble(case.src, &obj)?;
1511
1512 let opts = LinkOptions {
1513 inputs: vec![obj.clone(), tbd],
1514 output: Some(our_out.clone()),
1515 kind: OutputKind::Executable,
1516 ..LinkOptions::default()
1517 };
1518 Linker::run(&opts)
1519 .map_err(|e| format!("afs-ld classic-lazy link failed for {}: {e}", case.name))?;
1520 apple_link_classic_lazy(&obj, &apple_out, "_main", sdk, sdk_ver)?;
1521
1522 let our_bytes = fs::read(&our_out).map_err(|e| format!("read our output: {e}"))?;
1523 let apple_bytes = fs::read(&apple_out).map_err(|e| format!("read apple output: {e}"))?;
1524
1525 for (segname, sectname) in [("__TEXT", "__stubs"), ("__TEXT", "__stub_helper")] {
1526 let (_, ours) = output_section(&our_bytes, segname, sectname)
1527 .ok_or_else(|| format!("{}: missing our section {segname},{sectname}", case.name))?;
1528 let (_, theirs) = output_section(&apple_bytes, segname, sectname)
1529 .ok_or_else(|| format!("{}: missing apple section {segname},{sectname}", case.name))?;
1530 let diff = diff_macho(&ours, &theirs);
1531 if !diff.is_clean() {
1532 return Err(format!(
1533 "{}: section {},{} diverged from Apple ld: {:#?}",
1534 case.name, segname, sectname, diff.critical
1535 ));
1536 }
1537 }
1538
1539 if load_dylib_names(&our_bytes).map_err(|e| format!("our dylibs: {e}"))?
1540 != load_dylib_names(&apple_bytes).map_err(|e| format!("apple dylibs: {e}"))?
1541 {
1542 return Err(format!(
1543 "{}: LC_LOAD_DYLIB set diverged from Apple ld",
1544 case.name
1545 ));
1546 }
1547 if segment_flags(&our_bytes, "__DATA_CONST") != segment_flags(&apple_bytes, "__DATA_CONST") {
1548 return Err(format!(
1549 "{}: __DATA_CONST flags diverged from Apple ld",
1550 case.name
1551 ));
1552 }
1553 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::Rebase)
1554 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::Rebase)
1555 {
1556 return Err(format!(
1557 "{}: rebase stream diverged from Apple ld",
1558 case.name
1559 ));
1560 }
1561 if decode_rebase_records(&our_bytes).map_err(|e| format!("our rebases: {e}"))?
1562 != decode_rebase_records(&apple_bytes).map_err(|e| format!("apple rebases: {e}"))?
1563 {
1564 return Err(format!(
1565 "{}: rebase records diverged from Apple ld",
1566 case.name
1567 ));
1568 }
1569 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::Bind)
1570 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::Bind)
1571 {
1572 return Err(format!("{}: bind stream diverged from Apple ld", case.name));
1573 }
1574 if decode_bind_records(&our_bytes, false).map_err(|e| format!("our binds: {e}"))?
1575 != decode_bind_records(&apple_bytes, false).map_err(|e| format!("apple binds: {e}"))?
1576 {
1577 return Err(format!(
1578 "{}: bind records diverged from Apple ld",
1579 case.name
1580 ));
1581 }
1582 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::WeakBind)
1583 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::WeakBind)
1584 {
1585 return Err(format!(
1586 "{}: weak-bind stream diverged from Apple ld",
1587 case.name
1588 ));
1589 }
1590 if dyld_info_export_names(&our_bytes).map_err(|e| format!("our executable exports: {e}"))?
1591 != dyld_info_export_names(&apple_bytes)
1592 .map_err(|e| format!("apple executable exports: {e}"))?
1593 {
1594 return Err(format!(
1595 "{}: executable export trie diverged from Apple ld",
1596 case.name
1597 ));
1598 }
1599 if decode_bind_records(&our_bytes, true).map_err(|e| format!("our lazy binds: {e}"))?
1600 != decode_bind_records(&apple_bytes, true).map_err(|e| format!("apple lazy binds: {e}"))?
1601 {
1602 return Err(format!(
1603 "{}: lazy bind records diverged from Apple ld",
1604 case.name
1605 ));
1606 }
1607 if canonical_lazy_bind_stream(&our_bytes).map_err(|e| format!("our lazy stream: {e}"))?
1608 != canonical_lazy_bind_stream(&apple_bytes)
1609 .map_err(|e| format!("apple lazy stream: {e}"))?
1610 {
1611 return Err(format!(
1612 "{}: canonical lazy-bind stream diverged from Apple ld",
1613 case.name
1614 ));
1615 }
1616 if indirect_symbol_table(&our_bytes) != indirect_symbol_table(&apple_bytes) {
1617 return Err(format!(
1618 "{}: indirect symbol table diverged from Apple ld",
1619 case.name
1620 ));
1621 }
1622
1623 let _ = fs::remove_file(obj);
1624 let _ = fs::remove_file(our_out);
1625 let _ = fs::remove_file(apple_out);
1626 Ok(())
1627 }
1628
1629 fn assert_direct_bind_case_matches_apple_ld(
1630 case: &DirectBindParityCase,
1631 sdk: &str,
1632 sdk_ver: &str,
1633 ) -> Result<(), String> {
1634 let dylib = scratch(&format!("direct-bind-{}.dylib", case.name));
1635 let obj = scratch(&format!("direct-bind-{}.o", case.name));
1636 let our_out = scratch(&format!("direct-bind-{}-ours.out", case.name));
1637 let apple_out = scratch(&format!("direct-bind-{}-apple.out", case.name));
1638
1639 compile_dylib_c(case.dylib_src, &dylib)?;
1640 compile_c(case.main_src, &obj)?;
1641
1642 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
1643 if !tbd.exists() {
1644 return Err(format!("no libSystem.tbd at {}", tbd.display()));
1645 }
1646
1647 let opts = LinkOptions {
1648 inputs: vec![obj.clone(), tbd, dylib.clone()],
1649 output: Some(our_out.clone()),
1650 kind: OutputKind::Executable,
1651 ..LinkOptions::default()
1652 };
1653 Linker::run(&opts)
1654 .map_err(|e| format!("afs-ld direct-bind link failed for {}: {e}", case.name))?;
1655 let apple = Command::new("xcrun")
1656 .args([
1657 "ld",
1658 "-arch",
1659 "arm64",
1660 "-platform_version",
1661 "macos",
1662 sdk_ver,
1663 sdk_ver,
1664 "-syslibroot",
1665 sdk,
1666 "-no_fixup_chains",
1667 "-lSystem",
1668 "-e",
1669 "_main",
1670 "-o",
1671 ])
1672 .arg(&apple_out)
1673 .arg(&obj)
1674 .arg(&dylib)
1675 .output()
1676 .map_err(|e| format!("spawn xcrun ld: {e}"))?;
1677 if !apple.status.success() {
1678 return Err(format!(
1679 "xcrun ld failed for {}: {}",
1680 case.name,
1681 String::from_utf8_lossy(&apple.stderr)
1682 ));
1683 }
1684
1685 let our_bytes = fs::read(&our_out).map_err(|e| format!("read our output: {e}"))?;
1686 let apple_bytes = fs::read(&apple_out).map_err(|e| format!("read apple output: {e}"))?;
1687
1688 if load_dylib_names(&our_bytes).map_err(|e| format!("our dylibs: {e}"))?
1689 != load_dylib_names(&apple_bytes).map_err(|e| format!("apple dylibs: {e}"))?
1690 {
1691 return Err(format!(
1692 "{}: LC_LOAD_DYLIB set diverged from Apple ld",
1693 case.name
1694 ));
1695 }
1696 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::Rebase)
1697 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::Rebase)
1698 {
1699 return Err(format!(
1700 "{}: rebase stream diverged from Apple ld",
1701 case.name
1702 ));
1703 }
1704 if decode_rebase_records(&our_bytes).map_err(|e| format!("our rebases: {e}"))?
1705 != decode_rebase_records(&apple_bytes).map_err(|e| format!("apple rebases: {e}"))?
1706 {
1707 return Err(format!(
1708 "{}: rebase records diverged from Apple ld",
1709 case.name
1710 ));
1711 }
1712 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::Bind)
1713 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::Bind)
1714 {
1715 return Err(format!("{}: bind stream diverged from Apple ld", case.name));
1716 }
1717 if decode_bind_records(&our_bytes, false).map_err(|e| format!("our binds: {e}"))?
1718 != decode_bind_records(&apple_bytes, false).map_err(|e| format!("apple binds: {e}"))?
1719 {
1720 return Err(format!(
1721 "{}: bind records diverged from Apple ld",
1722 case.name
1723 ));
1724 }
1725 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::WeakBind)
1726 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::WeakBind)
1727 {
1728 return Err(format!(
1729 "{}: weak-bind stream diverged from Apple ld",
1730 case.name
1731 ));
1732 }
1733 if dyld_info_export_names(&our_bytes).map_err(|e| format!("our executable exports: {e}"))?
1734 != dyld_info_export_names(&apple_bytes)
1735 .map_err(|e| format!("apple executable exports: {e}"))?
1736 {
1737 return Err(format!(
1738 "{}: executable export trie diverged from Apple ld",
1739 case.name
1740 ));
1741 }
1742 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::LazyBind)
1743 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::LazyBind)
1744 {
1745 return Err(format!(
1746 "{}: lazy-bind stream diverged from Apple ld",
1747 case.name
1748 ));
1749 }
1750
1751 let _ = fs::remove_file(dylib);
1752 let _ = fs::remove_file(obj);
1753 let _ = fs::remove_file(our_out);
1754 let _ = fs::remove_file(apple_out);
1755 Ok(())
1756 }
1757
1758 fn decode_page_reference(
1759 bytes: &[u8],
1760 section_addr: u64,
1761 site_offset: u64,
1762 kind: &PageRefKind,
1763 ) -> Result<u64, String> {
1764 let start = site_offset as usize;
1765 let adrp = read_insn(bytes, start)?;
1766 let second = read_insn(bytes, start + 4)?;
1767 let place = section_addr + site_offset;
1768 let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
1769 let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
1770 let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
1771 let adrp_base = ((place as i64) & !0xfff) + (adrp_pages << 12);
1772 let low = match kind {
1773 PageRefKind::Add => ((second >> 10) & 0xfff) as u64,
1774 PageRefKind::Load => {
1775 let shift = ((second >> 30) & 0b11) as u64;
1776 (((second >> 10) & 0xfff) as u64) << shift
1777 }
1778 };
1779 Ok((adrp_base as u64) + low)
1780 }
1781
1782 fn decode_branch_target(bytes: &[u8], section_addr: u64, site_offset: u64) -> Result<u64, String> {
1783 let insn = read_insn(bytes, site_offset as usize)?;
1784 let imm26 = (insn & 0x03ff_ffff) as i64;
1785 let imm = sign_extend_26(imm26) << 2;
1786 Ok(section_addr
1787 .wrapping_add(site_offset)
1788 .wrapping_add_signed(imm))
1789 }
1790
1791 fn read_insn(bytes: &[u8], start: usize) -> Result<u32, String> {
1792 let end = start + 4;
1793 let slice = bytes
1794 .get(start..end)
1795 .ok_or_else(|| format!("instruction read OOB at 0x{start:x}"))?;
1796 Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
1797 }
1798
1799 fn sign_extend_26(value: i64) -> i64 {
1800 if value & (1 << 25) != 0 {
1801 value | !0x03ff_ffff
1802 } else {
1803 value
1804 }
1805 }
1806
1807 #[test]
1808 fn linker_run_emits_non_empty_executable_from_real_object() {
1809 if !have_xcrun() || !have_tool("codesign") {
1810 eprintln!("skipping: xcrun as or codesign unavailable");
1811 return;
1812 }
1813 let Some(sdk) = sdk_path() else {
1814 eprintln!("skipping: xcrun --show-sdk-path unavailable");
1815 return;
1816 };
1817 let Some(sdk_ver) = sdk_version() else {
1818 eprintln!("skipping: xcrun --show-sdk-version unavailable");
1819 return;
1820 };
1821
1822 let obj = scratch("main.o");
1823 let out = scratch("a.out");
1824 let apple_out = scratch("a-apple.out");
1825 let src = r#"
1826 .section __TEXT,__text,regular,pure_instructions
1827 .globl _main
1828 _main:
1829 mov x0, #0
1830 ret
1831 .subsections_via_symbols
1832 "#;
1833 if let Err(e) = assemble(src, &obj) {
1834 eprintln!("skipping: assemble failed: {e}");
1835 return;
1836 }
1837
1838 let opts = LinkOptions {
1839 inputs: vec![obj.clone()],
1840 output: Some(out.clone()),
1841 kind: OutputKind::Executable,
1842 ..LinkOptions::default()
1843 };
1844 Linker::run(&opts).unwrap();
1845 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
1846
1847 let bytes = fs::read(&out).unwrap();
1848 let apple_bytes = fs::read(&apple_out).unwrap();
1849 let header = parse_header(&bytes).unwrap();
1850 let commands = parse_commands(&header, &bytes).unwrap();
1851 let mut text_size = 0u64;
1852 let mut has_dylinker = false;
1853 let mut has_uuid = false;
1854 let mut has_source_version = false;
1855 for cmd in commands {
1856 match cmd {
1857 LoadCommand::Segment64(seg) => {
1858 for section in seg.sections {
1859 if section.sectname_str() == "__text" {
1860 text_size = section.size;
1861 }
1862 }
1863 }
1864 LoadCommand::Raw { cmd, data, .. }
1865 if cmd == afs_ld::macho::constants::LC_LOAD_DYLINKER =>
1866 {
1867 has_dylinker = data
1868 .windows(b"/usr/lib/dyld\0".len())
1869 .any(|window| window == b"/usr/lib/dyld\0");
1870 }
1871 LoadCommand::Raw { cmd, data, .. } if cmd == afs_ld::macho::constants::LC_UUID => {
1872 has_uuid = data.len() == 16 && data.iter().any(|byte| *byte != 0);
1873 }
1874 LoadCommand::Raw { cmd, .. } if cmd == afs_ld::macho::constants::LC_SOURCE_VERSION => {
1875 has_source_version = true;
1876 }
1877 _ => {}
1878 }
1879 }
1880 assert!(text_size > 0, "expected non-empty __text output");
1881 assert!(
1882 has_dylinker,
1883 "expected LC_LOAD_DYLINKER in executable output"
1884 );
1885 assert!(has_uuid, "expected LC_UUID in executable output");
1886 assert!(
1887 has_source_version,
1888 "expected LC_SOURCE_VERSION in executable output"
1889 );
1890 let our_cmds: Vec<u32> = command_ids(&bytes)
1891 .into_iter()
1892 .filter(|cmd| *cmd != afs_ld::macho::constants::LC_LOAD_DYLIB)
1893 .collect();
1894 let apple_cmds: Vec<u32> = command_ids(&apple_bytes)
1895 .into_iter()
1896 .filter(|cmd| *cmd != afs_ld::macho::constants::LC_LOAD_DYLIB)
1897 .collect();
1898 assert_eq!(our_cmds, apple_cmds);
1899 assert!(
1900 fs::metadata(&out).unwrap().permissions().mode() & 0o111 != 0,
1901 "expected executable output mode"
1902 );
1903 let verify = Command::new("codesign")
1904 .arg("-v")
1905 .arg(&out)
1906 .output()
1907 .unwrap();
1908 assert!(
1909 verify.status.success(),
1910 "codesign verify failed: {}",
1911 String::from_utf8_lossy(&verify.stderr)
1912 );
1913 let status = Command::new(&out).status().unwrap();
1914 assert_eq!(status.code(), Some(0), "expected executable to exit 0");
1915
1916 let _ = fs::remove_file(obj);
1917 let _ = fs::remove_file(out);
1918 let _ = fs::remove_file(apple_out);
1919 }
1920
1921 #[test]
1922 fn linker_run_emits_minimal_dylib_from_real_object() {
1923 if !have_xcrun() {
1924 eprintln!("skipping: xcrun as unavailable");
1925 return;
1926 }
1927
1928 let obj = scratch("lib.o");
1929 let out = scratch("libtiny.dylib");
1930 let src = r#"
1931 .section __TEXT,__text,regular,pure_instructions
1932 .globl _exported
1933 _exported:
1934 ret
1935 .subsections_via_symbols
1936 "#;
1937 if let Err(e) = assemble(src, &obj) {
1938 eprintln!("skipping: assemble failed: {e}");
1939 return;
1940 }
1941
1942 let opts = LinkOptions {
1943 inputs: vec![obj.clone()],
1944 output: Some(out.clone()),
1945 kind: OutputKind::Dylib,
1946 ..LinkOptions::default()
1947 };
1948 Linker::run(&opts).unwrap();
1949
1950 let bytes = fs::read(&out).unwrap();
1951 let header = parse_header(&bytes).unwrap();
1952 let commands = parse_commands(&header, &bytes).unwrap();
1953 let dyld_info = commands.iter().find_map(|cmd| match cmd {
1954 LoadCommand::DyldInfoOnly(cmd) => Some(*cmd),
1955 _ => None,
1956 });
1957 assert_eq!(header.filetype, afs_ld::macho::constants::MH_DYLIB);
1958 assert!(commands.iter().any(
1959 |cmd| matches!(cmd, LoadCommand::Dylib(d) if d.cmd == afs_ld::macho::constants::LC_ID_DYLIB)
1960 ));
1961 let dyld_info = dyld_info.expect("expected LC_DYLD_INFO_ONLY in dylib output");
1962 assert!(dyld_info.export_size > 0, "expected non-empty export trie");
1963
1964 let dylib = DylibFile::parse(&out, &bytes).unwrap();
1965 let mut exports = dylib.exports.entries().unwrap();
1966 exports.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
1967 assert!(
1968 exports.iter().any(|entry| entry.name == "_exported"),
1969 "expected _exported in export trie, got {:?}",
1970 exports
1971 .iter()
1972 .map(|entry| entry.name.as_str())
1973 .collect::<Vec<_>>()
1974 );
1975
1976 let _ = fs::remove_file(obj);
1977 let _ = fs::remove_file(out);
1978 }
1979
1980 #[test]
1981 fn linker_run_uses_dylib_identity_flags() {
1982 if !have_xcrun() {
1983 eprintln!("skipping: xcrun as unavailable");
1984 return;
1985 }
1986
1987 let obj = scratch("libmeta.o");
1988 let out = scratch("libmeta.dylib");
1989 let src = r#"
1990 .section __TEXT,__text,regular,pure_instructions
1991 .globl _exported
1992 _exported:
1993 ret
1994 .subsections_via_symbols
1995 "#;
1996 if let Err(e) = assemble(src, &obj) {
1997 eprintln!("skipping: assemble failed: {e}");
1998 return;
1999 }
2000
2001 let opts = LinkOptions {
2002 inputs: vec![obj.clone()],
2003 output: Some(out.clone()),
2004 kind: OutputKind::Dylib,
2005 install_name: Some("@rpath/libmeta_custom.dylib".into()),
2006 current_version: Some((2 << 16) | (3 << 8) | 4),
2007 compatibility_version: Some((1 << 16) | (5 << 8)),
2008 rpaths: vec!["@loader_path/../lib".into()],
2009 ..LinkOptions::default()
2010 };
2011 Linker::run(&opts).unwrap();
2012
2013 let bytes = fs::read(&out).unwrap();
2014 let header = parse_header(&bytes).unwrap();
2015 let commands = parse_commands(&header, &bytes).unwrap();
2016 let id_dylib = commands
2017 .iter()
2018 .find_map(|cmd| match cmd {
2019 LoadCommand::Dylib(cmd) if cmd.cmd == afs_ld::macho::constants::LC_ID_DYLIB => {
2020 Some(cmd.clone())
2021 }
2022 _ => None,
2023 })
2024 .expect("missing LC_ID_DYLIB");
2025 assert_eq!(id_dylib.name, "@rpath/libmeta_custom.dylib");
2026 assert_eq!(id_dylib.current_version, (2 << 16) | (3 << 8) | 4);
2027 assert_eq!(id_dylib.compatibility_version, (1 << 16) | (5 << 8));
2028 assert!(commands
2029 .iter()
2030 .any(|cmd| matches!(cmd, LoadCommand::Rpath(r) if r.path == "@loader_path/../lib")));
2031
2032 let _ = fs::remove_file(obj);
2033 let _ = fs::remove_file(out);
2034 }
2035
2036 #[test]
2037 fn linker_run_honors_exported_symbol_filters_like_ld() {
2038 if !have_xcrun() {
2039 eprintln!("skipping: xcrun as unavailable");
2040 return;
2041 }
2042
2043 let obj = scratch("export-filter.o");
2044 let our_out = scratch("export-filter-ours.dylib");
2045 let apple_out = scratch("export-filter-apple.dylib");
2046 let list_path = scratch("export-filter-exports.txt");
2047 let src = r#"
2048 .section __TEXT,__text,regular,pure_instructions
2049 .globl _alpha
2050 .globl _beta
2051 .globl _gamma
2052 _alpha:
2053 ret
2054 _beta:
2055 ret
2056 _gamma:
2057 ret
2058 .subsections_via_symbols
2059 "#;
2060 if let Err(e) = assemble(src, &obj) {
2061 eprintln!("skipping: assemble failed: {e}");
2062 return;
2063 }
2064 fs::write(&list_path, "_bet?\n").unwrap();
2065
2066 let opts = LinkOptions {
2067 inputs: vec![obj.clone()],
2068 output: Some(our_out.clone()),
2069 kind: OutputKind::Dylib,
2070 exported_symbols: vec!["_alpha".into()],
2071 exported_symbols_lists: vec![list_path.clone()],
2072 ..LinkOptions::default()
2073 };
2074 Linker::run(&opts).unwrap();
2075
2076 let apple = Command::new("xcrun")
2077 .args(["clang", "-arch", "arm64", "-dynamiclib"])
2078 .arg(&obj)
2079 .arg("-o")
2080 .arg(&apple_out)
2081 .arg("-Wl,-exported_symbol,_alpha")
2082 .arg(format!(
2083 "-Wl,-exported_symbols_list,{}",
2084 list_path.display()
2085 ))
2086 .output()
2087 .unwrap();
2088 assert!(
2089 apple.status.success(),
2090 "xcrun ld failed: {}",
2091 String::from_utf8_lossy(&apple.stderr)
2092 );
2093
2094 let our_bytes = fs::read(&our_out).unwrap();
2095 let apple_bytes = fs::read(&apple_out).unwrap();
2096 assert_eq!(
2097 canonical_export_records(&our_bytes),
2098 canonical_export_records(&apple_bytes)
2099 );
2100 assert_eq!(
2101 dyld_info_export_names(&our_bytes).unwrap(),
2102 vec!["_alpha".to_string(), "_beta".to_string()]
2103 );
2104 assert_eq!(
2105 canonical_symbol_record_map(&our_bytes),
2106 canonical_symbol_record_map(&apple_bytes)
2107 );
2108
2109 let our_symbols = canonical_symbol_record_map(&our_bytes);
2110 let gamma = our_symbols.get("_gamma").expect("missing _gamma");
2111 assert_ne!(
2112 gamma.n_type & N_PEXT,
2113 0,
2114 "expected _gamma to be private extern"
2115 );
2116
2117 let _ = fs::remove_file(obj);
2118 let _ = fs::remove_file(our_out);
2119 let _ = fs::remove_file(apple_out);
2120 let _ = fs::remove_file(list_path);
2121 }
2122
2123 #[test]
2124 fn linker_run_honors_unexported_symbol_filters_like_ld() {
2125 if !have_xcrun() {
2126 eprintln!("skipping: xcrun as unavailable");
2127 return;
2128 }
2129
2130 let obj = scratch("unexport-filter.o");
2131 let our_out = scratch("unexport-filter-ours.dylib");
2132 let apple_out = scratch("unexport-filter-apple.dylib");
2133 let list_path = scratch("unexport-filter-hidden.txt");
2134 let src = r#"
2135 .section __TEXT,__text,regular,pure_instructions
2136 .globl _alpha
2137 .globl _beta
2138 .globl _gamma
2139 _alpha:
2140 ret
2141 _beta:
2142 ret
2143 _gamma:
2144 ret
2145 .subsections_via_symbols
2146 "#;
2147 if let Err(e) = assemble(src, &obj) {
2148 eprintln!("skipping: assemble failed: {e}");
2149 return;
2150 }
2151 fs::write(&list_path, "_bet?\n").unwrap();
2152
2153 let opts = LinkOptions {
2154 inputs: vec![obj.clone()],
2155 output: Some(our_out.clone()),
2156 kind: OutputKind::Dylib,
2157 unexported_symbols: vec!["_gamma".into()],
2158 unexported_symbols_lists: vec![list_path.clone()],
2159 ..LinkOptions::default()
2160 };
2161 Linker::run(&opts).unwrap();
2162
2163 let apple = Command::new("xcrun")
2164 .args(["clang", "-arch", "arm64", "-dynamiclib"])
2165 .arg(&obj)
2166 .arg("-o")
2167 .arg(&apple_out)
2168 .arg("-Wl,-unexported_symbol,_gamma")
2169 .arg(format!(
2170 "-Wl,-unexported_symbols_list,{}",
2171 list_path.display()
2172 ))
2173 .output()
2174 .unwrap();
2175 assert!(
2176 apple.status.success(),
2177 "xcrun ld failed: {}",
2178 String::from_utf8_lossy(&apple.stderr)
2179 );
2180
2181 let our_bytes = fs::read(&our_out).unwrap();
2182 let apple_bytes = fs::read(&apple_out).unwrap();
2183 assert_eq!(
2184 canonical_export_records(&our_bytes),
2185 canonical_export_records(&apple_bytes)
2186 );
2187 assert_eq!(
2188 dyld_info_export_names(&our_bytes).unwrap(),
2189 vec!["_alpha".to_string()]
2190 );
2191 assert_eq!(
2192 canonical_symbol_record_map(&our_bytes),
2193 canonical_symbol_record_map(&apple_bytes)
2194 );
2195
2196 let our_symbols = canonical_symbol_record_map(&our_bytes);
2197 for name in ["_beta", "_gamma"] {
2198 let record = our_symbols
2199 .get(name)
2200 .unwrap_or_else(|| panic!("missing {name}"));
2201 assert_ne!(
2202 record.n_type & N_PEXT,
2203 0,
2204 "expected {name} to be private extern"
2205 );
2206 }
2207
2208 let _ = fs::remove_file(obj);
2209 let _ = fs::remove_file(our_out);
2210 let _ = fs::remove_file(apple_out);
2211 let _ = fs::remove_file(list_path);
2212 }
2213
2214 #[test]
2215 fn linker_run_loads_minimal_dylib_via_dlopen() {
2216 if !have_xcrun() || !have_tool("codesign") {
2217 eprintln!("skipping: xcrun clang/as or codesign unavailable");
2218 return;
2219 }
2220
2221 let obj = scratch("libfoo_add.o");
2222 let out = scratch("libfoo_add.dylib");
2223 let caller_src = scratch("libfoo_add-caller.c");
2224 let caller = scratch("libfoo_add-caller.out");
2225 let src = r#"
2226 .section __TEXT,__text,regular,pure_instructions
2227 .globl _foo_add
2228 _foo_add:
2229 add w0, w0, w1
2230 ret
2231 .subsections_via_symbols
2232 "#;
2233 if let Err(e) = assemble(src, &obj) {
2234 eprintln!("skipping: assemble failed: {e}");
2235 return;
2236 }
2237
2238 let opts = LinkOptions {
2239 inputs: vec![obj.clone()],
2240 output: Some(out.clone()),
2241 kind: OutputKind::Dylib,
2242 ..LinkOptions::default()
2243 };
2244 Linker::run(&opts).unwrap();
2245
2246 let verify = Command::new("codesign")
2247 .arg("-v")
2248 .arg(&out)
2249 .output()
2250 .unwrap();
2251 assert!(
2252 verify.status.success(),
2253 "codesign verify failed: {}",
2254 String::from_utf8_lossy(&verify.stderr)
2255 );
2256
2257 fs::write(
2258 &caller_src,
2259 r#"
2260 #include <dlfcn.h>
2261 typedef int (*foo_add_fn)(int, int);
2262 int main(int argc, char **argv) {
2263 if (argc != 2) return 10;
2264 void *handle = dlopen(argv[1], RTLD_NOW);
2265 if (!handle) return 11;
2266 foo_add_fn fn = (foo_add_fn)dlsym(handle, "foo_add");
2267 if (!fn) return 12;
2268 int value = fn(2, 3);
2269 dlclose(handle);
2270 return value == 5 ? 0 : 1;
2271 }
2272 "#,
2273 )
2274 .unwrap();
2275
2276 let output = Command::new("xcrun")
2277 .args(["--sdk", "macosx", "clang", "-arch", "arm64"])
2278 .arg(&caller_src)
2279 .arg("-o")
2280 .arg(&caller)
2281 .output()
2282 .unwrap();
2283 assert!(
2284 output.status.success(),
2285 "xcrun clang caller failed: {}",
2286 String::from_utf8_lossy(&output.stderr)
2287 );
2288
2289 let status = Command::new(&caller).arg(&out).status().unwrap();
2290 assert_eq!(status.code(), Some(0), "expected dlopen caller to exit 0");
2291
2292 let _ = fs::remove_file(obj);
2293 let _ = fs::remove_file(out);
2294 let _ = fs::remove_file(caller_src);
2295 let _ = fs::remove_file(caller);
2296 }
2297
2298 #[test]
2299 fn dylib_export_surfaces_match_apple_ld() {
2300 if !have_xcrun() || !have_xcrun_tool("ld") {
2301 eprintln!("skipping: xcrun as/ld unavailable");
2302 return;
2303 }
2304 let Some(sdk) = sdk_path() else {
2305 eprintln!("skipping: xcrun --show-sdk-path unavailable");
2306 return;
2307 };
2308 let Some(sdk_ver) = sdk_version() else {
2309 eprintln!("skipping: xcrun --show-sdk-version unavailable");
2310 return;
2311 };
2312
2313 let case = ExportParityCase {
2314 name: "export-parity",
2315 src: r#"
2316 .section __TEXT,__text,regular,pure_instructions
2317 .globl _exported
2318 _exported:
2319 ret
2320 .subsections_via_symbols
2321 "#,
2322 };
2323 assert_dylib_export_case_matches_apple_ld(&case, &sdk, &sdk_ver).unwrap();
2324 }
2325
2326 #[test]
2327 fn dylib_export_surfaces_match_apple_ld_with_shared_prefixes() {
2328 if !have_xcrun() || !have_xcrun_tool("ld") {
2329 eprintln!("skipping: xcrun as/ld unavailable");
2330 return;
2331 }
2332 let Some(sdk) = sdk_path() else {
2333 eprintln!("skipping: xcrun --show-sdk-path unavailable");
2334 return;
2335 };
2336 let Some(sdk_ver) = sdk_version() else {
2337 eprintln!("skipping: xcrun --show-sdk-version unavailable");
2338 return;
2339 };
2340
2341 let case = ExportParityCase {
2342 name: "export-prefix-parity",
2343 src: r#"
2344 .section __TEXT,__text,regular,pure_instructions
2345 .globl _alpha
2346 _alpha:
2347 ret
2348 .globl _alphabet
2349 _alphabet:
2350 ret
2351 .globl _alphanumeric
2352 _alphanumeric:
2353 ret
2354 .subsections_via_symbols
2355 "#,
2356 };
2357 assert_dylib_export_case_matches_apple_ld(&case, &sdk, &sdk_ver).unwrap();
2358 }
2359
2360 #[test]
2361 fn dylib_export_surfaces_match_apple_ld_across_fixture_matrix() {
2362 if !have_xcrun() || !have_xcrun_tool("ld") {
2363 eprintln!("skipping: xcrun as/ld unavailable");
2364 return;
2365 }
2366 let Some(sdk) = sdk_path() else {
2367 eprintln!("skipping: xcrun --show-sdk-path unavailable");
2368 return;
2369 };
2370 let Some(sdk_ver) = sdk_version() else {
2371 eprintln!("skipping: xcrun --show-sdk-version unavailable");
2372 return;
2373 };
2374
2375 let cases = [
2376 ExportParityCase {
2377 name: "export-ordering",
2378 src: r#"
2379 .section __TEXT,__text,regular,pure_instructions
2380 .globl _zeta
2381 _zeta:
2382 ret
2383 .globl _alpha
2384 _alpha:
2385 ret
2386 .globl _middle
2387 _middle:
2388 ret
2389 .subsections_via_symbols
2390 "#,
2391 },
2392 ExportParityCase {
2393 name: "export-text-data",
2394 src: r#"
2395 .section __TEXT,__text,regular,pure_instructions
2396 .globl _code_symbol
2397 _code_symbol:
2398 ret
2399 .section __DATA,__data
2400 .p2align 3
2401 .globl _data_symbol
2402 _data_symbol:
2403 .quad 0x1234
2404 .globl _more_data
2405 _more_data:
2406 .long 7
2407 .subsections_via_symbols
2408 "#,
2409 },
2410 ExportParityCase {
2411 name: "export-text-const",
2412 src: r#"
2413 .section __TEXT,__text,regular,pure_instructions
2414 .globl _entry
2415 _entry:
2416 ret
2417 .section __TEXT,__const
2418 .p2align 3
2419 .globl _ro_value
2420 _ro_value:
2421 .quad 0xfeedface
2422 .subsections_via_symbols
2423 "#,
2424 },
2425 ExportParityCase {
2426 name: "export-bss",
2427 src: r#"
2428 .section __TEXT,__text,regular,pure_instructions
2429 .globl _touch
2430 _touch:
2431 ret
2432 .zerofill __DATA,__bss,_global_bss,16,3
2433 .subsections_via_symbols
2434 "#,
2435 },
2436 ExportParityCase {
2437 name: "export-prefix-fanout",
2438 src: r#"
2439 .section __TEXT,__text,regular,pure_instructions
2440 .globl _pre
2441 _pre:
2442 ret
2443 .globl _prefix
2444 _prefix:
2445 ret
2446 .globl _prefix_long
2447 _prefix_long:
2448 ret
2449 .globl _prefix_lone
2450 _prefix_lone:
2451 ret
2452 .subsections_via_symbols
2453 "#,
2454 },
2455 ExportParityCase {
2456 name: "export-shared-data-prefix",
2457 src: r#"
2458 .section __DATA,__data
2459 .p2align 3
2460 .globl _alpha_data
2461 _alpha_data:
2462 .quad 1
2463 .globl _alphabet_data
2464 _alphabet_data:
2465 .quad 2
2466 .globl _alphanumeric_data
2467 _alphanumeric_data:
2468 .quad 3
2469 .subsections_via_symbols
2470 "#,
2471 },
2472 ];
2473
2474 let mut failures = Vec::new();
2475 for case in &cases {
2476 if let Err(err) = assert_dylib_export_case_matches_apple_ld(case, &sdk, &sdk_ver) {
2477 failures.push(err);
2478 }
2479 }
2480
2481 assert!(
2482 failures.is_empty(),
2483 "Apple ld dylib export parity failures ({} cases):\n{}",
2484 failures.len(),
2485 failures.join("\n\n")
2486 );
2487 }
2488
2489 #[test]
2490 fn linker_run_reports_unresolved_symbol() {
2491 if !have_xcrun() {
2492 eprintln!("skipping: xcrun as unavailable");
2493 return;
2494 }
2495
2496 let obj = scratch("missing.o");
2497 let src = r#"
2498 .section __TEXT,__text,regular,pure_instructions
2499 .globl _main
2500 _main:
2501 bl _missing
2502 ret
2503 .subsections_via_symbols
2504 "#;
2505 if let Err(e) = assemble(src, &obj) {
2506 eprintln!("skipping: assemble failed: {e}");
2507 return;
2508 }
2509
2510 let opts = LinkOptions {
2511 inputs: vec![obj.clone()],
2512 output: Some(scratch("missing.out")),
2513 kind: OutputKind::Executable,
2514 ..LinkOptions::default()
2515 };
2516 let err = Linker::run(&opts).unwrap_err();
2517 match err {
2518 LinkError::UndefinedSymbols(msg) => {
2519 assert!(msg.contains("undefined symbol: _missing"), "{msg}");
2520 }
2521 other => panic!("expected UndefinedSymbols, got {other:?}"),
2522 }
2523
2524 let _ = fs::remove_file(obj);
2525 }
2526
2527 #[test]
2528 fn linker_run_promotes_unresolved_symbol_to_dynamic_lookup() {
2529 if !have_xcrun() {
2530 eprintln!("skipping: xcrun as unavailable");
2531 return;
2532 }
2533
2534 let obj = scratch("missing-dynamic.o");
2535 let out = scratch("missing-dynamic.out");
2536 let src = r#"
2537 .section __TEXT,__text,regular,pure_instructions
2538 .globl _main
2539 _main:
2540 mov w0, #0
2541 ret
2542 .section __DATA,__data
2543 .p2align 3
2544 .globl _missing_slot
2545 _missing_slot:
2546 .quad _missing
2547 .subsections_via_symbols
2548 "#;
2549 if let Err(e) = assemble(src, &obj) {
2550 eprintln!("skipping: assemble failed: {e}");
2551 return;
2552 }
2553
2554 let opts = LinkOptions {
2555 inputs: vec![obj.clone()],
2556 output: Some(out.clone()),
2557 kind: OutputKind::Executable,
2558 undefined_treatment: afs_ld::resolve::UndefinedTreatment::DynamicLookup,
2559 ..LinkOptions::default()
2560 };
2561 Linker::run(&opts).unwrap();
2562
2563 let bytes = fs::read(&out).unwrap();
2564 let bind_records = decode_bind_records(&bytes, false).unwrap();
2565 assert!(
2566 bind_records
2567 .iter()
2568 .any(|record| record.symbol == "_missing" && record.ordinal == 0xFFFE),
2569 "expected flat-lookup bind for _missing, got {bind_records:?}"
2570 );
2571 let (_, _, undefs) = symbol_partition_names(&bytes);
2572 assert_eq!(undefs, vec!["_missing".to_string()]);
2573
2574 let _ = fs::remove_file(obj);
2575 let _ = fs::remove_file(out);
2576 }
2577
2578 #[test]
2579 fn linker_run_reports_duplicate_from_fetched_archive_member() {
2580 if !have_xcrun() {
2581 eprintln!("skipping: xcrun as unavailable");
2582 return;
2583 }
2584
2585 let main_obj = scratch("dup-main.o");
2586 let dup_obj = scratch("dup-member.o");
2587 let archive = scratch("dup.a");
2588
2589 let main_src = r#"
2590 .section __TEXT,__text,regular,pure_instructions
2591 .globl _main
2592 .globl _dup
2593 _main:
2594 bl _archive_sym
2595 ret
2596 _dup:
2597 ret
2598 .subsections_via_symbols
2599 "#;
2600 let dup_src = r#"
2601 .section __TEXT,__text,regular,pure_instructions
2602 .globl _archive_sym
2603 .globl _dup
2604 _archive_sym:
2605 ret
2606 _dup:
2607 ret
2608 .subsections_via_symbols
2609 "#;
2610
2611 for (src, out) in [(&main_src, &main_obj), (&dup_src, &dup_obj)] {
2612 if let Err(e) = assemble(src, out) {
2613 eprintln!("skipping: assemble failed: {e}");
2614 return;
2615 }
2616 }
2617
2618 let ar = Command::new("ar")
2619 .arg("rcs")
2620 .arg(&archive)
2621 .arg(&dup_obj)
2622 .output()
2623 .unwrap();
2624 if !ar.status.success() {
2625 eprintln!(
2626 "skipping: ar failed: {}",
2627 String::from_utf8_lossy(&ar.stderr)
2628 );
2629 return;
2630 }
2631
2632 let opts = LinkOptions {
2633 inputs: vec![main_obj.clone(), archive.clone()],
2634 output: Some(scratch("dup.out")),
2635 kind: OutputKind::Executable,
2636 ..LinkOptions::default()
2637 };
2638 let err = Linker::run(&opts).unwrap_err();
2639 match err {
2640 LinkError::DuplicateSymbols(msg) => {
2641 assert!(msg.contains("duplicate symbol _dup"), "{msg}");
2642 }
2643 other => panic!("expected DuplicateSymbols, got {other:?}"),
2644 }
2645
2646 let _ = fs::remove_file(main_obj);
2647 let _ = fs::remove_file(dup_obj);
2648 let _ = fs::remove_file(archive);
2649 }
2650
2651 #[test]
2652 fn fetched_archive_member_undefined_reports_member_referrer() {
2653 if !have_xcrun() {
2654 eprintln!("skipping: xcrun as unavailable");
2655 return;
2656 }
2657
2658 let main_obj = scratch("member-main.o");
2659 let member_obj = scratch("member-undef.o");
2660 let archive = scratch("member.a");
2661
2662 let main_src = r#"
2663 .section __TEXT,__text,regular,pure_instructions
2664 .globl _main
2665 _main:
2666 bl _archive_sym
2667 ret
2668 .subsections_via_symbols
2669 "#;
2670 let member_src = r#"
2671 .section __TEXT,__text,regular,pure_instructions
2672 .globl _archive_sym
2673 _archive_sym:
2674 bl _missing_from_member
2675 ret
2676 .subsections_via_symbols
2677 "#;
2678
2679 for (src, out) in [(&main_src, &main_obj), (&member_src, &member_obj)] {
2680 if let Err(e) = assemble(src, out) {
2681 eprintln!("skipping: assemble failed: {e}");
2682 return;
2683 }
2684 }
2685
2686 let ar = Command::new("ar")
2687 .arg("rcs")
2688 .arg(&archive)
2689 .arg(&member_obj)
2690 .output()
2691 .unwrap();
2692 if !ar.status.success() {
2693 eprintln!(
2694 "skipping: ar failed: {}",
2695 String::from_utf8_lossy(&ar.stderr)
2696 );
2697 return;
2698 }
2699
2700 let opts = LinkOptions {
2701 inputs: vec![main_obj.clone(), archive.clone()],
2702 output: Some(scratch("member.out")),
2703 kind: OutputKind::Executable,
2704 ..LinkOptions::default()
2705 };
2706 let err = Linker::run(&opts).unwrap_err();
2707 match err {
2708 LinkError::UndefinedSymbols(msg) => {
2709 assert!(
2710 msg.contains("undefined symbol: _missing_from_member"),
2711 "{msg}"
2712 );
2713 assert!(
2714 msg.contains(&format!("referenced by {}(", archive.display())),
2715 "expected archive-member referrer in:\n{msg}"
2716 );
2717 }
2718 other => panic!("expected UndefinedSymbols, got {other:?}"),
2719 }
2720
2721 let _ = fs::remove_file(main_obj);
2722 let _ = fs::remove_file(member_obj);
2723 let _ = fs::remove_file(archive);
2724 }
2725
2726 #[test]
2727 fn linker_run_all_load_pulls_entry_from_archive() {
2728 if !have_xcrun() || !have_tool("codesign") {
2729 eprintln!("skipping: xcrun as or codesign unavailable");
2730 return;
2731 }
2732 let Some(sdk) = sdk_path() else {
2733 eprintln!("skipping: no macOS SDK path");
2734 return;
2735 };
2736 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
2737 if !tbd.exists() {
2738 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
2739 return;
2740 }
2741
2742 let member_obj = scratch("all-load-main.o");
2743 let archive = scratch("all-load-main.a");
2744 let out = scratch("all-load-main.out");
2745 let src = r#"
2746 .section __TEXT,__text,regular,pure_instructions
2747 .globl _main
2748 _main:
2749 mov w0, #7
2750 ret
2751 .subsections_via_symbols
2752 "#;
2753 if let Err(e) = assemble(src, &member_obj) {
2754 eprintln!("skipping: assemble failed: {e}");
2755 return;
2756 }
2757 let ar = Command::new("ar")
2758 .arg("rcs")
2759 .arg(&archive)
2760 .arg(&member_obj)
2761 .output()
2762 .unwrap();
2763 if !ar.status.success() {
2764 eprintln!(
2765 "skipping: ar failed: {}",
2766 String::from_utf8_lossy(&ar.stderr)
2767 );
2768 return;
2769 }
2770
2771 let opts = LinkOptions {
2772 inputs: vec![archive.clone(), tbd],
2773 output: Some(out.clone()),
2774 kind: OutputKind::Executable,
2775 all_load: true,
2776 ..LinkOptions::default()
2777 };
2778 Linker::run(&opts).unwrap();
2779
2780 let verify = Command::new("codesign")
2781 .arg("-v")
2782 .arg(&out)
2783 .output()
2784 .unwrap();
2785 assert!(
2786 verify.status.success(),
2787 "codesign verify failed: {}",
2788 String::from_utf8_lossy(&verify.stderr)
2789 );
2790 let status = Command::new(&out).status().unwrap();
2791 assert_eq!(
2792 status.code(),
2793 Some(7),
2794 "expected all-load executable to exit 7"
2795 );
2796
2797 let _ = fs::remove_file(member_obj);
2798 let _ = fs::remove_file(archive);
2799 let _ = fs::remove_file(out);
2800 }
2801
2802 #[test]
2803 fn linker_run_force_load_pulls_entry_from_archive() {
2804 if !have_xcrun() || !have_tool("codesign") {
2805 eprintln!("skipping: xcrun as or codesign unavailable");
2806 return;
2807 }
2808 let Some(sdk) = sdk_path() else {
2809 eprintln!("skipping: no macOS SDK path");
2810 return;
2811 };
2812 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
2813 if !tbd.exists() {
2814 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
2815 return;
2816 }
2817
2818 let member_obj = scratch("force-load-main.o");
2819 let archive = scratch("force-load-main.a");
2820 let out = scratch("force-load-main.out");
2821 let src = r#"
2822 .section __TEXT,__text,regular,pure_instructions
2823 .globl _main
2824 _main:
2825 mov w0, #9
2826 ret
2827 .subsections_via_symbols
2828 "#;
2829 if let Err(e) = assemble(src, &member_obj) {
2830 eprintln!("skipping: assemble failed: {e}");
2831 return;
2832 }
2833 let ar = Command::new("ar")
2834 .arg("rcs")
2835 .arg(&archive)
2836 .arg(&member_obj)
2837 .output()
2838 .unwrap();
2839 if !ar.status.success() {
2840 eprintln!(
2841 "skipping: ar failed: {}",
2842 String::from_utf8_lossy(&ar.stderr)
2843 );
2844 return;
2845 }
2846
2847 let opts = LinkOptions {
2848 inputs: vec![archive.clone(), tbd],
2849 output: Some(out.clone()),
2850 kind: OutputKind::Executable,
2851 force_load_archives: vec![archive.clone()],
2852 ..LinkOptions::default()
2853 };
2854 Linker::run(&opts).unwrap();
2855
2856 let verify = Command::new("codesign")
2857 .arg("-v")
2858 .arg(&out)
2859 .output()
2860 .unwrap();
2861 assert!(
2862 verify.status.success(),
2863 "codesign verify failed: {}",
2864 String::from_utf8_lossy(&verify.stderr)
2865 );
2866 let status = Command::new(&out).status().unwrap();
2867 assert_eq!(
2868 status.code(),
2869 Some(9),
2870 "expected force-load executable to exit 9"
2871 );
2872
2873 let _ = fs::remove_file(member_obj);
2874 let _ = fs::remove_file(archive);
2875 let _ = fs::remove_file(out);
2876 }
2877
2878 #[test]
2879 fn linker_run_resolves_lsystem_via_syslibroot() {
2880 if !have_xcrun() || !have_tool("codesign") {
2881 eprintln!("skipping: xcrun as or codesign unavailable");
2882 return;
2883 }
2884 let Some(sdk) = sdk_path() else {
2885 eprintln!("skipping: no macOS SDK path");
2886 return;
2887 };
2888
2889 let obj = scratch("lsystem-main.o");
2890 let out = scratch("lsystem-main.out");
2891 let src = r#"
2892 .section __TEXT,__text,regular,pure_instructions
2893 .globl _main
2894 _main:
2895 mov w0, #0
2896 ret
2897 .subsections_via_symbols
2898 "#;
2899 if let Err(e) = assemble(src, &obj) {
2900 eprintln!("skipping: assemble failed: {e}");
2901 return;
2902 }
2903
2904 let opts = LinkOptions {
2905 inputs: vec![obj.clone()],
2906 library_names: vec!["System".into()],
2907 syslibroot: Some(PathBuf::from(&sdk)),
2908 output: Some(out.clone()),
2909 kind: OutputKind::Executable,
2910 ..LinkOptions::default()
2911 };
2912 Linker::run(&opts).unwrap();
2913
2914 let bytes = fs::read(&out).unwrap();
2915 let dylibs = load_dylib_names(&bytes).unwrap();
2916 assert!(
2917 dylibs
2918 .iter()
2919 .any(|name| name == "/usr/lib/libSystem.B.dylib"),
2920 "expected libSystem load command, got {dylibs:?}"
2921 );
2922 let verify = Command::new("codesign")
2923 .arg("-v")
2924 .arg(&out)
2925 .output()
2926 .unwrap();
2927 assert!(
2928 verify.status.success(),
2929 "codesign verify failed: {}",
2930 String::from_utf8_lossy(&verify.stderr)
2931 );
2932 let status = Command::new(&out).status().unwrap();
2933 assert_eq!(
2934 status.code(),
2935 Some(0),
2936 "expected executable linked via -lSystem to exit 0"
2937 );
2938
2939 let _ = fs::remove_file(obj);
2940 let _ = fs::remove_file(out);
2941 }
2942
2943 #[test]
2944 fn linker_run_resolves_framework_via_syslibroot() {
2945 if !have_xcrun() {
2946 eprintln!("skipping: xcrun unavailable");
2947 return;
2948 }
2949 let Some(sdk) = sdk_path() else {
2950 eprintln!("skipping: xcrun --show-sdk-path unavailable");
2951 return;
2952 };
2953 let metal = PathBuf::from(format!(
2954 "{sdk}/System/Library/Frameworks/Metal.framework/Metal.tbd"
2955 ));
2956 if !metal.exists() {
2957 eprintln!("skipping: no Metal.tbd at {}", metal.display());
2958 return;
2959 }
2960
2961 let obj = scratch("framework-main.o");
2962 let out = scratch("framework-main.out");
2963 let src = r#"
2964 .section __TEXT,__text,regular,pure_instructions
2965 .globl _main
2966 _main:
2967 mov w0, #0
2968 ret
2969 .subsections_via_symbols
2970 "#;
2971 if let Err(e) = assemble(src, &obj) {
2972 eprintln!("skipping: assemble failed: {e}");
2973 return;
2974 }
2975
2976 let opts = LinkOptions {
2977 inputs: vec![obj.clone()],
2978 frameworks: vec![FrameworkSpec {
2979 name: "Metal".into(),
2980 weak: false,
2981 }],
2982 syslibroot: Some(PathBuf::from(&sdk)),
2983 output: Some(out.clone()),
2984 kind: OutputKind::Executable,
2985 ..LinkOptions::default()
2986 };
2987 Linker::run(&opts).unwrap();
2988
2989 let bytes = fs::read(&out).unwrap();
2990 let header = parse_header(&bytes).unwrap();
2991 let commands = parse_commands(&header, &bytes).unwrap();
2992 assert!(commands.iter().any(|cmd| matches!(
2993 cmd,
2994 LoadCommand::Dylib(d)
2995 if d.cmd == afs_ld::macho::constants::LC_LOAD_DYLIB
2996 && d.name.contains("Metal.framework")
2997 )));
2998
2999 let _ = fs::remove_file(obj);
3000 let _ = fs::remove_file(out);
3001 }
3002
3003 #[test]
3004 fn linker_run_resolves_weak_framework_via_syslibroot() {
3005 if !have_xcrun() {
3006 eprintln!("skipping: xcrun unavailable");
3007 return;
3008 }
3009 let Some(sdk) = sdk_path() else {
3010 eprintln!("skipping: xcrun --show-sdk-path unavailable");
3011 return;
3012 };
3013 let metal = PathBuf::from(format!(
3014 "{sdk}/System/Library/Frameworks/Metal.framework/Metal.tbd"
3015 ));
3016 if !metal.exists() {
3017 eprintln!("skipping: no Metal.tbd at {}", metal.display());
3018 return;
3019 }
3020
3021 let obj = scratch("weak-framework-main.o");
3022 let out = scratch("weak-framework-main.out");
3023 let src = r#"
3024 .section __TEXT,__text,regular,pure_instructions
3025 .globl _main
3026 _main:
3027 mov w0, #0
3028 ret
3029 .subsections_via_symbols
3030 "#;
3031 if let Err(e) = assemble(src, &obj) {
3032 eprintln!("skipping: assemble failed: {e}");
3033 return;
3034 }
3035
3036 let opts = LinkOptions {
3037 inputs: vec![obj.clone()],
3038 frameworks: vec![FrameworkSpec {
3039 name: "Metal".into(),
3040 weak: true,
3041 }],
3042 syslibroot: Some(PathBuf::from(&sdk)),
3043 output: Some(out.clone()),
3044 kind: OutputKind::Executable,
3045 ..LinkOptions::default()
3046 };
3047 Linker::run(&opts).unwrap();
3048
3049 let bytes = fs::read(&out).unwrap();
3050 let header = parse_header(&bytes).unwrap();
3051 let commands = parse_commands(&header, &bytes).unwrap();
3052 assert!(commands.iter().any(|cmd| matches!(
3053 cmd,
3054 LoadCommand::Dylib(d)
3055 if d.cmd == afs_ld::macho::constants::LC_LOAD_WEAK_DYLIB
3056 && d.name.contains("Metal.framework")
3057 )));
3058
3059 let _ = fs::remove_file(obj);
3060 let _ = fs::remove_file(out);
3061 }
3062
3063 #[test]
3064 fn linker_run_uses_platform_version_for_build_command() {
3065 if !have_xcrun() {
3066 eprintln!("skipping: xcrun as unavailable");
3067 return;
3068 }
3069
3070 let obj = scratch("platform-version.o");
3071 let out = scratch("platform-version.out");
3072 let src = r#"
3073 .section __TEXT,__text,regular,pure_instructions
3074 .globl _main
3075 _main:
3076 mov w0, #0
3077 ret
3078 .subsections_via_symbols
3079 "#;
3080 if let Err(e) = assemble(src, &obj) {
3081 eprintln!("skipping: assemble failed: {e}");
3082 return;
3083 }
3084
3085 let opts = LinkOptions {
3086 inputs: vec![obj.clone()],
3087 output: Some(out.clone()),
3088 kind: OutputKind::Executable,
3089 platform_version: Some(afs_ld::PlatformVersion {
3090 minos: (13 << 16) | (2 << 8) | 1,
3091 sdk: (14 << 16) | (5 << 8),
3092 }),
3093 ..LinkOptions::default()
3094 };
3095 Linker::run(&opts).unwrap();
3096
3097 let bytes = fs::read(&out).unwrap();
3098 let header = parse_header(&bytes).unwrap();
3099 let commands = parse_commands(&header, &bytes).unwrap();
3100 let build = commands
3101 .into_iter()
3102 .find_map(|cmd| match cmd {
3103 LoadCommand::BuildVersion(cmd) => Some(cmd),
3104 _ => None,
3105 })
3106 .expect("missing LC_BUILD_VERSION");
3107 assert_eq!(build.minos, (13 << 16) | (2 << 8) | 1);
3108 assert_eq!(build.sdk, (14 << 16) | (5 << 8));
3109
3110 let _ = fs::remove_file(obj);
3111 let _ = fs::remove_file(out);
3112 }
3113
3114 #[test]
3115 fn linker_run_emits_rpath_command() {
3116 if !have_xcrun() {
3117 eprintln!("skipping: xcrun as unavailable");
3118 return;
3119 }
3120
3121 let obj = scratch("rpath-main.o");
3122 let out = scratch("rpath-main.out");
3123 let src = r#"
3124 .section __TEXT,__text,regular,pure_instructions
3125 .globl _main
3126 _main:
3127 mov w0, #0
3128 ret
3129 .subsections_via_symbols
3130 "#;
3131 if let Err(e) = assemble(src, &obj) {
3132 eprintln!("skipping: assemble failed: {e}");
3133 return;
3134 }
3135
3136 let opts = LinkOptions {
3137 inputs: vec![obj.clone()],
3138 output: Some(out.clone()),
3139 kind: OutputKind::Executable,
3140 rpaths: vec!["@loader_path/../Frameworks".into()],
3141 ..LinkOptions::default()
3142 };
3143 Linker::run(&opts).unwrap();
3144
3145 let bytes = fs::read(&out).unwrap();
3146 let header = parse_header(&bytes).unwrap();
3147 let commands = parse_commands(&header, &bytes).unwrap();
3148 let rpaths: Vec<String> = commands
3149 .into_iter()
3150 .filter_map(|cmd| match cmd {
3151 LoadCommand::Rpath(cmd) => Some(cmd.path),
3152 _ => None,
3153 })
3154 .collect();
3155 assert_eq!(rpaths, vec!["@loader_path/../Frameworks".to_string()]);
3156
3157 let _ = fs::remove_file(obj);
3158 let _ = fs::remove_file(out);
3159 }
3160
3161 #[test]
3162 fn linker_run_emits_map_file() {
3163 if !have_xcrun() {
3164 eprintln!("skipping: xcrun as unavailable");
3165 return;
3166 }
3167
3168 let obj = scratch("map-main.o");
3169 let out = scratch("map-main.out");
3170 let map = scratch("map-main.map");
3171 let src = r#"
3172 .section __TEXT,__text,regular,pure_instructions
3173 .globl _main
3174 _main:
3175 mov w0, #0
3176 ret
3177 .subsections_via_symbols
3178 "#;
3179 if let Err(e) = assemble(src, &obj) {
3180 eprintln!("skipping: assemble failed: {e}");
3181 return;
3182 }
3183
3184 let opts = LinkOptions {
3185 inputs: vec![obj.clone()],
3186 output: Some(out.clone()),
3187 map: Some(map.clone()),
3188 kind: OutputKind::Executable,
3189 ..LinkOptions::default()
3190 };
3191 Linker::run(&opts).unwrap();
3192
3193 let map_text = fs::read_to_string(&map).unwrap();
3194 assert!(map_text.contains("# Path:"));
3195 assert!(map_text.contains("# Object files:"));
3196 assert!(map_text.contains("linker synthesized"));
3197 assert!(map_text.contains(&obj.display().to_string()));
3198 assert!(map_text.contains("# Sections:"));
3199 assert!(map_text.contains("__TEXT"));
3200 assert!(map_text.contains("__text"));
3201 assert!(map_text.contains("# Symbols:"));
3202 assert!(map_text.contains("_main"));
3203 assert!(map_text.contains("# Dead stripped:"));
3204
3205 let _ = fs::remove_file(obj);
3206 let _ = fs::remove_file(out);
3207 let _ = fs::remove_file(map);
3208 }
3209
3210 #[test]
3211 fn linker_run_map_lists_dead_stripped_symbols() {
3212 if !have_xcrun() {
3213 eprintln!("skipping: xcrun as unavailable");
3214 return;
3215 }
3216
3217 let main_obj = scratch("map-dead-main.o");
3218 let helper_obj = scratch("map-dead-helper.o");
3219 let unused_obj = scratch("map-dead-unused.o");
3220 let out = scratch("map-dead.out");
3221 let map = scratch("map-dead.map");
3222 let main_src = r#"
3223 .section __TEXT,__text,regular,pure_instructions
3224 .globl _main
3225 _main:
3226 bl _helper
3227 mov w0, #0
3228 ret
3229 .subsections_via_symbols
3230 "#;
3231 let helper_src = r#"
3232 .section __TEXT,__text,regular,pure_instructions
3233 .globl _helper
3234 _helper:
3235 ret
3236 .subsections_via_symbols
3237 "#;
3238 let unused_src = r#"
3239 .section __TEXT,__text,regular,pure_instructions
3240 .globl _unused
3241 _unused:
3242 ret
3243 .subsections_via_symbols
3244 "#;
3245 if let Err(e) = assemble(main_src, &main_obj) {
3246 eprintln!("skipping: assemble failed: {e}");
3247 return;
3248 }
3249 if let Err(e) = assemble(helper_src, &helper_obj) {
3250 eprintln!("skipping: assemble failed: {e}");
3251 let _ = fs::remove_file(main_obj);
3252 return;
3253 }
3254 if let Err(e) = assemble(unused_src, &unused_obj) {
3255 eprintln!("skipping: assemble failed: {e}");
3256 let _ = fs::remove_file(main_obj);
3257 let _ = fs::remove_file(helper_obj);
3258 return;
3259 }
3260
3261 let opts = LinkOptions {
3262 inputs: vec![main_obj.clone(), helper_obj.clone(), unused_obj.clone()],
3263 output: Some(out.clone()),
3264 map: Some(map.clone()),
3265 dead_strip: true,
3266 kind: OutputKind::Executable,
3267 ..LinkOptions::default()
3268 };
3269 Linker::run(&opts).unwrap();
3270
3271 let map_text = fs::read_to_string(&map).unwrap();
3272 let dead_stripped_idx = map_text.find("# Dead stripped:").unwrap();
3273 let dead_stripped = &map_text[dead_stripped_idx..];
3274 assert!(dead_stripped.contains("_unused"));
3275 assert!(!dead_stripped.contains("_helper"));
3276
3277 let _ = fs::remove_file(main_obj);
3278 let _ = fs::remove_file(helper_obj);
3279 let _ = fs::remove_file(unused_obj);
3280 let _ = fs::remove_file(out);
3281 let _ = fs::remove_file(map);
3282 }
3283
3284 #[test]
3285 fn linker_run_map_lists_folded_symbols_under_icf_safe() {
3286 if !have_xcrun() {
3287 eprintln!("skipping: xcrun as unavailable");
3288 return;
3289 }
3290
3291 let obj = scratch("map-icf-folded.o");
3292 let out = scratch("map-icf-folded.out");
3293 let map = scratch("map-icf-folded.map");
3294 let src = r#"
3295 .section __TEXT,__text,regular,pure_instructions
3296 .globl _main
3297 _main:
3298 bl _helper1
3299 bl _helper2
3300 mov w0, #0
3301 ret
3302
3303 .private_extern _helper1
3304 _helper1:
3305 mov w0, #7
3306 ret
3307
3308 .private_extern _helper2
3309 _helper2:
3310 mov w0, #7
3311 ret
3312 .subsections_via_symbols
3313 "#;
3314 if let Err(e) = assemble(src, &obj) {
3315 eprintln!("skipping: assemble failed: {e}");
3316 return;
3317 }
3318
3319 let opts = LinkOptions {
3320 inputs: vec![obj.clone()],
3321 output: Some(out.clone()),
3322 map: Some(map.clone()),
3323 icf_mode: afs_ld::IcfMode::Safe,
3324 kind: OutputKind::Executable,
3325 ..LinkOptions::default()
3326 };
3327 Linker::run(&opts).unwrap();
3328
3329 let map_text = fs::read_to_string(&map).unwrap();
3330 let folded_idx = map_text.find("# Folded symbols:").unwrap();
3331 let folded = &map_text[folded_idx..];
3332 assert!(folded.contains("_helper2 folded to _helper1"));
3333
3334 let _ = fs::remove_file(obj);
3335 let _ = fs::remove_file(out);
3336 let _ = fs::remove_file(map);
3337 }
3338
3339 #[test]
3340 fn linker_run_carries_tbd_inputs_into_load_commands() {
3341 if !have_xcrun() {
3342 eprintln!("skipping: xcrun unavailable");
3343 return;
3344 }
3345 let Some(sdk) = sdk_path() else {
3346 eprintln!("skipping: xcrun --show-sdk-path unavailable");
3347 return;
3348 };
3349 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
3350 if !tbd.exists() {
3351 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
3352 return;
3353 }
3354
3355 let obj = scratch("tbd-main.o");
3356 let out = scratch("tbd-a.out");
3357 let src = r#"
3358 .section __TEXT,__text,regular,pure_instructions
3359 .globl _main
3360 _main:
3361 mov x0, #0
3362 ret
3363 .subsections_via_symbols
3364 "#;
3365 if let Err(e) = assemble(src, &obj) {
3366 eprintln!("skipping: assemble failed: {e}");
3367 return;
3368 }
3369
3370 let opts = LinkOptions {
3371 inputs: vec![obj.clone(), tbd.clone()],
3372 output: Some(out.clone()),
3373 kind: OutputKind::Executable,
3374 ..LinkOptions::default()
3375 };
3376 Linker::run(&opts).unwrap();
3377
3378 let bytes = fs::read(&out).unwrap();
3379 let header = parse_header(&bytes).unwrap();
3380 let commands = parse_commands(&header, &bytes).unwrap();
3381 assert!(
3382 commands.iter().any(|cmd| matches!(
3383 cmd,
3384 LoadCommand::Dylib(d) if d.cmd == afs_ld::macho::constants::LC_LOAD_DYLIB
3385 )),
3386 "expected at least one LC_LOAD_DYLIB in output"
3387 );
3388
3389 let _ = fs::remove_file(obj);
3390 let _ = fs::remove_file(out);
3391 }
3392
3393 #[test]
3394 fn linker_run_handles_non_standard_segment_without_panicking() {
3395 if !have_xcrun() {
3396 eprintln!("skipping: xcrun as unavailable");
3397 return;
3398 }
3399
3400 let obj = scratch("custom-segment.o");
3401 let out = scratch("custom-segment.out");
3402 let src = r#"
3403 .section __FOO,__bar
3404 .globl _custom
3405 _custom:
3406 .quad 1
3407 .subsections_via_symbols
3408 "#;
3409 if let Err(e) = assemble(src, &obj) {
3410 eprintln!("skipping: assemble failed: {e}");
3411 return;
3412 }
3413
3414 let opts = LinkOptions {
3415 inputs: vec![obj.clone()],
3416 output: Some(out.clone()),
3417 kind: OutputKind::Executable,
3418 ..LinkOptions::default()
3419 };
3420 Linker::run(&opts).unwrap();
3421
3422 let bytes = fs::read(&out).unwrap();
3423 let header = parse_header(&bytes).unwrap();
3424 let commands = parse_commands(&header, &bytes).unwrap();
3425 assert!(commands.iter().any(|cmd| match cmd {
3426 LoadCommand::Segment64(seg) => seg.segname_str() == "__FOO",
3427 _ => false,
3428 }));
3429
3430 let _ = fs::remove_file(obj);
3431 let _ = fs::remove_file(out);
3432 }
3433
3434 #[test]
3435 fn linker_run_uses_requested_entry_symbol() {
3436 if !have_xcrun() {
3437 eprintln!("skipping: xcrun as unavailable");
3438 return;
3439 }
3440
3441 let obj = scratch("entry.o");
3442 let out = scratch("entry.out");
3443 let src = r#"
3444 .section __TEXT,__text,regular,pure_instructions
3445 .globl _main
3446 _main:
3447 ret
3448 .globl _alt
3449 _alt:
3450 mov x0, #1
3451 ret
3452 .subsections_via_symbols
3453 "#;
3454 if let Err(e) = assemble(src, &obj) {
3455 eprintln!("skipping: assemble failed: {e}");
3456 return;
3457 }
3458
3459 let opts = LinkOptions {
3460 inputs: vec![obj.clone()],
3461 output: Some(out.clone()),
3462 entry: Some("_alt".into()),
3463 kind: OutputKind::Executable,
3464 ..LinkOptions::default()
3465 };
3466 Linker::run(&opts).unwrap();
3467
3468 let bytes = fs::read(&out).unwrap();
3469 let header = parse_header(&bytes).unwrap();
3470 let commands = parse_commands(&header, &bytes).unwrap();
3471 let mut text_offset = None;
3472 let mut main_entryoff = None;
3473 for cmd in commands {
3474 match cmd {
3475 LoadCommand::Segment64(seg) => {
3476 for section in seg.sections {
3477 if section.sectname_str() == "__text" {
3478 text_offset = Some(section.offset as u64);
3479 }
3480 }
3481 }
3482 LoadCommand::Raw { cmd, data, .. } if cmd == afs_ld::macho::constants::LC_MAIN => {
3483 let mut buf = [0u8; 8];
3484 buf.copy_from_slice(&data[0..8]);
3485 main_entryoff = Some(u64::from_le_bytes(buf));
3486 }
3487 _ => {}
3488 }
3489 }
3490
3491 let text_offset = text_offset.expect("text section offset");
3492 let main_entryoff = main_entryoff.expect("LC_MAIN entryoff");
3493 assert!(
3494 main_entryoff > text_offset,
3495 "expected custom entry to land after start of __text: text={text_offset}, entry={main_entryoff}"
3496 );
3497
3498 let _ = fs::remove_file(obj);
3499 let _ = fs::remove_file(out);
3500 }
3501
3502 #[test]
3503 fn linker_run_defaults_entry_to_main_symbol() {
3504 if !have_xcrun() {
3505 eprintln!("skipping: xcrun as unavailable");
3506 return;
3507 }
3508
3509 let obj = scratch("default-entry.o");
3510 let out = scratch("default-entry.out");
3511 let src = r#"
3512 .section __TEXT,__text,regular,pure_instructions
3513 .globl _helper
3514 _helper:
3515 mov w0, #7
3516 ret
3517 .globl _main
3518 _main:
3519 mov w0, #0
3520 ret
3521 .subsections_via_symbols
3522 "#;
3523 if let Err(e) = assemble(src, &obj) {
3524 eprintln!("skipping: assemble failed: {e}");
3525 return;
3526 }
3527
3528 let opts = LinkOptions {
3529 inputs: vec![obj.clone()],
3530 output: Some(out.clone()),
3531 kind: OutputKind::Executable,
3532 ..LinkOptions::default()
3533 };
3534 Linker::run(&opts).unwrap();
3535
3536 let status = Command::new(&out).status().unwrap();
3537 assert_eq!(
3538 status.code(),
3539 Some(0),
3540 "default executable entry should prefer _main over the first text atom"
3541 );
3542
3543 let _ = fs::remove_file(obj);
3544 let _ = fs::remove_file(out);
3545 }
3546
3547 #[test]
3548 fn linker_run_applies_core_arm64_relocations() {
3549 if !have_xcrun() {
3550 eprintln!("skipping: xcrun as unavailable");
3551 return;
3552 }
3553
3554 let obj = scratch("relocs.o");
3555 let out = scratch("relocs.out");
3556 let src = r#"
3557 .section __TEXT,__text,regular,pure_instructions
3558 .globl _main
3559 .globl _helper
3560 _main:
3561 adrp x0, _target@PAGE
3562 add x0, x0, _target@PAGEOFF
3563 bl _helper
3564 ret
3565 _helper:
3566 ret
3567
3568 .section __DATA,__data
3569 .p2align 3
3570 _target:
3571 .quad _helper
3572
3573 .section __TEXT,__const
3574 .p2align 3
3575 _delta:
3576 .quad _helper - _main
3577 .subsections_via_symbols
3578 "#;
3579 if let Err(e) = assemble(src, &obj) {
3580 eprintln!("skipping: assemble failed: {e}");
3581 return;
3582 }
3583
3584 let opts = LinkOptions {
3585 inputs: vec![obj.clone()],
3586 output: Some(out.clone()),
3587 kind: OutputKind::Executable,
3588 ..LinkOptions::default()
3589 };
3590 Linker::run(&opts).unwrap();
3591
3592 let bytes = fs::read(&out).unwrap();
3593 let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").expect("text section");
3594 let (data_addr, data) = output_section(&bytes, "__DATA", "__data").expect("data section");
3595 let (_, cdata) = output_section(&bytes, "__TEXT", "__const").expect("const section");
3596
3597 let adrp = u32::from_le_bytes(text[0..4].try_into().unwrap());
3598 let add = u32::from_le_bytes(text[4..8].try_into().unwrap());
3599 let branch = u32::from_le_bytes(text[8..12].try_into().unwrap());
3600 let data_ptr = u64::from_le_bytes(data[0..8].try_into().unwrap());
3601 let delta = u64::from_le_bytes(cdata[0..8].try_into().unwrap());
3602
3603 let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
3604 let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
3605 let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
3606 let adrp_base = ((text_addr as i64) & !0xfff) + (adrp_pages << 12);
3607 let add_imm = ((add >> 10) & 0xfff) as u64;
3608 let reconstructed_target = (adrp_base as u64) + add_imm;
3609
3610 assert_eq!(
3611 reconstructed_target, data_addr,
3612 "ADRP+ADD should resolve _target"
3613 );
3614 assert_eq!(
3615 branch & 0x03ff_ffff,
3616 0x2,
3617 "BL should branch forward 8 bytes"
3618 );
3619 assert_eq!(
3620 data_ptr,
3621 text_addr + 16,
3622 ".quad _helper should point at helper"
3623 );
3624 assert_eq!(delta, 16, "_helper - _main should fold through SUBTRACTOR");
3625
3626 let _ = fs::remove_file(obj);
3627 let _ = fs::remove_file(out);
3628 }
3629
3630 fn sign_extend_21(value: i64) -> i64 {
3631 if value & (1 << 20) != 0 {
3632 value | !0x1f_ffff
3633 } else {
3634 value
3635 }
3636 }
3637
3638 #[test]
3639 fn linker_run_applies_scaled_pageoff12_for_ldr_x() {
3640 if !have_xcrun() {
3641 eprintln!("skipping: xcrun as unavailable");
3642 return;
3643 }
3644
3645 let obj = scratch("scaled-ldr.o");
3646 let out = scratch("scaled-ldr.out");
3647 let src = r#"
3648 .section __TEXT,__text,regular,pure_instructions
3649 .globl _main
3650 _main:
3651 adrp x0, _target@PAGE
3652 ldr x1, [x0, _target@PAGEOFF]
3653 ret
3654
3655 .section __DATA,__data
3656 .space 0x3f8
3657 .p2align 3
3658 .globl _target
3659 _target:
3660 .quad 0x1122334455667788
3661 .subsections_via_symbols
3662 "#;
3663 if let Err(e) = assemble(src, &obj) {
3664 eprintln!("skipping: assemble failed: {e}");
3665 return;
3666 }
3667
3668 let opts = LinkOptions {
3669 inputs: vec![obj.clone()],
3670 output: Some(out.clone()),
3671 kind: OutputKind::Executable,
3672 ..LinkOptions::default()
3673 };
3674 Linker::run(&opts).unwrap();
3675
3676 let bytes = fs::read(&out).unwrap();
3677 let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").expect("text section");
3678 let (data_addr, data) = output_section(&bytes, "__DATA", "__data").expect("data section");
3679
3680 let adrp = u32::from_le_bytes(text[0..4].try_into().unwrap());
3681 let ldr = u32::from_le_bytes(text[4..8].try_into().unwrap());
3682 let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
3683 let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
3684 let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
3685 let adrp_base = ((text_addr as i64) & !0xfff) + (adrp_pages << 12);
3686 let ldr_shift = ((ldr >> 30) & 0b11) as u64;
3687 let ldr_imm = ((ldr >> 10) & 0xfff) as u64;
3688 let reconstructed_target = (adrp_base as u64) + (ldr_imm << ldr_shift);
3689
3690 assert_eq!(ldr_shift, 3, "expected 64-bit LDR scale");
3691 assert_eq!(ldr_imm, 0x7f, "scaled imm12 should store 0x3f8 >> 3");
3692 assert_eq!(reconstructed_target, data_addr + 0x3f8);
3693 assert_eq!(
3694 u64::from_le_bytes(data[0x3f8..0x400].try_into().unwrap()),
3695 0x1122334455667788
3696 );
3697
3698 let _ = fs::remove_file(obj);
3699 let _ = fs::remove_file(out);
3700 }
3701
3702 #[test]
3703 fn relocated_sections_match_apple_ld_across_fixture_matrix() {
3704 if !have_xcrun() || !have_xcrun_tool("ld") {
3705 eprintln!("skipping: xcrun as/ld unavailable");
3706 return;
3707 }
3708 let Some(sdk) = sdk_path() else {
3709 eprintln!("skipping: xcrun --show-sdk-path unavailable");
3710 return;
3711 };
3712 let Some(sdk_ver) = sdk_version() else {
3713 eprintln!("skipping: xcrun --show-sdk-version unavailable");
3714 return;
3715 };
3716 const TEXT: SectionCase = SectionCase {
3717 segname: "__TEXT",
3718 sectname: "__text",
3719 };
3720 const CONST: SectionCase = SectionCase {
3721 segname: "__TEXT",
3722 sectname: "__const",
3723 };
3724
3725 let cases = [
3726 ParityCase {
3727 name: "branch-forward",
3728 src: r#"
3729 .section __TEXT,__text,regular,pure_instructions
3730 .globl _main
3731 _main:
3732 bl _helper
3733 ret
3734 _helper:
3735 ret
3736 .subsections_via_symbols
3737 "#,
3738 check: ParityCheck::ExactSections(&[TEXT]),
3739 },
3740 ParityCase {
3741 name: "branch-backward",
3742 src: r#"
3743 .section __TEXT,__text,regular,pure_instructions
3744 .globl _helper
3745 _helper:
3746 ret
3747 .globl _main
3748 _main:
3749 bl _helper
3750 ret
3751 .subsections_via_symbols
3752 "#,
3753 check: ParityCheck::ExactSections(&[TEXT]),
3754 },
3755 ParityCase {
3756 name: "adrp-add-intra-text-forward",
3757 src: r#"
3758 .section __TEXT,__text,regular,pure_instructions
3759 .globl _main
3760 _main:
3761 adrp x0, _target@PAGE
3762 add x0, x0, _target@PAGEOFF
3763 ret
3764 .space 0x4ff4
3765 _target:
3766 .quad 0
3767 .subsections_via_symbols
3768 "#,
3769 check: ParityCheck::PageRef {
3770 section: TEXT,
3771 site_offset: 0,
3772 target_offset: 0x5000,
3773 kind: PageRefKind::Add,
3774 },
3775 },
3776 ParityCase {
3777 name: "adrp-add-intra-text-backward",
3778 src: r#"
3779 .section __TEXT,__text,regular,pure_instructions
3780 _target:
3781 .quad 0x55
3782 .space 0x4ff8
3783 .globl _main
3784 _main:
3785 adrp x0, _target@PAGE
3786 add x0, x0, _target@PAGEOFF
3787 ret
3788 .subsections_via_symbols
3789 "#,
3790 check: ParityCheck::PageRef {
3791 section: TEXT,
3792 site_offset: 0x5000,
3793 target_offset: 0,
3794 kind: PageRefKind::Add,
3795 },
3796 },
3797 ParityCase {
3798 name: "adrp-ldr-x-intra-text",
3799 src: r#"
3800 .section __TEXT,__text,regular,pure_instructions
3801 .globl _main
3802 _main:
3803 adrp x0, _target@PAGE
3804 ldr x1, [x0, _target@PAGEOFF]
3805 ret
3806 .space 0x3f4
3807 _target:
3808 .quad 0x1122334455667788
3809 .subsections_via_symbols
3810 "#,
3811 check: ParityCheck::PageRef {
3812 section: TEXT,
3813 site_offset: 0,
3814 target_offset: 0x400,
3815 kind: PageRefKind::Load,
3816 },
3817 },
3818 ParityCase {
3819 name: "adrp-ldr-w-intra-text",
3820 src: r#"
3821 .section __TEXT,__text,regular,pure_instructions
3822 .globl _main
3823 _main:
3824 adrp x0, _target@PAGE
3825 ldr w1, [x0, _target@PAGEOFF]
3826 ret
3827 .space 0x2f4
3828 _target:
3829 .long 0x11223344
3830 .subsections_via_symbols
3831 "#,
3832 check: ParityCheck::PageRef {
3833 section: TEXT,
3834 site_offset: 0,
3835 target_offset: 0x300,
3836 kind: PageRefKind::Load,
3837 },
3838 },
3839 ParityCase {
3840 name: "adrp-ldrh-intra-text",
3841 src: r#"
3842 .section __TEXT,__text,regular,pure_instructions
3843 .globl _main
3844 _main:
3845 adrp x0, _target@PAGE
3846 ldrh w1, [x0, _target@PAGEOFF]
3847 ret
3848 .space 0x1f4
3849 _target:
3850 .hword 0x3344
3851 .subsections_via_symbols
3852 "#,
3853 check: ParityCheck::PageRef {
3854 section: TEXT,
3855 site_offset: 0,
3856 target_offset: 0x200,
3857 kind: PageRefKind::Load,
3858 },
3859 },
3860 ParityCase {
3861 name: "adrp-ldrb-intra-text",
3862 src: r#"
3863 .section __TEXT,__text,regular,pure_instructions
3864 .globl _main
3865 _main:
3866 adrp x0, _target@PAGE
3867 ldrb w1, [x0, _target@PAGEOFF]
3868 ret
3869 .space 0xf4
3870 _target:
3871 .byte 0x44
3872 .subsections_via_symbols
3873 "#,
3874 check: ParityCheck::PageRef {
3875 section: TEXT,
3876 site_offset: 0,
3877 target_offset: 0x100,
3878 kind: PageRefKind::Load,
3879 },
3880 },
3881 ParityCase {
3882 name: "mixed-branch-adrp-text",
3883 src: r#"
3884 .section __TEXT,__text,regular,pure_instructions
3885 .globl _main
3886 .globl _helper
3887 _main:
3888 adrp x0, _target@PAGE
3889 add x0, x0, _target@PAGEOFF
3890 bl _helper
3891 ret
3892 _helper:
3893 ret
3894 .space 0xff0
3895 _target:
3896 .quad 0x99
3897 .subsections_via_symbols
3898 "#,
3899 check: ParityCheck::PageRef {
3900 section: TEXT,
3901 site_offset: 0,
3902 target_offset: 0x1004,
3903 kind: PageRefKind::Add,
3904 },
3905 },
3906 ParityCase {
3907 name: "subtractor-positive",
3908 src: r#"
3909 .section __TEXT,__text,regular,pure_instructions
3910 .globl _helper
3911 _helper:
3912 ret
3913 .globl _main
3914 _main:
3915 bl _helper
3916 ret
3917 .section __TEXT,__const
3918 .p2align 3
3919 _delta:
3920 .quad _helper - _main
3921 .subsections_via_symbols
3922 "#,
3923 check: ParityCheck::ExactSections(&[CONST]),
3924 },
3925 ParityCase {
3926 name: "subtractor-negative",
3927 src: r#"
3928 .section __TEXT,__text,regular,pure_instructions
3929 .globl _helper
3930 _helper:
3931 ret
3932 .globl _main
3933 _main:
3934 ret
3935 .section __TEXT,__const
3936 .p2align 3
3937 _delta:
3938 .quad _main - _helper
3939 .subsections_via_symbols
3940 "#,
3941 check: ParityCheck::ExactSections(&[CONST]),
3942 },
3943 ParityCase {
3944 name: "branch-and-subtractor",
3945 src: r#"
3946 .section __TEXT,__text,regular,pure_instructions
3947 .globl _helper
3948 _helper:
3949 ret
3950 .globl _main
3951 _main:
3952 bl _helper
3953 ret
3954 .section __TEXT,__const
3955 .p2align 3
3956 _delta:
3957 .quad _main - _helper
3958 .subsections_via_symbols
3959 "#,
3960 check: ParityCheck::ExactSections(&[TEXT, CONST]),
3961 },
3962 ];
3963
3964 let mut failures = Vec::new();
3965 for case in &cases {
3966 if let Err(err) = assert_case_matches_apple_ld(case, &sdk, &sdk_ver) {
3967 failures.push(err);
3968 }
3969 }
3970
3971 assert!(
3972 failures.is_empty(),
3973 "Apple ld parity failures ({} cases):\n{}",
3974 failures.len(),
3975 failures.join("\n\n")
3976 );
3977 }
3978
3979 #[test]
3980 fn linker_run_rejects_out_of_range_branch26() {
3981 if !have_xcrun() {
3982 eprintln!("skipping: xcrun as unavailable");
3983 return;
3984 }
3985
3986 let obj = scratch("branch26-range.o");
3987 let out = scratch("branch26-range.out");
3988 let src = r#"
3989 .section __TEXT,__text,regular,pure_instructions
3990 .globl _main
3991 _main:
3992 bl _helper
3993 ret
3994
3995 .zerofill __DATA,__bss,_gap,0x9000000,0
3996
3997 .section __FAR,__text,regular,pure_instructions
3998 .globl _helper
3999 _helper:
4000 ret
4001 .subsections_via_symbols
4002 "#;
4003 if let Err(e) = assemble(src, &obj) {
4004 eprintln!("skipping: assemble failed: {e}");
4005 return;
4006 }
4007
4008 let opts = LinkOptions {
4009 inputs: vec![obj.clone()],
4010 output: Some(out),
4011 kind: OutputKind::Executable,
4012 ..LinkOptions::default()
4013 };
4014 let err = Linker::run(&opts).unwrap_err();
4015 match err {
4016 LinkError::Reloc(err) => {
4017 let msg = err.to_string();
4018 assert!(msg.contains("Branch26"), "{msg}");
4019 assert!(msg.contains("out of BRANCH26 range"), "{msg}");
4020 assert!(msg.contains("_helper"), "{msg}");
4021 }
4022 other => panic!("expected Reloc error, got {other:?}"),
4023 }
4024
4025 let _ = fs::remove_file(obj);
4026 }
4027
4028 #[test]
4029 fn linker_run_routes_dylib_imports_through_synthetic_sections() {
4030 if !have_xcrun() {
4031 eprintln!("skipping: xcrun unavailable");
4032 return;
4033 }
4034 let Some(sdk) = sdk_path() else {
4035 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4036 return;
4037 };
4038 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4039 if !tbd.exists() {
4040 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4041 return;
4042 }
4043
4044 let obj = scratch("import-reloc.o");
4045 let out = scratch("import-reloc.out");
4046 let src = r#"
4047 .section __TEXT,__text,regular,pure_instructions
4048 .globl _main
4049 _main:
4050 adrp x0, _write@GOTPAGE
4051 ldr x0, [x0, _write@GOTPAGEOFF]
4052 bl _write
4053 ret
4054 .subsections_via_symbols
4055 "#;
4056 if let Err(e) = assemble(src, &obj) {
4057 eprintln!("skipping: assemble failed: {e}");
4058 return;
4059 }
4060
4061 let opts = LinkOptions {
4062 inputs: vec![obj.clone(), tbd.clone()],
4063 output: Some(out.clone()),
4064 kind: OutputKind::Executable,
4065 ..LinkOptions::default()
4066 };
4067 Linker::run(&opts).unwrap();
4068
4069 let bytes = fs::read(&out).unwrap();
4070 let header = parse_header(&bytes).unwrap();
4071 let commands = parse_commands(&header, &bytes).unwrap();
4072 let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
4073 let (stubs_addr, stubs) = output_section(&bytes, "__TEXT", "__stubs").unwrap();
4074 let (helper_addr, helper) = output_section(&bytes, "__TEXT", "__stub_helper").unwrap();
4075 let (got_addr, got) = output_section(&bytes, "__DATA_CONST", "__got").unwrap();
4076 let (lazy_addr, lazy) = output_section(&bytes, "__DATA", "__la_symbol_ptr").unwrap();
4077 let (dyld_private_addr, _) = output_section(&bytes, "__DATA", "__data").unwrap();
4078 let stubs_hdr = output_section_header(&bytes, "__TEXT", "__stubs").unwrap();
4079 let got_hdr = output_section_header(&bytes, "__DATA_CONST", "__got").unwrap();
4080 let lazy_hdr = output_section_header(&bytes, "__DATA", "__la_symbol_ptr").unwrap();
4081
4082 let symtab = commands
4083 .iter()
4084 .find_map(|cmd| match cmd {
4085 LoadCommand::Symtab(cmd) => Some(*cmd),
4086 _ => None,
4087 })
4088 .unwrap();
4089 let dysymtab = commands
4090 .iter()
4091 .find_map(|cmd| match cmd {
4092 LoadCommand::Dysymtab(cmd) => Some(*cmd),
4093 _ => None,
4094 })
4095 .unwrap();
4096 let dyld_info = commands
4097 .iter()
4098 .find_map(|cmd| match cmd {
4099 LoadCommand::DyldInfoOnly(cmd) => Some(*cmd),
4100 _ => None,
4101 })
4102 .unwrap();
4103 let libsystem_load = commands
4104 .iter()
4105 .find_map(|cmd| match cmd {
4106 LoadCommand::Dylib(cmd)
4107 if cmd.cmd == afs_ld::macho::constants::LC_LOAD_DYLIB
4108 && cmd.name == "/usr/lib/libSystem.B.dylib" =>
4109 {
4110 Some(cmd.clone())
4111 }
4112 _ => None,
4113 })
4114 .unwrap();
4115 let symbols = parse_nlist_table(&bytes, symtab.symoff, symtab.nsyms).unwrap();
4116 let strings = StringTable::from_file(&bytes, symtab.stroff, symtab.strsize).unwrap();
4117 let symbol_names: Vec<&str> = symbols
4118 .iter()
4119 .map(|symbol| strings.get(symbol.strx()).unwrap())
4120 .collect();
4121
4122 assert_eq!(got.len(), 16);
4123 assert_eq!(stubs.len(), 12);
4124 assert_eq!(helper.len(), 36);
4125 assert_eq!(lazy.len(), 8);
4126 assert_eq!(symtab.nsyms, 5);
4127 assert_eq!(dysymtab.nlocalsym, 1);
4128 assert_eq!(dysymtab.nextdefsym, 2);
4129 assert_eq!(dysymtab.nundefsym, 2);
4130 assert_eq!(dysymtab.nindirectsyms, 4);
4131 assert_eq!(stubs_hdr.reserved1, 0);
4132 assert_eq!(got_hdr.reserved1, 1);
4133 assert_eq!(lazy_hdr.reserved1, 3);
4134 assert_eq!(stubs_hdr.reserved2, 12);
4135 assert!(libsystem_load.current_version >= (1 << 16));
4136 assert_eq!(libsystem_load.compatibility_version, 1 << 16);
4137 assert!(dyld_info.rebase_size > 0);
4138 assert!(dyld_info.bind_size > 0);
4139 assert!(dyld_info.lazy_bind_size > 0);
4140 assert_eq!(
4141 decode_page_reference(&text, text_addr, 0, &PageRefKind::Load).unwrap(),
4142 got_addr
4143 );
4144 assert_eq!(
4145 decode_branch_target(&text, text_addr, 8).unwrap(),
4146 stubs_addr
4147 );
4148 assert_eq!(
4149 decode_page_reference(&stubs, stubs_addr, 0, &PageRefKind::Load).unwrap(),
4150 lazy_addr
4151 );
4152 assert_eq!(read_insn(&stubs, 8).unwrap(), 0xd61f0200);
4153 assert_eq!(
4154 u64::from_le_bytes(lazy[0..8].try_into().unwrap()),
4155 helper_addr + 24
4156 );
4157 assert_eq!(
4158 decode_page_reference(&helper, helper_addr, 0, &PageRefKind::Add).unwrap(),
4159 dyld_private_addr
4160 );
4161 assert_eq!(
4162 decode_page_reference(&helper, helper_addr, 12, &PageRefKind::Load).unwrap(),
4163 got_addr + 8
4164 );
4165 assert_eq!(read_insn(&helper, 20).unwrap(), 0xd61f0200);
4166 assert_eq!(read_insn(&helper, 24).unwrap(), 0x1800_0050);
4167 assert_eq!(
4168 decode_branch_target(&helper, helper_addr, 28).unwrap(),
4169 helper_addr
4170 );
4171 assert_eq!(u32::from_le_bytes(helper[32..36].try_into().unwrap()), 0);
4172 let (locals, extdefs, undefs) = symbol_partition_names(&bytes);
4173 assert_eq!(locals, vec!["__dyld_private".to_string()]);
4174 assert_eq!(
4175 extdefs,
4176 vec!["__mh_execute_header".to_string(), "_main".to_string()]
4177 );
4178 assert_eq!(
4179 undefs,
4180 vec!["_write".to_string(), "dyld_stub_binder".to_string()]
4181 );
4182 assert!(symbol_names.contains(&"__dyld_private"));
4183 assert!(symbols[dysymtab.iundefsym as usize..]
4184 .iter()
4185 .all(|symbol| symbol.kind() == SymKind::Undef));
4186 assert!(symbols[dysymtab.iundefsym as usize..]
4187 .iter()
4188 .all(|symbol| symbol.library_ordinal().unwrap() > 0));
4189 assert!(symbol_names.contains(&"_write"));
4190 assert!(symbol_names.contains(&"dyld_stub_binder"));
4191
4192 let _ = fs::remove_file(out);
4193 let _ = fs::remove_file(obj);
4194 }
4195
4196 #[test]
4197 fn synthetic_import_surfaces_match_apple_ld_classic_lazy_model() {
4198 if !have_xcrun() || !have_xcrun_tool("ld") {
4199 eprintln!("skipping: xcrun as/ld unavailable");
4200 return;
4201 }
4202 let Some(sdk) = sdk_path() else {
4203 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4204 return;
4205 };
4206 let Some(sdk_ver) = sdk_version() else {
4207 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4208 return;
4209 };
4210 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4211 if !tbd.exists() {
4212 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4213 return;
4214 }
4215
4216 let obj = scratch("import-parity.o");
4217 let our_out = scratch("import-parity-ours.out");
4218 let apple_out = scratch("import-parity-apple.out");
4219 let src = r#"
4220 .section __TEXT,__text,regular,pure_instructions
4221 .globl _main
4222 _main:
4223 adrp x0, _write@GOTPAGE
4224 ldr x0, [x0, _write@GOTPAGEOFF]
4225 bl _write
4226 ret
4227 .subsections_via_symbols
4228 "#;
4229 if let Err(e) = assemble(src, &obj) {
4230 eprintln!("skipping: assemble failed: {e}");
4231 return;
4232 }
4233
4234 let opts = LinkOptions {
4235 inputs: vec![obj.clone(), tbd],
4236 output: Some(our_out.clone()),
4237 kind: OutputKind::Executable,
4238 ..LinkOptions::default()
4239 };
4240 Linker::run(&opts).unwrap();
4241 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4242
4243 let our_bytes = fs::read(&our_out).unwrap();
4244 let apple_bytes = fs::read(&apple_out).unwrap();
4245
4246 for (segname, sectname) in [("__TEXT", "__stubs"), ("__TEXT", "__stub_helper")] {
4247 let (_, ours) = output_section(&our_bytes, segname, sectname).unwrap();
4248 let (_, apple) = output_section(&apple_bytes, segname, sectname).unwrap();
4249 let diff = diff_macho(&ours, &apple);
4250 assert!(
4251 diff.is_clean(),
4252 "{segname},{sectname} diverged from Apple ld: {:#?}",
4253 diff.critical
4254 );
4255 }
4256
4257 let (our_helper_addr, _) = output_section(&our_bytes, "__TEXT", "__stub_helper").unwrap();
4258 let (apple_helper_addr, _) = output_section(&apple_bytes, "__TEXT", "__stub_helper").unwrap();
4259 let (_, our_lazy) = output_section(&our_bytes, "__DATA", "__la_symbol_ptr").unwrap();
4260 let (_, apple_lazy) = output_section(&apple_bytes, "__DATA", "__la_symbol_ptr").unwrap();
4261 assert_eq!(
4262 u64::from_le_bytes(our_lazy[0..8].try_into().unwrap()) - our_helper_addr,
4263 24
4264 );
4265 assert_eq!(
4266 u64::from_le_bytes(apple_lazy[0..8].try_into().unwrap()) - apple_helper_addr,
4267 24
4268 );
4269
4270 assert_eq!(
4271 load_dylib_names(&our_bytes).unwrap(),
4272 load_dylib_names(&apple_bytes).unwrap()
4273 );
4274 assert_eq!(
4275 segment_flags(&our_bytes, "__DATA_CONST"),
4276 Some(SG_READ_ONLY)
4277 );
4278 assert_eq!(
4279 segment_flags(&our_bytes, "__DATA_CONST"),
4280 segment_flags(&apple_bytes, "__DATA_CONST")
4281 );
4282
4283 let our_rebases = decode_rebase_records(&our_bytes).unwrap();
4284 let apple_rebases = decode_rebase_records(&apple_bytes).unwrap();
4285 assert!(our_rebases
4286 .iter()
4287 .all(|record| record.rebase_type == REBASE_TYPE_POINTER));
4288 assert_eq!(our_rebases, apple_rebases);
4289 assert_eq!(
4290 decode_bind_records(&our_bytes, false).unwrap(),
4291 decode_bind_records(&apple_bytes, false).unwrap()
4292 );
4293 assert_eq!(
4294 dyld_info_stream(&our_bytes, DyldInfoStreamKind::WeakBind).unwrap(),
4295 dyld_info_stream(&apple_bytes, DyldInfoStreamKind::WeakBind).unwrap()
4296 );
4297 assert_eq!(
4298 decode_bind_records(&our_bytes, true).unwrap(),
4299 decode_bind_records(&apple_bytes, true).unwrap()
4300 );
4301 assert_eq!(
4302 canonical_lazy_bind_stream(&our_bytes).unwrap(),
4303 canonical_lazy_bind_stream(&apple_bytes).unwrap()
4304 );
4305 assert_eq!(
4306 indirect_symbol_table(&our_bytes),
4307 indirect_symbol_table(&apple_bytes)
4308 );
4309
4310 let _ = fs::remove_file(apple_out);
4311 let _ = fs::remove_file(our_out);
4312 let _ = fs::remove_file(obj);
4313 }
4314
4315 #[test]
4316 fn classic_lazy_surfaces_match_apple_ld_across_fixture_matrix() {
4317 if !have_xcrun() || !have_xcrun_tool("ld") {
4318 eprintln!("skipping: xcrun as/ld unavailable");
4319 return;
4320 }
4321 let Some(sdk) = sdk_path() else {
4322 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4323 return;
4324 };
4325 let Some(sdk_ver) = sdk_version() else {
4326 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4327 return;
4328 };
4329
4330 let cases = [
4331 ClassicLazyParityCase {
4332 name: "single-got-and-call",
4333 src: r#"
4334 .section __TEXT,__text,regular,pure_instructions
4335 .globl _main
4336 _main:
4337 adrp x0, _write@GOTPAGE
4338 ldr x0, [x0, _write@GOTPAGEOFF]
4339 bl _write
4340 ret
4341 .subsections_via_symbols
4342 "#,
4343 },
4344 ClassicLazyParityCase {
4345 name: "batched-got-and-calls",
4346 src: r#"
4347 .section __TEXT,__text,regular,pure_instructions
4348 .globl _main
4349 _main:
4350 adrp x0, _write@GOTPAGE
4351 ldr x0, [x0, _write@GOTPAGEOFF]
4352 bl _write
4353 adrp x1, _close@GOTPAGE
4354 ldr x1, [x1, _close@GOTPAGEOFF]
4355 bl _close
4356 adrp x2, _read@GOTPAGE
4357 ldr x2, [x2, _read@GOTPAGEOFF]
4358 bl _read
4359 ret
4360 .subsections_via_symbols
4361 "#,
4362 },
4363 ClassicLazyParityCase {
4364 name: "branch-only-calls",
4365 src: r#"
4366 .section __TEXT,__text,regular,pure_instructions
4367 .globl _main
4368 _main:
4369 bl _write
4370 bl _close
4371 bl _read
4372 ret
4373 .subsections_via_symbols
4374 "#,
4375 },
4376 ClassicLazyParityCase {
4377 name: "deduped-import",
4378 src: r#"
4379 .section __TEXT,__text,regular,pure_instructions
4380 .globl _main
4381 _main:
4382 adrp x0, _write@GOTPAGE
4383 ldr x0, [x0, _write@GOTPAGEOFF]
4384 bl _write
4385 bl _write
4386 adrp x1, _write@GOTPAGE
4387 ldr x1, [x1, _write@GOTPAGEOFF]
4388 ret
4389 .subsections_via_symbols
4390 "#,
4391 },
4392 ];
4393
4394 let mut failures = Vec::new();
4395 for case in &cases {
4396 if let Err(err) = assert_classic_lazy_case_matches_apple_ld(case, &sdk, &sdk_ver) {
4397 failures.push(err);
4398 }
4399 }
4400
4401 assert!(
4402 failures.is_empty(),
4403 "Apple ld classic-lazy parity failures ({} cases):\n{}",
4404 failures.len(),
4405 failures.join("\n\n")
4406 );
4407 }
4408
4409 #[test]
4410 fn linker_run_binds_direct_dylib_import_pointers() {
4411 if !have_xcrun() || !have_tool("codesign") {
4412 eprintln!("skipping: xcrun clang or codesign unavailable");
4413 return;
4414 }
4415 let Some(sdk) = sdk_path() else {
4416 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4417 return;
4418 };
4419 let Some(sdk_ver) = sdk_version() else {
4420 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4421 return;
4422 };
4423 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4424 if !tbd.exists() {
4425 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4426 return;
4427 }
4428
4429 let dylib_src = r#"
4430 int ext_data = 5;
4431 "#;
4432 let direct_case = DirectBindParityCase {
4433 name: "direct-data",
4434 dylib_src,
4435 main_src: r#"
4436 extern int ext_data;
4437 int *p = &ext_data;
4438 int main(void) { return *p == 5 ? 0 : 1; }
4439 "#,
4440 };
4441 if let Err(e) = assert_direct_bind_case_matches_apple_ld(&direct_case, &sdk, &sdk_ver) {
4442 panic!("{e}");
4443 }
4444
4445 let dylib = scratch("direct-data.dylib");
4446 let obj = scratch("direct-data.o");
4447 let our_out = scratch("direct-data-ours.out");
4448
4449 if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
4450 eprintln!("skipping: dylib compile failed: {e}");
4451 return;
4452 }
4453
4454 let main_src = r#"
4455 extern int ext_data;
4456 int *p = &ext_data;
4457 int main(void) { return *p == 5 ? 0 : 1; }
4458 "#;
4459 if let Err(e) = compile_c(main_src, &obj) {
4460 eprintln!("skipping: compile failed: {e}");
4461 return;
4462 }
4463
4464 let opts = LinkOptions {
4465 inputs: vec![obj.clone(), tbd.clone(), dylib.clone()],
4466 output: Some(our_out.clone()),
4467 kind: OutputKind::Executable,
4468 ..LinkOptions::default()
4469 };
4470 Linker::run(&opts).unwrap();
4471 let our_bytes = fs::read(&our_out).unwrap();
4472 let binds = decode_bind_records(&our_bytes, false).unwrap();
4473 assert!(
4474 binds.iter().any(|record| {
4475 record.segment == "__DATA"
4476 && record.section == "__data"
4477 && record.section_offset == 0
4478 && record.symbol == "_ext_data"
4479 }),
4480 "missing direct bind for imported data: {binds:#?}"
4481 );
4482 let verify = Command::new("codesign")
4483 .arg("-v")
4484 .arg(&our_out)
4485 .output()
4486 .unwrap();
4487 assert!(
4488 verify.status.success(),
4489 "codesign verify failed: {}",
4490 String::from_utf8_lossy(&verify.stderr)
4491 );
4492 let status = Command::new(&our_out).status().unwrap();
4493 assert_eq!(
4494 status.code(),
4495 Some(0),
4496 "expected direct-import pointer executable to exit 0"
4497 );
4498
4499 let _ = fs::remove_file(dylib);
4500 let _ = fs::remove_file(obj);
4501 let _ = fs::remove_file(our_out);
4502 }
4503
4504 #[test]
4505 fn direct_bind_surfaces_match_apple_ld_across_fixture_matrix() {
4506 if !have_xcrun() || !have_tool("codesign") {
4507 eprintln!("skipping: xcrun clang or codesign unavailable");
4508 return;
4509 }
4510 let Some(sdk) = sdk_path() else {
4511 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4512 return;
4513 };
4514 let Some(sdk_ver) = sdk_version() else {
4515 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4516 return;
4517 };
4518
4519 let cases = [
4520 DirectBindParityCase {
4521 name: "direct-multi-data",
4522 dylib_src: r#"
4523 int ext_data = 5;
4524 int more_data = 9;
4525 "#,
4526 main_src: r#"
4527 extern int ext_data;
4528 extern int more_data;
4529 int *p = &ext_data;
4530 int *q = &more_data;
4531 int main(void) { return (*p == 5 && *q == 9) ? 0 : 1; }
4532 "#,
4533 },
4534 DirectBindParityCase {
4535 name: "direct-and-call-mixed",
4536 dylib_src: r#"
4537 int ext_data = 5;
4538 int ext_fn(void) { return ext_data + 1; }
4539 "#,
4540 main_src: r#"
4541 extern int ext_data;
4542 extern int ext_fn(void);
4543 int *p = &ext_data;
4544 int main(void) { return *p + ext_fn() == 11 ? 0 : 1; }
4545 "#,
4546 },
4547 DirectBindParityCase {
4548 name: "direct-deduped",
4549 dylib_src: r#"
4550 int ext_data = 5;
4551 "#,
4552 main_src: r#"
4553 extern int ext_data;
4554 int *p = &ext_data;
4555 int *q = &ext_data;
4556 int main(void) { return (*p == 5 && *q == 5) ? 0 : 1; }
4557 "#,
4558 },
4559 ];
4560
4561 let mut failures = Vec::new();
4562 for case in &cases {
4563 if let Err(err) = assert_direct_bind_case_matches_apple_ld(case, &sdk, &sdk_ver) {
4564 failures.push(err);
4565 }
4566 }
4567
4568 assert!(
4569 failures.is_empty(),
4570 "Apple ld direct-bind parity failures ({} cases):\n{}",
4571 failures.len(),
4572 failures.join("\n\n")
4573 );
4574 }
4575
4576 #[test]
4577 fn linker_run_rebases_local_absolute_pointers_like_ld() {
4578 if !have_xcrun() {
4579 eprintln!("skipping: xcrun unavailable");
4580 return;
4581 }
4582 let Some(sdk) = sdk_path() else {
4583 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4584 return;
4585 };
4586 let Some(sdk_ver) = sdk_version() else {
4587 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4588 return;
4589 };
4590 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4591 if !tbd.exists() {
4592 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4593 return;
4594 }
4595
4596 let obj = scratch("local-rebase.o");
4597 let our_out = scratch("local-rebase-ours.out");
4598 let apple_out = scratch("local-rebase-apple.out");
4599 let src = r#"
4600 int ext = 7;
4601 int *p = &ext;
4602 int main(void) { return *p == 7 ? 0 : 1; }
4603 "#;
4604 if let Err(e) = compile_c(src, &obj) {
4605 eprintln!("skipping: clang compile failed: {e}");
4606 return;
4607 }
4608
4609 let opts = LinkOptions {
4610 inputs: vec![obj.clone(), tbd],
4611 output: Some(our_out.clone()),
4612 kind: OutputKind::Executable,
4613 ..LinkOptions::default()
4614 };
4615 Linker::run(&opts).unwrap();
4616 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4617
4618 let our_bytes = fs::read(&our_out).unwrap();
4619 let apple_bytes = fs::read(&apple_out).unwrap();
4620 assert!(!dyld_info_stream(&our_bytes, DyldInfoStreamKind::Rebase)
4621 .unwrap()
4622 .is_empty());
4623 assert_eq!(
4624 dyld_info_stream(&our_bytes, DyldInfoStreamKind::Rebase).unwrap(),
4625 dyld_info_stream(&apple_bytes, DyldInfoStreamKind::Rebase).unwrap()
4626 );
4627 assert_eq!(
4628 decode_rebase_records(&our_bytes).unwrap(),
4629 decode_rebase_records(&apple_bytes).unwrap()
4630 );
4631
4632 let our_status = Command::new(&our_out).status().unwrap();
4633 let apple_status = Command::new(&apple_out).status().unwrap();
4634 assert_eq!(our_status.code(), Some(0));
4635 assert_eq!(apple_status.code(), Some(0));
4636
4637 let _ = fs::remove_file(obj);
4638 let _ = fs::remove_file(our_out);
4639 let _ = fs::remove_file(apple_out);
4640 }
4641
4642 #[test]
4643 fn linker_run_routes_local_got_loads_through_rebased_slots() {
4644 if !have_xcrun() || !have_tool("codesign") {
4645 eprintln!("skipping: xcrun or codesign unavailable");
4646 return;
4647 }
4648 let Some(sdk) = sdk_path() else {
4649 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4650 return;
4651 };
4652 let Some(sdk_ver) = sdk_version() else {
4653 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4654 return;
4655 };
4656 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4657 if !tbd.exists() {
4658 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4659 return;
4660 }
4661
4662 let obj = scratch("local-got.o");
4663 let our_out = scratch("local-got-ours.out");
4664 let apple_out = scratch("local-got-apple.out");
4665 let src = r#"
4666 .section __TEXT,__text,regular,pure_instructions
4667 .globl _main
4668 _main:
4669 adrp x8, _value@GOTPAGE
4670 ldr x8, [x8, _value@GOTPAGEOFF]
4671 ldr w0, [x8]
4672 ret
4673
4674 .section __DATA,__data
4675 .globl _value
4676 .p2align 2
4677 _value:
4678 .long 7
4679 .subsections_via_symbols
4680 "#;
4681 if let Err(e) = assemble(src, &obj) {
4682 eprintln!("skipping: assemble failed: {e}");
4683 return;
4684 }
4685
4686 let opts = LinkOptions {
4687 inputs: vec![obj.clone(), tbd.clone()],
4688 output: Some(our_out.clone()),
4689 kind: OutputKind::Executable,
4690 ..LinkOptions::default()
4691 };
4692 Linker::run(&opts).unwrap();
4693 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4694
4695 let our_bytes = fs::read(&our_out).unwrap();
4696 let apple_bytes = fs::read(&apple_out).unwrap();
4697 let our_binds = decode_bind_records(&our_bytes, false).unwrap();
4698 let apple_binds = decode_bind_records(&apple_bytes, false).unwrap();
4699 assert_eq!(our_binds, apple_binds);
4700 assert!(
4701 our_binds.iter().all(|record| record.symbol != "_value"),
4702 "local GOT target should not be emitted as a dylib bind: {our_binds:#?}"
4703 );
4704 assert_eq!(
4705 output_section(&our_bytes, "__DATA_CONST", "__got")
4706 .expect("missing __got section")
4707 .1
4708 .len(),
4709 8
4710 );
4711 let verify = Command::new("codesign")
4712 .arg("-v")
4713 .arg(&our_out)
4714 .output()
4715 .unwrap();
4716 assert!(
4717 verify.status.success(),
4718 "codesign verify failed: {}",
4719 String::from_utf8_lossy(&verify.stderr)
4720 );
4721 let status = Command::new(&our_out).status().unwrap();
4722 assert_eq!(
4723 status.code(),
4724 Some(7),
4725 "expected local GOT executable to exit 7"
4726 );
4727
4728 let _ = fs::remove_file(obj);
4729 let _ = fs::remove_file(our_out);
4730 let _ = fs::remove_file(apple_out);
4731 }
4732
4733 #[test]
4734 fn linker_run_dead_strip_prunes_synthetic_import_sections() {
4735 if !have_xcrun() || !have_tool("codesign") {
4736 eprintln!("skipping: xcrun or codesign unavailable");
4737 return;
4738 }
4739 let Some(sdk) = sdk_path() else {
4740 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4741 return;
4742 };
4743 let Some(sdk_ver) = sdk_version() else {
4744 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4745 return;
4746 };
4747 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4748 if !tbd.exists() {
4749 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4750 return;
4751 }
4752
4753 let obj = scratch("dead-strip-import.o");
4754 let our_out = scratch("dead-strip-import-ours.out");
4755 let apple_out = scratch("dead-strip-import-apple.out");
4756 let src = r#"
4757 .section __TEXT,__text,regular,pure_instructions
4758 .globl _main
4759 _main:
4760 mov w0, #0
4761 ret
4762
4763 .globl _unused
4764 _unused:
4765 bl _puts
4766 mov w0, #0
4767 ret
4768 .subsections_via_symbols
4769 "#;
4770 if let Err(e) = assemble(src, &obj) {
4771 eprintln!("skipping: assemble failed: {e}");
4772 return;
4773 }
4774
4775 let opts = LinkOptions {
4776 inputs: vec![obj.clone(), tbd.clone()],
4777 output: Some(our_out.clone()),
4778 dead_strip: true,
4779 kind: OutputKind::Executable,
4780 ..LinkOptions::default()
4781 };
4782 Linker::run(&opts).unwrap();
4783 apple_link_with_args(
4784 &obj,
4785 &apple_out,
4786 "_main",
4787 &sdk,
4788 &sdk_ver,
4789 &["-dead_strip", "-no_fixup_chains"],
4790 )
4791 .unwrap();
4792
4793 let our_bytes = fs::read(&our_out).unwrap();
4794 let apple_bytes = fs::read(&apple_out).unwrap();
4795 for (segname, sectname) in [
4796 ("__TEXT", "__stubs"),
4797 ("__TEXT", "__stub_helper"),
4798 ("__DATA", "__la_symbol_ptr"),
4799 ("__DATA_CONST", "__got"),
4800 ] {
4801 assert!(
4802 output_section(&our_bytes, segname, sectname).is_none(),
4803 "unexpected synthetic section {segname},{sectname} in our output"
4804 );
4805 assert!(
4806 output_section(&apple_bytes, segname, sectname).is_none(),
4807 "unexpected synthetic section {segname},{sectname} in apple output"
4808 );
4809 }
4810 assert!(decode_bind_records(&our_bytes, false).unwrap().is_empty());
4811 assert_eq!(
4812 decode_bind_records(&our_bytes, false).unwrap(),
4813 decode_bind_records(&apple_bytes, false).unwrap()
4814 );
4815
4816 let verify = Command::new("codesign")
4817 .arg("-v")
4818 .arg(&our_out)
4819 .output()
4820 .unwrap();
4821 assert!(
4822 verify.status.success(),
4823 "codesign verify failed: {}",
4824 String::from_utf8_lossy(&verify.stderr)
4825 );
4826 let status = Command::new(&our_out).status().unwrap();
4827 assert_eq!(status.code(), Some(0));
4828
4829 let _ = fs::remove_file(obj);
4830 let _ = fs::remove_file(our_out);
4831 let _ = fs::remove_file(apple_out);
4832 }
4833
4834 #[test]
4835 fn linker_run_relaxes_hidden_got_loads_like_apple_ld() {
4836 if !have_xcrun() || !have_tool("codesign") {
4837 eprintln!("skipping: xcrun or codesign unavailable");
4838 return;
4839 }
4840 let Some(sdk) = sdk_path() else {
4841 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4842 return;
4843 };
4844 let Some(sdk_ver) = sdk_version() else {
4845 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4846 return;
4847 };
4848 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4849 if !tbd.exists() {
4850 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4851 return;
4852 }
4853
4854 let obj = scratch("hidden-got.o");
4855 let our_out = scratch("hidden-got-ours.out");
4856 let apple_out = scratch("hidden-got-apple.out");
4857 let src = r#"
4858 .section __TEXT,__text,regular,pure_instructions
4859 .globl _main
4860 _main:
4861 adrp x8, _value@GOTPAGE
4862 ldr x8, [x8, _value@GOTPAGEOFF]
4863 ldr w0, [x8]
4864 ret
4865
4866 .private_extern _value
4867 .section __DATA,__data
4868 .p2align 2
4869 _value:
4870 .long 7
4871 .subsections_via_symbols
4872 "#;
4873 if let Err(e) = assemble(src, &obj) {
4874 eprintln!("skipping: assemble failed: {e}");
4875 return;
4876 }
4877
4878 let opts = LinkOptions {
4879 inputs: vec![obj.clone(), tbd.clone()],
4880 output: Some(our_out.clone()),
4881 kind: OutputKind::Executable,
4882 ..LinkOptions::default()
4883 };
4884 Linker::run(&opts).unwrap();
4885 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4886
4887 let our_bytes = fs::read(&our_out).unwrap();
4888 let apple_bytes = fs::read(&apple_out).unwrap();
4889 let (our_text_addr, our_text) = output_section(&our_bytes, "__TEXT", "__text").unwrap();
4890 let (apple_text_addr, apple_text) = output_section(&apple_bytes, "__TEXT", "__text").unwrap();
4891 assert_eq!(
4892 decode_page_reference(&our_text, our_text_addr, 0, &PageRefKind::Add).unwrap(),
4893 decode_page_reference(&apple_text, apple_text_addr, 0, &PageRefKind::Add).unwrap()
4894 );
4895 assert_eq!(our_text, apple_text);
4896 assert!(output_section(&our_bytes, "__DATA_CONST", "__got").is_none());
4897 assert!(output_section(&apple_bytes, "__DATA_CONST", "__got").is_none());
4898
4899 let verify = Command::new("codesign")
4900 .arg("-v")
4901 .arg(&our_out)
4902 .output()
4903 .unwrap();
4904 assert!(
4905 verify.status.success(),
4906 "codesign verify failed: {}",
4907 String::from_utf8_lossy(&verify.stderr)
4908 );
4909 let status = Command::new(&our_out).status().unwrap();
4910 assert_eq!(
4911 status.code(),
4912 Some(7),
4913 "expected hidden GOT executable to exit 7"
4914 );
4915
4916 let _ = fs::remove_file(obj);
4917 let _ = fs::remove_file(our_out);
4918 let _ = fs::remove_file(apple_out);
4919 }
4920
4921 #[test]
4922 fn linker_run_partitions_symtab_like_ld() {
4923 if !have_xcrun() {
4924 eprintln!("skipping: xcrun unavailable");
4925 return;
4926 }
4927
4928 let dylib = scratch("symtab-partition.dylib");
4929 let obj = scratch("symtab-partition.o");
4930 let our_out = scratch("symtab-partition-ours.out");
4931 let apple_out = scratch("symtab-partition-apple.out");
4932
4933 let dylib_src = r#"
4934 int ext_data = 5;
4935 "#;
4936 if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
4937 eprintln!("skipping: dylib compile failed: {e}");
4938 return;
4939 }
4940
4941 let asm = r#"
4942 .text
4943 .private_extern _hidden
4944 .globl _visible
4945 .globl _main
4946 .p2align 2
4947 _local:
4948 ret
4949 _hidden:
4950 ret
4951 _visible:
4952 ret
4953 _main:
4954 ret
4955
4956 .data
4957 .quad _ext_data
4958 .subsections_via_symbols
4959 "#;
4960 if let Err(e) = assemble(asm, &obj) {
4961 eprintln!("skipping: assemble failed: {e}");
4962 return;
4963 }
4964
4965 let opts = LinkOptions {
4966 inputs: vec![obj.clone(), dylib.clone()],
4967 output: Some(our_out.clone()),
4968 kind: OutputKind::Executable,
4969 ..LinkOptions::default()
4970 };
4971 Linker::run(&opts).unwrap();
4972
4973 let apple = Command::new("xcrun")
4974 .args(["ld", "-arch", "arm64", "-e", "_main", "-o"])
4975 .arg(&apple_out)
4976 .arg(&obj)
4977 .arg(&dylib)
4978 .output()
4979 .unwrap();
4980 assert!(
4981 apple.status.success(),
4982 "xcrun ld failed: {}",
4983 String::from_utf8_lossy(&apple.stderr)
4984 );
4985
4986 let our_bytes = fs::read(&our_out).unwrap();
4987 let apple_bytes = fs::read(&apple_out).unwrap();
4988 let (our_symtab, our_dysymtab) = symtab_and_dysymtab(&our_bytes);
4989 let (apple_symtab, apple_dysymtab) = symtab_and_dysymtab(&apple_bytes);
4990
4991 assert_eq!(our_symtab.nsyms, apple_symtab.nsyms);
4992 assert_eq!(our_dysymtab.ilocalsym, apple_dysymtab.ilocalsym);
4993 assert_eq!(our_dysymtab.nlocalsym, apple_dysymtab.nlocalsym);
4994 assert_eq!(our_dysymtab.iextdefsym, apple_dysymtab.iextdefsym);
4995 assert_eq!(our_dysymtab.nextdefsym, apple_dysymtab.nextdefsym);
4996 assert_eq!(our_dysymtab.iundefsym, apple_dysymtab.iundefsym);
4997 assert_eq!(our_dysymtab.nundefsym, apple_dysymtab.nundefsym);
4998 assert_eq!(
4999 canonical_symbol_records(&our_bytes),
5000 canonical_symbol_records(&apple_bytes)
5001 );
5002 assert_strtab_within_five_percent(
5003 &raw_string_table(&our_bytes),
5004 &raw_string_table(&apple_bytes),
5005 );
5006
5007 assert_eq!(
5008 symbol_partition_names(&our_bytes),
5009 symbol_partition_names(&apple_bytes)
5010 );
5011
5012 let _ = fs::remove_file(dylib);
5013 let _ = fs::remove_file(obj);
5014 let _ = fs::remove_file(our_out);
5015 let _ = fs::remove_file(apple_out);
5016 }
5017
5018 #[test]
5019 fn linker_run_strips_locals_with_x_like_ld() {
5020 if !have_xcrun() {
5021 eprintln!("skipping: xcrun unavailable");
5022 return;
5023 }
5024
5025 let dylib = scratch("symtab-strip.dylib");
5026 let obj = scratch("symtab-strip.o");
5027 let our_out = scratch("symtab-strip-ours.out");
5028 let apple_out = scratch("symtab-strip-apple.out");
5029
5030 let dylib_src = r#"
5031 int ext_data = 5;
5032 "#;
5033 if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
5034 eprintln!("skipping: dylib compile failed: {e}");
5035 return;
5036 }
5037
5038 let asm = r#"
5039 .text
5040 .private_extern _hidden
5041 .globl _visible
5042 .globl _main
5043 .p2align 2
5044 _local:
5045 ret
5046 _hidden:
5047 ret
5048 _visible:
5049 ret
5050 _main:
5051 ret
5052
5053 .data
5054 .quad _ext_data
5055 .subsections_via_symbols
5056 "#;
5057 if let Err(e) = assemble(asm, &obj) {
5058 eprintln!("skipping: assemble failed: {e}");
5059 return;
5060 }
5061
5062 let opts = LinkOptions {
5063 inputs: vec![obj.clone(), dylib.clone()],
5064 output: Some(our_out.clone()),
5065 kind: OutputKind::Executable,
5066 strip_locals: true,
5067 ..LinkOptions::default()
5068 };
5069 Linker::run(&opts).unwrap();
5070
5071 let apple = Command::new("xcrun")
5072 .args(["ld", "-arch", "arm64", "-x", "-e", "_main", "-o"])
5073 .arg(&apple_out)
5074 .arg(&obj)
5075 .arg(&dylib)
5076 .output()
5077 .unwrap();
5078 assert!(
5079 apple.status.success(),
5080 "xcrun ld failed: {}",
5081 String::from_utf8_lossy(&apple.stderr)
5082 );
5083
5084 let our_bytes = fs::read(&our_out).unwrap();
5085 let apple_bytes = fs::read(&apple_out).unwrap();
5086 let (our_symtab, our_dysymtab) = symtab_and_dysymtab(&our_bytes);
5087 let (apple_symtab, apple_dysymtab) = symtab_and_dysymtab(&apple_bytes);
5088
5089 assert_eq!(our_symtab.nsyms, apple_symtab.nsyms);
5090 assert_eq!(our_dysymtab.ilocalsym, apple_dysymtab.ilocalsym);
5091 assert_eq!(our_dysymtab.nlocalsym, apple_dysymtab.nlocalsym);
5092 assert_eq!(our_dysymtab.iextdefsym, apple_dysymtab.iextdefsym);
5093 assert_eq!(our_dysymtab.nextdefsym, apple_dysymtab.nextdefsym);
5094 assert_eq!(our_dysymtab.iundefsym, apple_dysymtab.iundefsym);
5095 assert_eq!(our_dysymtab.nundefsym, apple_dysymtab.nundefsym);
5096 assert_eq!(
5097 canonical_symbol_records(&our_bytes),
5098 canonical_symbol_records(&apple_bytes)
5099 );
5100
5101 let (locals, extdefs, undefs) = symbol_partition_names(&our_bytes);
5102 assert!(locals.is_empty());
5103 assert_eq!(
5104 extdefs,
5105 vec![
5106 "__mh_execute_header".to_string(),
5107 "_main".to_string(),
5108 "_visible".to_string()
5109 ]
5110 );
5111 assert_eq!(undefs, vec!["_ext_data".to_string()]);
5112
5113 let _ = fs::remove_file(dylib);
5114 let _ = fs::remove_file(obj);
5115 let _ = fs::remove_file(our_out);
5116 let _ = fs::remove_file(apple_out);
5117 }
5118
5119 #[test]
5120 fn linker_run_emits_leaf_unwind_info_like_ld() {
5121 if !have_xcrun() {
5122 eprintln!("skipping: xcrun unavailable");
5123 return;
5124 }
5125 let Some(sdk) = sdk_path() else {
5126 eprintln!("skipping: xcrun --show-sdk-path unavailable");
5127 return;
5128 };
5129 let Some(sdk_ver) = sdk_version() else {
5130 eprintln!("skipping: xcrun --show-sdk-version unavailable");
5131 return;
5132 };
5133
5134 let obj = scratch("unwind-leaf.o");
5135 let our_out = scratch("unwind-leaf-ours.out");
5136 let apple_out = scratch("unwind-leaf-apple.out");
5137 let src = r#"
5138 int main(void) {
5139 return 0;
5140 }
5141 "#;
5142 if let Err(e) = compile_c(src, &obj) {
5143 eprintln!("skipping: clang compile failed: {e}");
5144 return;
5145 }
5146
5147 let opts = LinkOptions {
5148 inputs: vec![obj.clone()],
5149 output: Some(our_out.clone()),
5150 kind: OutputKind::Executable,
5151 ..LinkOptions::default()
5152 };
5153 Linker::run(&opts).unwrap();
5154 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5155
5156 let our_bytes = fs::read(&our_out).unwrap();
5157 let apple_bytes = fs::read(&apple_out).unwrap();
5158 assert_eq!(
5159 rebased_unwind_bytes(&our_bytes),
5160 rebased_unwind_bytes(&apple_bytes)
5161 );
5162 assert!(output_section(&our_bytes, "__LD", "__compact_unwind").is_none());
5163
5164 let _ = fs::remove_file(obj);
5165 let _ = fs::remove_file(our_out);
5166 let _ = fs::remove_file(apple_out);
5167 }
5168
5169 #[test]
5170 fn linker_run_emits_multi_function_unwind_info_like_ld() {
5171 if !have_xcrun() {
5172 eprintln!("skipping: xcrun unavailable");
5173 return;
5174 }
5175 let Some(sdk) = sdk_path() else {
5176 eprintln!("skipping: xcrun --show-sdk-path unavailable");
5177 return;
5178 };
5179 let Some(sdk_ver) = sdk_version() else {
5180 eprintln!("skipping: xcrun --show-sdk-version unavailable");
5181 return;
5182 };
5183
5184 let obj = scratch("unwind-mixed.o");
5185 let our_out = scratch("unwind-mixed-ours.out");
5186 let apple_out = scratch("unwind-mixed-apple.out");
5187 let src = r#"
5188 int helper(void) {
5189 return 1;
5190 }
5191
5192 int main(void) {
5193 return helper();
5194 }
5195 "#;
5196 if let Err(e) = compile_c(src, &obj) {
5197 eprintln!("skipping: clang compile failed: {e}");
5198 return;
5199 }
5200
5201 let opts = LinkOptions {
5202 inputs: vec![obj.clone()],
5203 output: Some(our_out.clone()),
5204 kind: OutputKind::Executable,
5205 ..LinkOptions::default()
5206 };
5207 Linker::run(&opts).unwrap();
5208 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5209
5210 let our_bytes = fs::read(&our_out).unwrap();
5211 let apple_bytes = fs::read(&apple_out).unwrap();
5212 assert_eq!(
5213 rebased_unwind_bytes(&our_bytes),
5214 rebased_unwind_bytes(&apple_bytes)
5215 );
5216 assert!(output_section(&our_bytes, "__LD", "__compact_unwind").is_none());
5217
5218 let _ = fs::remove_file(obj);
5219 let _ = fs::remove_file(our_out);
5220 let _ = fs::remove_file(apple_out);
5221 }
5222
5223 #[test]
5224 fn linker_run_dead_strip_prunes_unused_unwind_records_like_ld() {
5225 if !have_xcrun() {
5226 eprintln!("skipping: xcrun unavailable");
5227 return;
5228 }
5229 let Some(sdk) = sdk_path() else {
5230 eprintln!("skipping: xcrun --show-sdk-path unavailable");
5231 return;
5232 };
5233 let Some(sdk_ver) = sdk_version() else {
5234 eprintln!("skipping: xcrun --show-sdk-version unavailable");
5235 return;
5236 };
5237
5238 let obj = scratch("unwind-dead-strip.o");
5239 let our_out = scratch("unwind-dead-strip-ours.out");
5240 let apple_out = scratch("unwind-dead-strip-apple.out");
5241 let src = r#"
5242 int helper(void) {
5243 return 1;
5244 }
5245
5246 int unused(void) {
5247 return 2;
5248 }
5249
5250 int main(void) {
5251 return helper();
5252 }
5253 "#;
5254 if let Err(e) = compile_c(src, &obj) {
5255 eprintln!("skipping: clang compile failed: {e}");
5256 return;
5257 }
5258
5259 let opts = LinkOptions {
5260 inputs: vec![obj.clone()],
5261 output: Some(our_out.clone()),
5262 kind: OutputKind::Executable,
5263 dead_strip: true,
5264 ..LinkOptions::default()
5265 };
5266 Linker::run(&opts).unwrap();
5267 apple_link_with_args(&obj, &apple_out, "_main", &sdk, &sdk_ver, &["-dead_strip"]).unwrap();
5268
5269 let our_bytes = fs::read(&our_out).unwrap();
5270 let apple_bytes = fs::read(&apple_out).unwrap();
5271 let (_, our_unwind) = output_section(&our_bytes, "__TEXT", "__unwind_info").unwrap();
5272 let (_, apple_unwind) = output_section(&apple_bytes, "__TEXT", "__unwind_info").unwrap();
5273 let our_decoded = decode_unwind_info(&our_unwind).unwrap();
5274 let apple_decoded = decode_unwind_info(&apple_unwind).unwrap();
5275 let normalize = |records: &[afs_ld::synth::unwind::DecodedUnwindRecord]| {
5276 let base = records
5277 .first()
5278 .map(|record| record.function_offset)
5279 .unwrap_or(0);
5280 records
5281 .iter()
5282 .map(|record| (record.function_offset - base, record.encoding))
5283 .collect::<Vec<_>>()
5284 };
5285 assert_eq!(
5286 normalize(&our_decoded.records),
5287 normalize(&apple_decoded.records)
5288 );
5289 assert_eq!(our_decoded.records.len(), 2);
5290
5291 let _ = fs::remove_file(obj);
5292 let _ = fs::remove_file(our_out);
5293 let _ = fs::remove_file(apple_out);
5294 }
5295
5296 #[test]
5297 fn linker_run_handles_large_unwind_function_gaps() {
5298 if !have_xcrun() {
5299 eprintln!("skipping: xcrun unavailable");
5300 return;
5301 }
5302
5303 let obj = scratch("unwind-gap.o");
5304 let out = scratch("unwind-gap-ours.out");
5305 let asm = r#"
5306 .text
5307 .globl _main
5308 .p2align 2
5309 _main:
5310 .cfi_startproc
5311 bl _helper
5312 ret
5313 .cfi_endproc
5314 .space 0x1000010
5315 .globl _helper
5316 .p2align 2
5317 _helper:
5318 .cfi_startproc
5319 ret
5320 .cfi_endproc
5321 .subsections_via_symbols
5322 "#;
5323 if let Err(e) = assemble(asm, &obj) {
5324 eprintln!("skipping: assemble failed: {e}");
5325 return;
5326 }
5327
5328 let opts = LinkOptions {
5329 inputs: vec![obj.clone()],
5330 output: Some(out.clone()),
5331 kind: OutputKind::Executable,
5332 ..LinkOptions::default()
5333 };
5334 Linker::run(&opts).unwrap();
5335
5336 let bytes = fs::read(&out).unwrap();
5337 let (_, unwind) = output_section(&bytes, "__TEXT", "__unwind_info").unwrap();
5338 let decoded = decode_unwind_info(&unwind).unwrap();
5339 assert!(
5340 decoded
5341 .records
5342 .windows(2)
5343 .all(|pair| pair[0].function_offset < pair[1].function_offset),
5344 "expected strictly ascending unwind records after large-gap pagination"
5345 );
5346
5347 let _ = fs::remove_file(obj);
5348 let _ = fs::remove_file(out);
5349 }
5350
5351 #[test]
5352 fn linker_run_preserves_eh_frame_like_ld() {
5353 if !have_xcrun() || !have_xcrun_tool("dwarfdump") {
5354 eprintln!("skipping: xcrun dwarfdump unavailable");
5355 return;
5356 }
5357 let Some(sdk) = sdk_path() else {
5358 eprintln!("skipping: xcrun --show-sdk-path unavailable");
5359 return;
5360 };
5361 let Some(sdk_ver) = sdk_version() else {
5362 eprintln!("skipping: xcrun --show-sdk-version unavailable");
5363 return;
5364 };
5365
5366 let obj = scratch("eh-frame.o");
5367 let our_out = scratch("eh-frame-ours.out");
5368 let apple_out = scratch("eh-frame-apple.out");
5369 let asm = r#"
5370 .text
5371 .globl _main
5372 .p2align 2
5373 _main:
5374 .cfi_startproc
5375 sub sp, sp, #16
5376 .cfi_def_cfa_offset 16
5377 str x30, [sp, #8]
5378 .cfi_offset w30, -8
5379 bl _helper
5380 ldr x30, [sp, #8]
5381 add sp, sp, #16
5382 ret
5383 .cfi_endproc
5384
5385 .globl _helper
5386 .p2align 2
5387 _helper:
5388 .cfi_startproc
5389 ret
5390 .cfi_endproc
5391 .subsections_via_symbols
5392 "#;
5393 if let Err(e) = assemble(asm, &obj) {
5394 eprintln!("skipping: assemble failed: {e}");
5395 return;
5396 }
5397
5398 let opts = LinkOptions {
5399 inputs: vec![obj.clone()],
5400 output: Some(our_out.clone()),
5401 kind: OutputKind::Executable,
5402 ..LinkOptions::default()
5403 };
5404 Linker::run(&opts).unwrap();
5405 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5406
5407 let our_bytes = fs::read(&our_out).unwrap();
5408 let apple_bytes = fs::read(&apple_out).unwrap();
5409 assert!(output_section(&our_bytes, "__TEXT", "__eh_frame").is_some());
5410 assert_eq!(
5411 output_section(&our_bytes, "__TEXT", "__eh_frame")
5412 .unwrap()
5413 .1
5414 .len(),
5415 output_section(&apple_bytes, "__TEXT", "__eh_frame")
5416 .unwrap()
5417 .1
5418 .len()
5419 );
5420 let our_dump = normalized_eh_frame_dump(
5421 &our_out,
5422 output_section(&our_bytes, "__TEXT", "__text").unwrap().0,
5423 )
5424 .unwrap();
5425 let apple_dump = normalized_eh_frame_dump(
5426 &apple_out,
5427 output_section(&apple_bytes, "__TEXT", "__text").unwrap().0,
5428 )
5429 .unwrap();
5430 assert_eq!(our_dump, apple_dump);
5431
5432 let _ = fs::remove_file(obj);
5433 let _ = fs::remove_file(our_out);
5434 let _ = fs::remove_file(apple_out);
5435 }
5436
5437 #[test]
5438 fn linker_run_dead_strip_preserves_pruned_eh_frame_like_ld() {
5439 if !have_xcrun() || !have_xcrun_tool("dwarfdump") {
5440 eprintln!("skipping: xcrun dwarfdump unavailable");
5441 return;
5442 }
5443 let Some(sdk) = sdk_path() else {
5444 eprintln!("skipping: xcrun --show-sdk-path unavailable");
5445 return;
5446 };
5447 let Some(sdk_ver) = sdk_version() else {
5448 eprintln!("skipping: xcrun --show-sdk-version unavailable");
5449 return;
5450 };
5451
5452 let obj = scratch("eh-frame-dead-strip.o");
5453 let our_out = scratch("eh-frame-dead-strip-ours.out");
5454 let apple_out = scratch("eh-frame-dead-strip-apple.out");
5455 let asm = r#"
5456 .text
5457 .globl _main
5458 .p2align 2
5459 _main:
5460 .cfi_startproc
5461 sub sp, sp, #16
5462 .cfi_def_cfa_offset 16
5463 str x30, [sp, #8]
5464 .cfi_offset w30, -8
5465 bl _helper
5466 ldr x30, [sp, #8]
5467 add sp, sp, #16
5468 ret
5469 .cfi_endproc
5470
5471 .globl _helper
5472 .p2align 2
5473 _helper:
5474 .cfi_startproc
5475 sub sp, sp, #16
5476 .cfi_def_cfa_offset 16
5477 str x30, [sp, #8]
5478 .cfi_offset w30, -8
5479 ldr x30, [sp, #8]
5480 add sp, sp, #16
5481 ret
5482 .cfi_endproc
5483
5484 .globl _unused
5485 .p2align 2
5486 _unused:
5487 .cfi_startproc
5488 sub sp, sp, #16
5489 .cfi_def_cfa_offset 16
5490 str x30, [sp, #8]
5491 .cfi_offset w30, -8
5492 ldr x30, [sp, #8]
5493 add sp, sp, #16
5494 ret
5495 .cfi_endproc
5496 .subsections_via_symbols
5497 "#;
5498 if let Err(e) = assemble(asm, &obj) {
5499 eprintln!("skipping: assemble failed: {e}");
5500 return;
5501 }
5502
5503 let opts = LinkOptions {
5504 inputs: vec![obj.clone()],
5505 output: Some(our_out.clone()),
5506 kind: OutputKind::Executable,
5507 dead_strip: true,
5508 ..LinkOptions::default()
5509 };
5510 Linker::run(&opts).unwrap();
5511 apple_link_with_args(&obj, &apple_out, "_main", &sdk, &sdk_ver, &["-dead_strip"]).unwrap();
5512
5513 let our_bytes = fs::read(&our_out).unwrap();
5514 let apple_bytes = fs::read(&apple_out).unwrap();
5515 assert!(output_section(&our_bytes, "__TEXT", "__eh_frame").is_some());
5516 assert_eq!(
5517 output_section(&our_bytes, "__TEXT", "__eh_frame")
5518 .unwrap()
5519 .1
5520 .len(),
5521 output_section(&apple_bytes, "__TEXT", "__eh_frame")
5522 .unwrap()
5523 .1
5524 .len()
5525 );
5526 let our_dump = normalized_eh_frame_dump(
5527 &our_out,
5528 output_section(&our_bytes, "__TEXT", "__text").unwrap().0,
5529 )
5530 .unwrap();
5531 let apple_dump = normalized_eh_frame_dump(
5532 &apple_out,
5533 output_section(&apple_bytes, "__TEXT", "__text").unwrap().0,
5534 )
5535 .unwrap();
5536 assert_eq!(our_dump, apple_dump);
5537
5538 let _ = fs::remove_file(obj);
5539 let _ = fs::remove_file(our_out);
5540 let _ = fs::remove_file(apple_out);
5541 }
5542
5543 #[test]
5544 fn linker_run_emits_backtrace_metadata_like_apple_ld() {
5545 if !have_xcrun() {
5546 eprintln!("skipping: xcrun unavailable");
5547 return;
5548 }
5549 let Some(sdk) = sdk_path() else {
5550 eprintln!("skipping: xcrun --show-sdk-path unavailable");
5551 return;
5552 };
5553 let Some(sdk_ver) = sdk_version() else {
5554 eprintln!("skipping: xcrun --show-sdk-version unavailable");
5555 return;
5556 };
5557 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5558 if !tbd.exists() {
5559 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5560 return;
5561 }
5562
5563 let obj = scratch("unwind-backtrace.o");
5564 let our_out = scratch("unwind-backtrace-ours.out");
5565 let apple_out = scratch("unwind-backtrace-apple.out");
5566 let src = r#"
5567 #include <unwind.h>
5568
5569 static _Unwind_Reason_Code cb(struct _Unwind_Context* ctx, void* arg) {
5570 (void)ctx;
5571 int* count = (int*)arg;
5572 (*count)++;
5573 return *count >= 8 ? _URC_END_OF_STACK : _URC_NO_REASON;
5574 }
5575
5576 __attribute__((noinline)) int helper(void) {
5577 int count = 0;
5578 _Unwind_Backtrace(cb, &count);
5579 return count;
5580 }
5581
5582 int main(void) {
5583 return helper() > 1 ? 0 : 1;
5584 }
5585 "#;
5586 if let Err(e) = compile_c(src, &obj) {
5587 eprintln!("skipping: clang compile failed: {e}");
5588 return;
5589 }
5590
5591 let opts = LinkOptions {
5592 inputs: vec![obj.clone(), tbd],
5593 output: Some(our_out.clone()),
5594 kind: OutputKind::Executable,
5595 ..LinkOptions::default()
5596 };
5597 Linker::run(&opts).unwrap();
5598 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5599
5600 let our_bytes = fs::read(&our_out).unwrap();
5601 let apple_bytes = fs::read(&apple_out).unwrap();
5602 assert_eq!(
5603 rebased_unwind_bytes(&our_bytes),
5604 rebased_unwind_bytes(&apple_bytes)
5605 );
5606 assert_eq!(
5607 normalize_function_start_offsets(&decode_function_starts(&our_bytes)),
5608 normalize_function_start_offsets(&decode_function_starts(&apple_bytes))
5609 );
5610
5611 let _ = fs::remove_file(obj);
5612 let _ = fs::remove_file(our_out);
5613 let _ = fs::remove_file(apple_out);
5614 }
5615
5616 #[test]
5617 fn linker_run_preserves_exception_unwind_metadata_like_apple_ld() {
5618 if !have_xcrun() || !have_xcrun_tool("clang++") || !have_tool("codesign") {
5619 eprintln!("skipping: xcrun clang++ or codesign unavailable");
5620 return;
5621 }
5622
5623 let Some(sdk) = sdk_path() else {
5624 eprintln!("skipping: xcrun --show-sdk-path unavailable");
5625 return;
5626 };
5627 let libsystem = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5628 let libcxx = PathBuf::from(format!("{sdk}/usr/lib/libc++.tbd"));
5629 if !libsystem.exists() {
5630 eprintln!("skipping: no libSystem.tbd at {}", libsystem.display());
5631 return;
5632 }
5633 if !libcxx.exists() {
5634 eprintln!("skipping: no libc++.tbd at {}", libcxx.display());
5635 return;
5636 }
5637
5638 let obj = scratch("cxx-exc.o");
5639 let our_out = scratch("cxx-exc-ours.out");
5640 let apple_out = scratch("cxx-exc-apple.out");
5641 let src = r#"
5642 int helper() { throw 7; }
5643 int main() {
5644 try { return helper(); }
5645 catch (...) { return 42; }
5646 }
5647 "#;
5648 if let Err(e) = compile_cxx(src, &obj) {
5649 eprintln!("skipping: clang++ compile failed: {e}");
5650 return;
5651 }
5652
5653 let opts = LinkOptions {
5654 inputs: vec![obj.clone(), libcxx.clone(), libsystem.clone()],
5655 output: Some(our_out.clone()),
5656 kind: OutputKind::Executable,
5657 ..LinkOptions::default()
5658 };
5659 Linker::run(&opts).unwrap();
5660 apple_link_cxx_classic(&obj, &apple_out).unwrap();
5661
5662 let our_bytes = fs::read(&our_out).unwrap();
5663 let apple_bytes = fs::read(&apple_out).unwrap();
5664 assert_eq!(
5665 decode_bind_records(&our_bytes, false).unwrap(),
5666 decode_bind_records(&apple_bytes, false).unwrap()
5667 );
5668 assert_eq!(
5669 decode_bind_records(&our_bytes, true).unwrap(),
5670 decode_bind_records(&apple_bytes, true).unwrap()
5671 );
5672 assert_eq!(
5673 canonical_lazy_bind_stream(&our_bytes).unwrap(),
5674 canonical_lazy_bind_stream(&apple_bytes).unwrap()
5675 );
5676 let our_decoded = canonical_unwind_info(&our_bytes);
5677 let apple_decoded = canonical_unwind_info(&apple_bytes);
5678 assert_eq!(our_decoded, apple_decoded);
5679 assert_eq!(our_decoded.personalities.len(), 1);
5680 assert_eq!(our_decoded.lsdas.len(), 1);
5681 assert!(output_section(&our_bytes, "__TEXT", "__gcc_except_tab").is_some());
5682 let our_status = Command::new(&our_out).status().unwrap();
5683 let apple_status = Command::new(&apple_out).status().unwrap();
5684 assert_eq!(our_status.code(), Some(42));
5685 assert_eq!(apple_status.code(), Some(42));
5686
5687 let _ = fs::remove_file(obj);
5688 let _ = fs::remove_file(our_out);
5689 let _ = fs::remove_file(apple_out);
5690 }
5691
5692 #[test]
5693 fn linker_run_resolves_backtrace_symbols_at_runtime() {
5694 if !have_xcrun() {
5695 eprintln!("skipping: xcrun unavailable");
5696 return;
5697 }
5698 let Some(sdk) = sdk_path() else {
5699 eprintln!("skipping: xcrun --show-sdk-path unavailable");
5700 return;
5701 };
5702 let Some(sdk_ver) = sdk_version() else {
5703 eprintln!("skipping: xcrun --show-sdk-version unavailable");
5704 return;
5705 };
5706 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5707 if !tbd.exists() {
5708 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5709 return;
5710 }
5711
5712 let obj = scratch("execinfo-backtrace.o");
5713 let our_out = scratch("execinfo-backtrace-ours.out");
5714 let apple_out = scratch("execinfo-backtrace-apple.out");
5715 let src = r#"
5716 #include <execinfo.h>
5717 #include <stdio.h>
5718 #include <stdlib.h>
5719 #include <string.h>
5720
5721 __attribute__((noinline)) int helper(void) {
5722 void *frames[8];
5723 int n = backtrace(frames, 8);
5724 char **syms = backtrace_symbols(frames, n);
5725 int saw_helper = 0;
5726 int saw_main = 0;
5727 if (!syms) return 2;
5728 for (int i = 0; i < n; i++) {
5729 puts(syms[i]);
5730 saw_helper |= strstr(syms[i], "helper") != NULL;
5731 saw_main |= strstr(syms[i], "main") != NULL;
5732 }
5733 free(syms);
5734 return (saw_helper && saw_main) ? 0 : 1;
5735 }
5736
5737 int main(void) {
5738 return helper();
5739 }
5740 "#;
5741 if let Err(e) = compile_c(src, &obj) {
5742 eprintln!("skipping: clang compile failed: {e}");
5743 return;
5744 }
5745
5746 let opts = LinkOptions {
5747 inputs: vec![obj.clone(), tbd],
5748 output: Some(our_out.clone()),
5749 kind: OutputKind::Executable,
5750 ..LinkOptions::default()
5751 };
5752 Linker::run(&opts).unwrap();
5753 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5754
5755 let our_output = Command::new(&our_out).output().unwrap();
5756 let apple_output = Command::new(&apple_out).output().unwrap();
5757 let our_stdout = String::from_utf8_lossy(&our_output.stdout);
5758 let apple_stdout = String::from_utf8_lossy(&apple_output.stdout);
5759
5760 assert_eq!(our_output.status.code(), Some(0));
5761 assert_eq!(apple_output.status.code(), Some(0));
5762 assert!(
5763 our_stdout.contains("helper"),
5764 "expected helper in output: {our_stdout}"
5765 );
5766 assert!(
5767 our_stdout.contains("main"),
5768 "expected main in output: {our_stdout}"
5769 );
5770 assert!(
5771 apple_stdout.contains("helper"),
5772 "expected helper in apple output: {apple_stdout}"
5773 );
5774 assert!(
5775 apple_stdout.contains("main"),
5776 "expected main in apple output: {apple_stdout}"
5777 );
5778
5779 let _ = fs::remove_file(obj);
5780 let _ = fs::remove_file(our_out);
5781 let _ = fs::remove_file(apple_out);
5782 }
5783
5784 #[test]
5785 fn linker_run_emits_function_starts_like_ld() {
5786 if !have_xcrun() {
5787 eprintln!("skipping: xcrun unavailable");
5788 return;
5789 }
5790 let Some(sdk) = sdk_path() else {
5791 eprintln!("skipping: xcrun --show-sdk-path unavailable");
5792 return;
5793 };
5794 let Some(sdk_ver) = sdk_version() else {
5795 eprintln!("skipping: xcrun --show-sdk-version unavailable");
5796 return;
5797 };
5798 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5799 if !tbd.exists() {
5800 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5801 return;
5802 }
5803
5804 let obj = scratch("function-starts.o");
5805 let our_out = scratch("function-starts-ours.out");
5806 let apple_out = scratch("function-starts-apple.out");
5807 let asm = r#"
5808 .section __TEXT,__text,regular,pure_instructions
5809 .globl _main
5810 .p2align 2
5811 _main:
5812 adrp x0, _write@GOTPAGE
5813 ldr x0, [x0, _write@GOTPAGEOFF]
5814 bl _write
5815 ret
5816 .subsections_via_symbols
5817 "#;
5818 if let Err(e) = assemble(asm, &obj) {
5819 eprintln!("skipping: assemble failed: {e}");
5820 return;
5821 }
5822
5823 let opts = LinkOptions {
5824 inputs: vec![obj.clone(), tbd],
5825 output: Some(our_out.clone()),
5826 kind: OutputKind::Executable,
5827 ..LinkOptions::default()
5828 };
5829 Linker::run(&opts).unwrap();
5830 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5831
5832 let our_bytes = fs::read(&our_out).unwrap();
5833 let apple_bytes = fs::read(&apple_out).unwrap();
5834 let our_fstarts = raw_linkedit_data_cmd(&our_bytes, LC_FUNCTION_STARTS);
5835 let apple_fstarts = raw_linkedit_data_cmd(&apple_bytes, LC_FUNCTION_STARTS);
5836 assert_ne!(our_fstarts.0, 0);
5837 assert_eq!(our_fstarts.1, apple_fstarts.1);
5838 assert_eq!(our_fstarts.1, 8);
5839 assert!(output_section(&our_bytes, "__TEXT", "__stubs").is_some());
5840 assert!(output_section(&our_bytes, "__TEXT", "__stub_helper").is_some());
5841 assert_eq!(decode_function_starts(&our_bytes).len(), 1);
5842 assert_eq!(decode_function_starts(&apple_bytes).len(), 1);
5843 let our_text_addr = output_section(&our_bytes, "__TEXT", "__text").unwrap().0;
5844 let apple_text_addr = output_section(&apple_bytes, "__TEXT", "__text").unwrap().0;
5845 let our_text_base = segment_vmaddr(&our_bytes, "__TEXT").unwrap();
5846 let apple_text_base = segment_vmaddr(&apple_bytes, "__TEXT").unwrap();
5847 assert_eq!(
5848 decode_function_starts(&our_bytes),
5849 vec![our_text_addr - our_text_base]
5850 );
5851 assert_eq!(
5852 decode_function_starts(&apple_bytes),
5853 vec![apple_text_addr - apple_text_base]
5854 );
5855
5856 let our_dic = raw_linkedit_data_cmd(&our_bytes, LC_DATA_IN_CODE);
5857 let apple_dic = raw_linkedit_data_cmd(&apple_bytes, LC_DATA_IN_CODE);
5858 assert_ne!(our_dic.0, 0);
5859 assert_eq!(our_dic.1, apple_dic.1);
5860 assert_eq!(our_dic.0, our_fstarts.0 + our_fstarts.1);
5861 assert_eq!(apple_dic.0, apple_fstarts.0 + apple_fstarts.1);
5862
5863 let _ = fs::remove_file(obj);
5864 let _ = fs::remove_file(our_out);
5865 let _ = fs::remove_file(apple_out);
5866 }
5867
5868 #[test]
5869 fn linker_run_emits_function_starts_for_other_text_sections_like_ld() {
5870 if !have_xcrun() {
5871 eprintln!("skipping: xcrun unavailable");
5872 return;
5873 }
5874 let Some(sdk) = sdk_path() else {
5875 eprintln!("skipping: xcrun --show-sdk-path unavailable");
5876 return;
5877 };
5878 let Some(sdk_ver) = sdk_version() else {
5879 eprintln!("skipping: xcrun --show-sdk-version unavailable");
5880 return;
5881 };
5882
5883 let obj = scratch("function-starts-textcoal.o");
5884 let our_out = scratch("function-starts-textcoal-ours.out");
5885 let apple_out = scratch("function-starts-textcoal-apple.out");
5886 let asm = r#"
5887 .section __TEXT,__text,regular,pure_instructions
5888 .globl _main
5889 .p2align 2
5890 _main:
5891 ret
5892
5893 .section __TEXT,__textcoal_nt,regular,pure_instructions
5894 .globl _helper
5895 .p2align 2
5896 _helper:
5897 ret
5898 .subsections_via_symbols
5899 "#;
5900 if let Err(e) = assemble(asm, &obj) {
5901 eprintln!("skipping: assemble failed: {e}");
5902 return;
5903 }
5904
5905 let opts = LinkOptions {
5906 inputs: vec![obj.clone()],
5907 output: Some(our_out.clone()),
5908 kind: OutputKind::Executable,
5909 ..LinkOptions::default()
5910 };
5911 Linker::run(&opts).unwrap();
5912 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5913
5914 let our_bytes = fs::read(&our_out).unwrap();
5915 let apple_bytes = fs::read(&apple_out).unwrap();
5916 assert_eq!(decode_function_starts(&our_bytes).len(), 2);
5917 assert_eq!(decode_function_starts(&apple_bytes).len(), 2);
5918
5919 let our_text_addr = output_section(&our_bytes, "__TEXT", "__text").unwrap().0;
5920 let our_textcoal_addr = output_section(&our_bytes, "__TEXT", "__textcoal_nt")
5921 .unwrap()
5922 .0;
5923 let our_text_base = segment_vmaddr(&our_bytes, "__TEXT").unwrap();
5924 assert_eq!(
5925 decode_function_starts(&our_bytes),
5926 vec![
5927 our_text_addr - our_text_base,
5928 our_textcoal_addr - our_text_base
5929 ]
5930 );
5931
5932 let _ = fs::remove_file(obj);
5933 let _ = fs::remove_file(our_out);
5934 let _ = fs::remove_file(apple_out);
5935 }
5936
5937 #[test]
5938 fn linker_run_remaps_data_in_code_like_ld() {
5939 if !have_xcrun() {
5940 eprintln!("skipping: xcrun unavailable");
5941 return;
5942 }
5943 let Some(sdk) = sdk_path() else {
5944 eprintln!("skipping: xcrun --show-sdk-path unavailable");
5945 return;
5946 };
5947 let Some(sdk_ver) = sdk_version() else {
5948 eprintln!("skipping: xcrun --show-sdk-version unavailable");
5949 return;
5950 };
5951 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
5952 if !tbd.exists() {
5953 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
5954 return;
5955 }
5956
5957 let obj = scratch("data-in-code.o");
5958 let our_out = scratch("data-in-code-ours.out");
5959 let apple_out = scratch("data-in-code-apple.out");
5960 let asm = r#"
5961 .text
5962 .globl _main
5963 .p2align 2
5964 _main:
5965 mov w0, #0
5966 b Ldispatch
5967 .p2align 2
5968 Ltable:
5969 .data_region jt32
5970 .long Lcase0-Ltable
5971 .long Lcase1-Ltable
5972 .end_data_region
5973 Ldispatch:
5974 cmp w0, #0
5975 b.eq Lcase0
5976 b Lcase1
5977 Lcase0:
5978 mov w0, #1
5979 ret
5980 Lcase1:
5981 mov w0, #2
5982 ret
5983 .subsections_via_symbols
5984 "#;
5985 if let Err(e) = assemble(asm, &obj) {
5986 eprintln!("skipping: assemble failed: {e}");
5987 return;
5988 }
5989
5990 let opts = LinkOptions {
5991 inputs: vec![obj.clone(), tbd],
5992 output: Some(our_out.clone()),
5993 kind: OutputKind::Executable,
5994 ..LinkOptions::default()
5995 };
5996 Linker::run(&opts).unwrap();
5997 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
5998
5999 let our_bytes = fs::read(&our_out).unwrap();
6000 let apple_bytes = fs::read(&apple_out).unwrap();
6001 let our_dic = raw_linkedit_data_cmd(&our_bytes, LC_DATA_IN_CODE);
6002 let apple_dic = raw_linkedit_data_cmd(&apple_bytes, LC_DATA_IN_CODE);
6003 assert_ne!(our_dic.1, 0);
6004 assert_eq!(our_dic.1, apple_dic.1);
6005 assert_eq!(decode_data_in_code(&our_bytes).len(), 1);
6006 assert_eq!(
6007 canonical_data_in_code(&our_bytes),
6008 canonical_data_in_code(&apple_bytes)
6009 );
6010 assert_eq!(
6011 canonical_data_in_code(&our_bytes),
6012 vec![DataInCodeRecord {
6013 offset: 8,
6014 length: 8,
6015 kind: DICE_KIND_JUMP_TABLE32,
6016 }]
6017 );
6018
6019 let _ = fs::remove_file(obj);
6020 let _ = fs::remove_file(our_out);
6021 let _ = fs::remove_file(apple_out);
6022 }
6023
6024 #[test]
6025 fn linker_run_remaps_data_in_code_in_later_text_section_like_ld() {
6026 if !have_xcrun() {
6027 eprintln!("skipping: xcrun unavailable");
6028 return;
6029 }
6030 let Some(sdk) = sdk_path() else {
6031 eprintln!("skipping: xcrun --show-sdk-path unavailable");
6032 return;
6033 };
6034 let Some(sdk_ver) = sdk_version() else {
6035 eprintln!("skipping: xcrun --show-sdk-version unavailable");
6036 return;
6037 };
6038 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6039 if !tbd.exists() {
6040 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
6041 return;
6042 }
6043
6044 let obj = scratch("data-in-code-late.o");
6045 let our_out = scratch("data-in-code-late-ours.out");
6046 let apple_out = scratch("data-in-code-late-apple.out");
6047 let asm = r#"
6048 .text
6049 .globl _main
6050 .p2align 2
6051 _main:
6052 ret
6053
6054 .section __TEXT,__text2,regular,pure_instructions
6055 .globl _helper
6056 .p2align 2
6057 _helper:
6058 b Ldispatch
6059 .p2align 2
6060 Ltable:
6061 .data_region jt32
6062 .long Lcase0-Ltable
6063 .long Lcase1-Ltable
6064 .end_data_region
6065 Ldispatch:
6066 ret
6067 Lcase0:
6068 ret
6069 Lcase1:
6070 ret
6071 .subsections_via_symbols
6072 "#;
6073 if let Err(e) = assemble(asm, &obj) {
6074 eprintln!("skipping: assemble failed: {e}");
6075 return;
6076 }
6077
6078 let opts = LinkOptions {
6079 inputs: vec![obj.clone(), tbd],
6080 output: Some(our_out.clone()),
6081 kind: OutputKind::Executable,
6082 ..LinkOptions::default()
6083 };
6084 Linker::run(&opts).unwrap();
6085 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
6086
6087 let our_bytes = fs::read(&our_out).unwrap();
6088 let apple_bytes = fs::read(&apple_out).unwrap();
6089 assert_eq!(
6090 canonical_data_in_code(&our_bytes),
6091 canonical_data_in_code(&apple_bytes)
6092 );
6093 assert_eq!(
6094 canonical_data_in_code(&our_bytes),
6095 vec![DataInCodeRecord {
6096 offset: 8,
6097 length: 8,
6098 kind: DICE_KIND_JUMP_TABLE32,
6099 }]
6100 );
6101
6102 let _ = fs::remove_file(obj);
6103 let _ = fs::remove_file(our_out);
6104 let _ = fs::remove_file(apple_out);
6105 }
6106
6107 #[test]
6108 fn linker_run_remaps_data_in_code_after_large_first_text_section_like_ld() {
6109 if !have_xcrun() {
6110 eprintln!("skipping: xcrun unavailable");
6111 return;
6112 }
6113 let Some(sdk) = sdk_path() else {
6114 eprintln!("skipping: xcrun --show-sdk-path unavailable");
6115 return;
6116 };
6117 let Some(sdk_ver) = sdk_version() else {
6118 eprintln!("skipping: xcrun --show-sdk-version unavailable");
6119 return;
6120 };
6121 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6122 if !tbd.exists() {
6123 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
6124 return;
6125 }
6126
6127 let obj = scratch("data-in-code-large-first.o");
6128 let our_out = scratch("data-in-code-large-first-ours.out");
6129 let apple_out = scratch("data-in-code-large-first-apple.out");
6130 let asm = r#"
6131 .text
6132 .globl _main
6133 .p2align 2
6134 _main:
6135 nop
6136 nop
6137 nop
6138 nop
6139 nop
6140 ret
6141
6142 .section __TEXT,__text2,regular,pure_instructions
6143 .globl _helper
6144 _helper:
6145 b Ldispatch
6146 .p2align 2
6147 Ltable:
6148 .data_region jt32
6149 .long Lcase0-Ltable
6150 .long Lcase1-Ltable
6151 .end_data_region
6152 Ldispatch:
6153 ret
6154 Lcase0:
6155 ret
6156 Lcase1:
6157 ret
6158 .subsections_via_symbols
6159 "#;
6160 if let Err(e) = assemble(asm, &obj) {
6161 eprintln!("skipping: assemble failed: {e}");
6162 return;
6163 }
6164
6165 let opts = LinkOptions {
6166 inputs: vec![obj.clone(), tbd],
6167 output: Some(our_out.clone()),
6168 kind: OutputKind::Executable,
6169 ..LinkOptions::default()
6170 };
6171 Linker::run(&opts).unwrap();
6172 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
6173
6174 let our_bytes = fs::read(&our_out).unwrap();
6175 let apple_bytes = fs::read(&apple_out).unwrap();
6176 assert_eq!(
6177 canonical_data_in_code(&our_bytes),
6178 canonical_data_in_code(&apple_bytes)
6179 );
6180 assert_eq!(
6181 canonical_data_in_code(&our_bytes),
6182 vec![DataInCodeRecord {
6183 offset: 28,
6184 length: 8,
6185 kind: DICE_KIND_JUMP_TABLE32,
6186 }]
6187 );
6188
6189 let _ = fs::remove_file(obj);
6190 let _ = fs::remove_file(our_out);
6191 let _ = fs::remove_file(apple_out);
6192 }
6193
6194 #[test]
6195 fn linker_run_dedups_output_strtab_like_ld() {
6196 if !have_xcrun() {
6197 eprintln!("skipping: xcrun unavailable");
6198 return;
6199 }
6200 let Some(sdk) = sdk_path() else {
6201 eprintln!("skipping: xcrun --show-sdk-path unavailable");
6202 return;
6203 };
6204 let Some(sdk_ver) = sdk_version() else {
6205 eprintln!("skipping: xcrun --show-sdk-version unavailable");
6206 return;
6207 };
6208 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6209 if !tbd.exists() {
6210 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
6211 return;
6212 }
6213
6214 let obj = scratch("strtab-dedup.o");
6215 let our_out = scratch("strtab-dedup-ours.out");
6216 let apple_out = scratch("strtab-dedup-apple.out");
6217 let mut asm =
6218 String::from(" .text\n .globl _afs_array_sum\n .globl _main\n");
6219 for idx in 0..20 {
6220 let symbol = format!("_pad_symbol_{idx:02}");
6221 asm.push_str(&format!(" .globl {symbol}\n"));
6222 }
6223 asm.push_str(" .p2align 2\n");
6224 asm.push_str(" _array_sum:\n ret\n");
6225 asm.push_str(" _afs_array_sum:\n ret\n");
6226 for idx in 0..20 {
6227 let symbol = format!("_pad_symbol_{idx:02}");
6228 asm.push_str(&format!(" {symbol}:\n ret\n"));
6229 }
6230 asm.push_str(" _main:\n bl _afs_array_sum\n ret\n");
6231 asm.push_str(" .subsections_via_symbols\n");
6232 if let Err(e) = assemble(&asm, &obj) {
6233 eprintln!("skipping: assemble failed: {e}");
6234 return;
6235 }
6236
6237 let opts = LinkOptions {
6238 inputs: vec![obj.clone(), tbd],
6239 output: Some(our_out.clone()),
6240 kind: OutputKind::Executable,
6241 ..LinkOptions::default()
6242 };
6243 Linker::run(&opts).unwrap();
6244 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
6245
6246 let our_bytes = fs::read(&our_out).unwrap();
6247 let apple_bytes = fs::read(&apple_out).unwrap();
6248 assert_eq!(
6249 canonical_symbol_records(&our_bytes),
6250 canonical_symbol_records(&apple_bytes)
6251 );
6252 let our_strtab = raw_string_table(&our_bytes);
6253 let apple_strtab = raw_string_table(&apple_bytes);
6254 assert_strtab_within_five_percent(&our_strtab, &apple_strtab);
6255 assert!(
6256 our_strtab.len() <= apple_strtab.len(),
6257 "suffix dedup should not grow the output string table: ours={} apple={}",
6258 our_strtab.len(),
6259 apple_strtab.len()
6260 );
6261
6262 let offsets = symbol_name_offsets(&our_bytes);
6263 assert_eq!(offsets["_array_sum"], offsets["_afs_array_sum"] + 4);
6264
6265 let _ = fs::remove_file(obj);
6266 let _ = fs::remove_file(our_out);
6267 let _ = fs::remove_file(apple_out);
6268 }
6269
6270 #[test]
6271 fn linker_run_launches_with_classic_lazy_dylib_import() {
6272 if !have_xcrun() || !have_tool("codesign") {
6273 eprintln!("skipping: xcrun clang or codesign unavailable");
6274 return;
6275 }
6276 let Some(sdk) = sdk_path() else {
6277 eprintln!("skipping: xcrun --show-sdk-path unavailable");
6278 return;
6279 };
6280 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6281 if !tbd.exists() {
6282 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
6283 return;
6284 }
6285
6286 let dylib = scratch("lazy-runtime.dylib");
6287 let obj = scratch("lazy-runtime.o");
6288 let out = scratch("lazy-runtime.out");
6289
6290 let dylib_src = r#"
6291 int ext_fn(void) { return 7; }
6292 "#;
6293 if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
6294 eprintln!("skipping: dylib compile failed: {e}");
6295 return;
6296 }
6297
6298 let main_src = r#"
6299 int ext_fn(void);
6300 int main(void) { return ext_fn() == 7 ? 0 : 1; }
6301 "#;
6302 if let Err(e) = compile_c(main_src, &obj) {
6303 eprintln!("skipping: compile failed: {e}");
6304 return;
6305 }
6306
6307 let opts = LinkOptions {
6308 inputs: vec![obj.clone(), tbd, dylib.clone()],
6309 output: Some(out.clone()),
6310 kind: OutputKind::Executable,
6311 ..LinkOptions::default()
6312 };
6313 Linker::run(&opts).unwrap();
6314
6315 let verify = Command::new("codesign")
6316 .arg("-v")
6317 .arg(&out)
6318 .output()
6319 .unwrap();
6320 assert!(
6321 verify.status.success(),
6322 "codesign verify failed: {}",
6323 String::from_utf8_lossy(&verify.stderr)
6324 );
6325 let status = Command::new(&out).status().unwrap();
6326 assert_eq!(
6327 status.code(),
6328 Some(0),
6329 "expected dylib-import executable to exit 0"
6330 );
6331
6332 let _ = fs::remove_file(dylib);
6333 let _ = fs::remove_file(obj);
6334 let _ = fs::remove_file(out);
6335 }
6336
6337 #[test]
6338 fn linker_run_handles_local_tlv_descriptors() {
6339 if !have_xcrun() || !have_tool("codesign") {
6340 eprintln!("skipping: xcrun clang or codesign unavailable");
6341 return;
6342 }
6343 let Some(sdk) = sdk_path() else {
6344 eprintln!("skipping: xcrun --show-sdk-path unavailable");
6345 return;
6346 };
6347 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6348 if !tbd.exists() {
6349 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
6350 return;
6351 }
6352
6353 let obj = scratch("tlvp-local.o");
6354 let out = scratch("tlvp-local.out");
6355 let src = r#"
6356 __thread long tls_a = 7;
6357 __thread long tls_b;
6358
6359 static long tls_sum(void) {
6360 return tls_a + tls_b;
6361 }
6362
6363 int main(void) {
6364 return tls_sum() == 7 ? 0 : 1;
6365 }
6366 "#;
6367 if let Err(e) = compile_c(src, &obj) {
6368 eprintln!("skipping: compile failed: {e}");
6369 return;
6370 }
6371
6372 let opts = LinkOptions {
6373 inputs: vec![obj.clone(), tbd],
6374 output: Some(out.clone()),
6375 kind: OutputKind::Executable,
6376 ..LinkOptions::default()
6377 };
6378 Linker::run(&opts).unwrap();
6379
6380 let bytes = fs::read(&out).unwrap();
6381 let (_, thread_vars) = output_section(&bytes, "__DATA", "__thread_vars").unwrap();
6382 let (_, thread_data) = output_section(&bytes, "__DATA", "__thread_data").unwrap();
6383 assert!(output_section(&bytes, "__DATA", "__thread_ptrs").is_none());
6384 assert_eq!(thread_vars.len(), 48);
6385 assert_eq!(thread_data.len(), 8);
6386 assert_eq!(
6387 u64::from_le_bytes(thread_vars[16..24].try_into().unwrap()),
6388 0
6389 );
6390 assert_eq!(
6391 u64::from_le_bytes(thread_vars[40..48].try_into().unwrap()),
6392 8
6393 );
6394
6395 let binds = decode_bind_records(&bytes, false).unwrap();
6396 let mut tlv_binds: Vec<_> = binds
6397 .into_iter()
6398 .filter(|record| record.section == "__thread_vars" && record.symbol == "__tlv_bootstrap")
6399 .collect();
6400 tlv_binds.sort_by_key(|record| record.section_offset);
6401 assert_eq!(tlv_binds.len(), 2);
6402 assert_eq!(tlv_binds[0].section_offset, 0);
6403 assert_eq!(tlv_binds[1].section_offset, 24);
6404
6405 let header = parse_header(&bytes).unwrap();
6406 let commands = parse_commands(&header, &bytes).unwrap();
6407 let symtab = commands
6408 .iter()
6409 .find_map(|cmd| match cmd {
6410 LoadCommand::Symtab(cmd) => Some(*cmd),
6411 _ => None,
6412 })
6413 .unwrap();
6414 let symbols = parse_nlist_table(&bytes, symtab.symoff, symtab.nsyms).unwrap();
6415 let strings = StringTable::from_file(&bytes, symtab.stroff, symtab.strsize).unwrap();
6416 let symbol_names: Vec<&str> = symbols
6417 .iter()
6418 .map(|symbol| strings.get(symbol.strx()).unwrap())
6419 .collect();
6420 assert!(symbol_names.contains(&"__tlv_bootstrap"));
6421
6422 let verify = Command::new("codesign")
6423 .arg("-v")
6424 .arg(&out)
6425 .output()
6426 .unwrap();
6427 assert!(
6428 verify.status.success(),
6429 "codesign verify failed: {}",
6430 String::from_utf8_lossy(&verify.stderr)
6431 );
6432 let status = Command::new(&out).status().unwrap();
6433 assert_eq!(status.code(), Some(0), "expected TLV executable to exit 0");
6434
6435 let _ = fs::remove_file(obj);
6436 let _ = fs::remove_file(out);
6437 }
6438
6439 #[test]
6440 fn linker_run_routes_imported_tlv_through_got() {
6441 if !have_xcrun() || !have_tool("codesign") {
6442 eprintln!("skipping: xcrun clang or codesign unavailable");
6443 return;
6444 }
6445 let Some(sdk) = sdk_path() else {
6446 eprintln!("skipping: xcrun --show-sdk-path unavailable");
6447 return;
6448 };
6449 let Some(sdk_ver) = sdk_version() else {
6450 eprintln!("skipping: xcrun --show-sdk-version unavailable");
6451 return;
6452 };
6453 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6454 if !tbd.exists() {
6455 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
6456 return;
6457 }
6458
6459 let dylib = scratch("libtlvprobe.dylib");
6460 let obj = scratch("imported-tlv.o");
6461 let our_out = scratch("imported-tlv-ours.out");
6462 let apple_out = scratch("imported-tlv-apple.out");
6463
6464 let dylib_src = r#"
6465 __thread long ext_tls = 5;
6466 long read_lib_tls(void) { return ext_tls; }
6467 "#;
6468 if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
6469 eprintln!("skipping: dylib compile failed: {e}");
6470 return;
6471 }
6472
6473 let main_src = r#"
6474 extern __thread long ext_tls;
6475 int main(void) { return ext_tls == 5 ? 0 : 1; }
6476 "#;
6477 if let Err(e) = compile_c(main_src, &obj) {
6478 eprintln!("skipping: compile failed: {e}");
6479 return;
6480 }
6481
6482 let opts = LinkOptions {
6483 inputs: vec![obj.clone(), tbd.clone(), dylib.clone()],
6484 output: Some(our_out.clone()),
6485 kind: OutputKind::Executable,
6486 ..LinkOptions::default()
6487 };
6488 Linker::run(&opts).unwrap();
6489
6490 let apple = Command::new("xcrun")
6491 .args([
6492 "ld",
6493 "-arch",
6494 "arm64",
6495 "-platform_version",
6496 "macos",
6497 &sdk_ver,
6498 &sdk_ver,
6499 "-syslibroot",
6500 &sdk,
6501 "-no_fixup_chains",
6502 "-lSystem",
6503 "-e",
6504 "_main",
6505 "-o",
6506 ])
6507 .arg(&apple_out)
6508 .arg(&obj)
6509 .arg(&dylib)
6510 .output()
6511 .unwrap();
6512 assert!(
6513 apple.status.success(),
6514 "xcrun ld failed: {}",
6515 String::from_utf8_lossy(&apple.stderr)
6516 );
6517
6518 let our_bytes = fs::read(&our_out).unwrap();
6519 let apple_bytes = fs::read(&apple_out).unwrap();
6520 let (our_text_addr, our_text) = output_section(&our_bytes, "__TEXT", "__text").unwrap();
6521 let (apple_text_addr, apple_text) = output_section(&apple_bytes, "__TEXT", "__text").unwrap();
6522 let (our_got_addr, our_got) = output_section(&our_bytes, "__DATA_CONST", "__got").unwrap();
6523 let (apple_got_addr, apple_got) =
6524 output_section(&apple_bytes, "__DATA_CONST", "__got").unwrap();
6525
6526 assert!(output_section(&our_bytes, "__DATA", "__thread_ptrs").is_none());
6527 assert!(output_section(&apple_bytes, "__DATA", "__thread_ptrs").is_none());
6528 assert_eq!(our_got.len(), 8);
6529 assert_eq!(our_got, apple_got);
6530 assert_eq!(
6531 decode_page_reference(&our_text, our_text_addr, 20, &PageRefKind::Load).unwrap(),
6532 our_got_addr
6533 );
6534 assert_eq!(
6535 decode_page_reference(&apple_text, apple_text_addr, 20, &PageRefKind::Load).unwrap(),
6536 apple_got_addr
6537 );
6538 assert_eq!(our_text, apple_text);
6539 assert_eq!(read_insn(&our_text, 24).unwrap(), 0xf9400000);
6540 assert_eq!(read_insn(&our_text, 28).unwrap(), 0xf9400008);
6541 assert_eq!(read_insn(&our_text, 32).unwrap(), 0xd63f0100);
6542 assert_eq!(
6543 decode_bind_records(&our_bytes, false).unwrap(),
6544 decode_bind_records(&apple_bytes, false).unwrap()
6545 );
6546 assert_eq!(
6547 load_dylib_names(&our_bytes).unwrap(),
6548 load_dylib_names(&apple_bytes).unwrap()
6549 );
6550 let verify = Command::new("codesign")
6551 .arg("-v")
6552 .arg(&our_out)
6553 .output()
6554 .unwrap();
6555 assert!(
6556 verify.status.success(),
6557 "codesign verify failed: {}",
6558 String::from_utf8_lossy(&verify.stderr)
6559 );
6560 let status = Command::new(&our_out).status().unwrap();
6561 assert_eq!(
6562 status.code(),
6563 Some(0),
6564 "expected imported TLV executable to exit 0"
6565 );
6566
6567 let _ = fs::remove_file(dylib);
6568 let _ = fs::remove_file(obj);
6569 let _ = fs::remove_file(our_out);
6570 let _ = fs::remove_file(apple_out);
6571 }
6572
6573 #[test]
6574 fn linker_run_preserves_runtime_tlv_descriptor_offsets() {
6575 if !have_xcrun() || !have_tool("codesign") {
6576 eprintln!("skipping: xcrun or codesign unavailable");
6577 return;
6578 }
6579 let Some(runtime) = find_runtime_archive() else {
6580 eprintln!("skipping: libarmfortas_rt.a not built");
6581 return;
6582 };
6583 let Some(sdk) = sdk_path() else {
6584 eprintln!("skipping: no macOS SDK path");
6585 return;
6586 };
6587 let Some(sdk_ver) = sdk_version() else {
6588 eprintln!("skipping: no macOS SDK version");
6589 return;
6590 };
6591 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6592 if !tbd.exists() {
6593 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
6594 return;
6595 }
6596
6597 let obj = scratch("runtime-hello.o");
6598 let out = scratch("runtime-hello.out");
6599 let apple_out = scratch("runtime-hello-apple.out");
6600 let src = r#"
6601 extern void afs_program_init(void);
6602 extern void afs_program_finalize(void);
6603 extern void afs_write_string(int, const char *, long);
6604 extern void afs_write_newline(int);
6605
6606 int main(void) {
6607 afs_program_init();
6608 afs_write_string(6, "Hello, World!", 13);
6609 afs_write_newline(6);
6610 afs_program_finalize();
6611 return 0;
6612 }
6613 "#;
6614 if let Err(e) = compile_c(src, &obj) {
6615 eprintln!("skipping: compile failed: {e}");
6616 return;
6617 }
6618
6619 let opts = LinkOptions {
6620 inputs: vec![obj.clone(), runtime.clone(), tbd],
6621 output: Some(out.clone()),
6622 kind: OutputKind::Executable,
6623 ..LinkOptions::default()
6624 };
6625 Linker::run(&opts).unwrap();
6626
6627 let apple = Command::new("xcrun")
6628 .args([
6629 "ld",
6630 "-arch",
6631 "arm64",
6632 "-platform_version",
6633 "macos",
6634 &sdk_ver,
6635 &sdk_ver,
6636 "-syslibroot",
6637 &sdk,
6638 "-lSystem",
6639 "-e",
6640 "_main",
6641 "-no_fixup_chains",
6642 "-o",
6643 ])
6644 .arg(&apple_out)
6645 .arg(&obj)
6646 .arg(&runtime)
6647 .output()
6648 .unwrap();
6649 assert!(
6650 apple.status.success(),
6651 "xcrun ld failed: {}",
6652 String::from_utf8_lossy(&apple.stderr)
6653 );
6654
6655 let verify = Command::new("codesign")
6656 .arg("-v")
6657 .arg(&out)
6658 .output()
6659 .unwrap();
6660 assert!(
6661 verify.status.success(),
6662 "codesign verify failed: {}",
6663 String::from_utf8_lossy(&verify.stderr)
6664 );
6665
6666 let bytes = fs::read(&out).unwrap();
6667 let apple_bytes = fs::read(&apple_out).unwrap();
6668 assert!(
6669 output_section(&bytes, "__DATA_CONST", "__const").is_some(),
6670 "runtime hello should promote file-backed __const data into __DATA_CONST"
6671 );
6672 assert!(
6673 output_section(&bytes, "__DATA", "__const").is_none(),
6674 "runtime hello should not leave file-backed __const data in __DATA"
6675 );
6676 let (thread_vars_addr, thread_vars) =
6677 output_section(&bytes, "__DATA", "__thread_vars").unwrap();
6678 let (thread_data_addr, _) = output_section(&bytes, "__DATA", "__thread_data").unwrap();
6679 let symbols = symbol_values(&bytes);
6680 let tlv_binds: Vec<_> = decode_bind_records(&bytes, false)
6681 .unwrap()
6682 .into_iter()
6683 .filter(|record| record.section == "__thread_vars")
6684 .collect();
6685 assert_eq!(
6686 tlv_binds.len(),
6687 thread_vars.len() / 24,
6688 "every TLV descriptor should carry exactly one bootstrap bind"
6689 );
6690 assert!(tlv_binds
6691 .iter()
6692 .all(|record| record.symbol == "__tlv_bootstrap"));
6693
6694 assert_eq!(
6695 decode_bind_records(&bytes, false).unwrap(),
6696 decode_bind_records(&apple_bytes, false).unwrap(),
6697 "runtime hello bind records diverged from Apple ld"
6698 );
6699 assert_eq!(
6700 decode_bind_records(&bytes, true).unwrap(),
6701 decode_bind_records(&apple_bytes, true).unwrap(),
6702 "runtime hello lazy-bind records diverged from Apple ld"
6703 );
6704 assert_eq!(
6705 decode_rebase_records(&bytes).unwrap(),
6706 decode_rebase_records(&apple_bytes).unwrap(),
6707 "runtime hello rebase records diverged from Apple ld"
6708 );
6709 assert_eq!(
6710 indirect_symbol_identities(&bytes),
6711 indirect_symbol_identities(&apple_bytes),
6712 "runtime hello indirect symbol identities diverged from Apple ld"
6713 );
6714
6715 for (name, descriptor_addr) in symbols.iter().filter(|(name, value)| {
6716 !name.ends_with("$tlv$init")
6717 && **value >= thread_vars_addr
6718 && **value < thread_vars_addr + thread_vars.len() as u64
6719 }) {
6720 let init_name = format!("{name}$tlv$init");
6721 let Some(init_addr) = symbols.get(&init_name) else {
6722 continue;
6723 };
6724 let offset = (*descriptor_addr - thread_vars_addr) as usize;
6725 let actual = u64::from_le_bytes(thread_vars[offset + 16..offset + 24].try_into().unwrap());
6726 let expected = init_addr - thread_data_addr;
6727 assert_eq!(
6728 actual, expected,
6729 "TLV descriptor {} should point at {} via template offset",
6730 name, init_name
6731 );
6732 }
6733
6734 let output = Command::new(&out).output().unwrap();
6735 let apple_output = Command::new(&apple_out).output().unwrap();
6736 assert_eq!(
6737 output.status.code(),
6738 Some(0),
6739 "expected runtime hello executable to exit 0, stderr={}",
6740 String::from_utf8_lossy(&output.stderr)
6741 );
6742 assert_eq!(
6743 apple_output.status.code(),
6744 Some(0),
6745 "expected Apple-linked runtime hello executable to exit 0, stderr={}",
6746 String::from_utf8_lossy(&apple_output.stderr)
6747 );
6748 assert_eq!(
6749 String::from_utf8_lossy(&output.stdout),
6750 String::from_utf8_lossy(&apple_output.stdout)
6751 );
6752
6753 let _ = fs::remove_file(obj);
6754 let _ = fs::remove_file(out);
6755 let _ = fs::remove_file(apple_out);
6756 }
6757
6758 #[test]
6759 fn linker_run_rebases_runtime_init_metadata_like_apple_ld() {
6760 if !have_xcrun() || !have_tool("codesign") {
6761 eprintln!("skipping: xcrun or codesign unavailable");
6762 return;
6763 }
6764 let Some(runtime) = find_runtime_archive() else {
6765 eprintln!("skipping: libarmfortas_rt.a not built");
6766 return;
6767 };
6768 let Some(sdk) = sdk_path() else {
6769 eprintln!("skipping: no macOS SDK path");
6770 return;
6771 };
6772 let Some(sdk_ver) = sdk_version() else {
6773 eprintln!("skipping: no macOS SDK version");
6774 return;
6775 };
6776 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
6777 if !tbd.exists() {
6778 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
6779 return;
6780 }
6781
6782 let obj = scratch("runtime-init-only.o");
6783 let our_out = scratch("runtime-init-only-ours.out");
6784 let apple_out = scratch("runtime-init-only-apple.out");
6785 let src = r#"
6786 extern void afs_program_init(void);
6787
6788 int main(void) {
6789 afs_program_init();
6790 return 0;
6791 }
6792 "#;
6793 if let Err(e) = compile_c(src, &obj) {
6794 eprintln!("skipping: compile failed: {e}");
6795 return;
6796 }
6797
6798 let opts = LinkOptions {
6799 inputs: vec![obj.clone(), runtime.clone(), tbd],
6800 output: Some(our_out.clone()),
6801 kind: OutputKind::Executable,
6802 ..LinkOptions::default()
6803 };
6804 Linker::run(&opts).unwrap();
6805
6806 let apple = Command::new("xcrun")
6807 .args([
6808 "ld",
6809 "-arch",
6810 "arm64",
6811 "-platform_version",
6812 "macos",
6813 &sdk_ver,
6814 &sdk_ver,
6815 "-syslibroot",
6816 &sdk,
6817 "-lSystem",
6818 "-e",
6819 "_main",
6820 "-no_fixup_chains",
6821 "-o",
6822 ])
6823 .arg(&apple_out)
6824 .arg(&obj)
6825 .arg(&runtime)
6826 .output()
6827 .unwrap();
6828 assert!(
6829 apple.status.success(),
6830 "xcrun ld failed: {}",
6831 String::from_utf8_lossy(&apple.stderr)
6832 );
6833
6834 let our_bytes = fs::read(&our_out).unwrap();
6835 let apple_bytes = fs::read(&apple_out).unwrap();
6836 let our_rebases = decode_rebase_records(&our_bytes).unwrap();
6837 let apple_rebases = decode_rebase_records(&apple_bytes).unwrap();
6838 assert_eq!(
6839 our_rebases
6840 .iter()
6841 .filter(|record| record.section == "__const")
6842 .count(),
6843 apple_rebases
6844 .iter()
6845 .filter(|record| record.section == "__const")
6846 .count(),
6847 "runtime init const rebases diverged from Apple ld"
6848 );
6849 assert_eq!(
6850 our_rebases
6851 .iter()
6852 .filter(|record| record.section == "__la_symbol_ptr")
6853 .count(),
6854 apple_rebases
6855 .iter()
6856 .filter(|record| record.section == "__la_symbol_ptr")
6857 .count(),
6858 "runtime init lazy-pointer rebases diverged from Apple ld"
6859 );
6860
6861 let our_status = Command::new(&our_out).status().unwrap();
6862 let apple_status = Command::new(&apple_out).status().unwrap();
6863 assert_eq!(our_status.code(), Some(0));
6864 assert_eq!(apple_status.code(), Some(0));
6865
6866 let _ = fs::remove_file(obj);
6867 let _ = fs::remove_file(our_out);
6868 let _ = fs::remove_file(apple_out);
6869 }
6870
6871 #[test]
6872 fn linker_run_icf_safe_folds_identical_private_text() {
6873 if !have_xcrun() {
6874 eprintln!("skipping: xcrun unavailable");
6875 return;
6876 };
6877
6878 let obj = scratch("icf-fold.o");
6879 let baseline_out = scratch("icf-fold-baseline.out");
6880 let our_out = scratch("icf-fold-ours.out");
6881 let src = r#"
6882 .section __TEXT,__text,regular,pure_instructions
6883 .globl _main
6884 _main:
6885 stp x29, x30, [sp, #-16]!
6886 mov x29, sp
6887 bl _helper1
6888 bl _helper2
6889 ldp x29, x30, [sp], #16
6890 ret
6891
6892 .private_extern _helper1
6893 _helper1:
6894 mov w0, #7
6895 ret
6896
6897 .private_extern _helper2
6898 _helper2:
6899 mov w0, #7
6900 ret
6901 .subsections_via_symbols
6902 "#;
6903 if let Err(e) = assemble(src, &obj) {
6904 eprintln!("skipping: assemble failed: {e}");
6905 return;
6906 }
6907
6908 let baseline_opts = LinkOptions {
6909 inputs: vec![obj.clone()],
6910 output: Some(baseline_out.clone()),
6911 kind: OutputKind::Executable,
6912 ..LinkOptions::default()
6913 };
6914 Linker::run(&baseline_opts).unwrap();
6915
6916 let opts = LinkOptions {
6917 inputs: vec![obj.clone()],
6918 output: Some(our_out.clone()),
6919 kind: OutputKind::Executable,
6920 icf_mode: afs_ld::IcfMode::Safe,
6921 ..LinkOptions::default()
6922 };
6923 Linker::run(&opts).unwrap();
6924
6925 let baseline_bytes = fs::read(&baseline_out).unwrap();
6926 let our_bytes = fs::read(&our_out).unwrap();
6927 let baseline_symbols = symbol_values(&baseline_bytes);
6928 let our_symbols = symbol_values(&our_bytes);
6929 let baseline_text = output_section(&baseline_bytes, "__TEXT", "__text")
6930 .unwrap()
6931 .1;
6932 let our_text = output_section(&our_bytes, "__TEXT", "__text").unwrap().1;
6933
6934 assert_eq!(
6935 our_symbols.get("_helper1"),
6936 our_symbols.get("_helper2"),
6937 "expected afs-ld -icf=safe to coalesce identical private text atoms"
6938 );
6939 assert_ne!(
6940 baseline_symbols.get("_helper1"),
6941 baseline_symbols.get("_helper2"),
6942 "expected baseline link to keep identical helpers separate"
6943 );
6944 assert_eq!(
6945 Command::new(&our_out).status().unwrap().code(),
6946 Some(7),
6947 "folded executable should preserve runtime behavior"
6948 );
6949 assert!(
6950 our_text.len() < baseline_text.len(),
6951 "expected -icf=safe to reduce text size on identical helpers"
6952 );
6953
6954 let _ = fs::remove_file(obj);
6955 let _ = fs::remove_file(baseline_out);
6956 let _ = fs::remove_file(our_out);
6957 }
6958
6959 #[test]
6960 fn linker_run_icf_safe_keeps_address_taken_functions_distinct() {
6961 if !have_xcrun() {
6962 eprintln!("skipping: xcrun unavailable");
6963 return;
6964 };
6965
6966 let obj = scratch("icf-address-taken.o");
6967 let baseline_out = scratch("icf-address-taken-baseline.out");
6968 let our_out = scratch("icf-address-taken-ours.out");
6969 let src = r#"
6970 .section __TEXT,__text,regular,pure_instructions
6971 .globl _main
6972 _main:
6973 stp x29, x30, [sp, #-16]!
6974 mov x29, sp
6975 bl _helper1
6976 bl _helper2
6977 ldp x29, x30, [sp], #16
6978 ret
6979
6980 .private_extern _helper1
6981 _helper1:
6982 mov w0, #7
6983 ret
6984
6985 .private_extern _helper2
6986 _helper2:
6987 mov w0, #7
6988 ret
6989
6990 .section __DATA,__const
6991 .p2align 3
6992 _ptrs:
6993 .quad _helper1
6994 .quad _helper2
6995 .subsections_via_symbols
6996 "#;
6997 if let Err(e) = assemble(src, &obj) {
6998 eprintln!("skipping: assemble failed: {e}");
6999 return;
7000 }
7001
7002 let baseline_opts = LinkOptions {
7003 inputs: vec![obj.clone()],
7004 output: Some(baseline_out.clone()),
7005 kind: OutputKind::Executable,
7006 ..LinkOptions::default()
7007 };
7008 Linker::run(&baseline_opts).unwrap();
7009
7010 let opts = LinkOptions {
7011 inputs: vec![obj.clone()],
7012 output: Some(our_out.clone()),
7013 kind: OutputKind::Executable,
7014 icf_mode: afs_ld::IcfMode::Safe,
7015 ..LinkOptions::default()
7016 };
7017 Linker::run(&opts).unwrap();
7018
7019 let baseline_bytes = fs::read(&baseline_out).unwrap();
7020 let our_bytes = fs::read(&our_out).unwrap();
7021 let baseline_symbols = symbol_values(&baseline_bytes);
7022 let our_symbols = symbol_values(&our_bytes);
7023 let baseline_text = output_section(&baseline_bytes, "__TEXT", "__text")
7024 .unwrap()
7025 .1;
7026 let our_text = output_section(&our_bytes, "__TEXT", "__text").unwrap().1;
7027
7028 assert_ne!(
7029 our_symbols.get("_helper1"),
7030 our_symbols.get("_helper2"),
7031 "address-taken helpers should not be folded by afs-ld -icf=safe"
7032 );
7033 assert_ne!(
7034 baseline_symbols.get("_helper1"),
7035 baseline_symbols.get("_helper2"),
7036 "baseline link should keep address-taken helpers separate"
7037 );
7038 assert_eq!(
7039 Command::new(&our_out).status().unwrap().code(),
7040 Some(7),
7041 "address-taken executable should preserve runtime behavior"
7042 );
7043 assert_eq!(
7044 our_text.len(),
7045 baseline_text.len(),
7046 "address-taken helpers should not shrink under -icf=safe"
7047 );
7048
7049 let _ = fs::remove_file(obj);
7050 let _ = fs::remove_file(baseline_out);
7051 let _ = fs::remove_file(our_out);
7052 }
7053
7054 #[test]
7055 fn linker_run_icf_safe_keeps_adrp_add_address_taken_functions_distinct() {
7056 if !have_xcrun() {
7057 eprintln!("skipping: xcrun unavailable");
7058 return;
7059 };
7060
7061 let obj = scratch("icf-adrp-address-taken.o");
7062 let baseline_out = scratch("icf-adrp-address-taken-baseline.out");
7063 let our_out = scratch("icf-adrp-address-taken-ours.out");
7064 let src = r#"
7065 .section __TEXT,__text,regular,pure_instructions
7066 .globl _main
7067 _main:
7068 stp x29, x30, [sp, #-16]!
7069 mov x29, sp
7070 adrp x10, _helper1@PAGE
7071 add x10, x10, _helper1@PAGEOFF
7072 adrp x11, _helper2@PAGE
7073 add x11, x11, _helper2@PAGEOFF
7074 cmp x10, x11
7075 b.ne 1f
7076 mov w0, #1
7077 ldp x29, x30, [sp], #16
7078 ret
7079 1:
7080 mov w0, #0
7081 ldp x29, x30, [sp], #16
7082 ret
7083
7084 .private_extern _helper1
7085 _helper1:
7086 mov w0, #7
7087 ret
7088
7089 .private_extern _helper2
7090 _helper2:
7091 mov w0, #7
7092 ret
7093 .subsections_via_symbols
7094 "#;
7095 if let Err(e) = assemble(src, &obj) {
7096 eprintln!("skipping: assemble failed: {e}");
7097 return;
7098 }
7099
7100 let baseline_opts = LinkOptions {
7101 inputs: vec![obj.clone()],
7102 output: Some(baseline_out.clone()),
7103 kind: OutputKind::Executable,
7104 ..LinkOptions::default()
7105 };
7106 Linker::run(&baseline_opts).unwrap();
7107
7108 let opts = LinkOptions {
7109 inputs: vec![obj.clone()],
7110 output: Some(our_out.clone()),
7111 kind: OutputKind::Executable,
7112 icf_mode: afs_ld::IcfMode::Safe,
7113 ..LinkOptions::default()
7114 };
7115 Linker::run(&opts).unwrap();
7116
7117 let baseline_bytes = fs::read(&baseline_out).unwrap();
7118 let our_bytes = fs::read(&our_out).unwrap();
7119 let baseline_symbols = symbol_values(&baseline_bytes);
7120 let our_symbols = symbol_values(&our_bytes);
7121 let baseline_text = output_section(&baseline_bytes, "__TEXT", "__text")
7122 .unwrap()
7123 .1;
7124 let our_text = output_section(&our_bytes, "__TEXT", "__text").unwrap().1;
7125
7126 assert_ne!(
7127 our_symbols.get("_helper1"),
7128 our_symbols.get("_helper2"),
7129 "adrp/add address-taken helpers should not be folded by afs-ld -icf=safe"
7130 );
7131 assert_ne!(
7132 baseline_symbols.get("_helper1"),
7133 baseline_symbols.get("_helper2"),
7134 "baseline link should keep adrp/add address-taken helpers separate"
7135 );
7136 assert_eq!(
7137 Command::new(&our_out).status().unwrap().code(),
7138 Some(0),
7139 "adrp/add address-taken executable should preserve pointer inequality"
7140 );
7141 assert_eq!(
7142 our_text.len(),
7143 baseline_text.len(),
7144 "adrp/add address-taken helpers should not shrink under -icf=safe"
7145 );
7146
7147 let _ = fs::remove_file(obj);
7148 let _ = fs::remove_file(baseline_out);
7149 let _ = fs::remove_file(our_out);
7150 }
7151
7152 #[test]
7153 fn linker_run_icf_safe_folds_matching_branch_relocs() {
7154 if !have_xcrun() {
7155 eprintln!("skipping: xcrun unavailable");
7156 return;
7157 };
7158
7159 let obj = scratch("icf-branch-match.o");
7160 let baseline_out = scratch("icf-branch-match-baseline.out");
7161 let our_out = scratch("icf-branch-match-ours.out");
7162 let src = r#"
7163 .section __TEXT,__text,regular,pure_instructions
7164 .globl _main
7165 _main:
7166 stp x29, x30, [sp, #-32]!
7167 mov x29, sp
7168 bl _wrapper1
7169 str w0, [sp, #16]
7170 bl _wrapper2
7171 ldr w8, [sp, #16]
7172 add w0, w8, w0
7173 ldp x29, x30, [sp], #32
7174 ret
7175
7176 .private_extern _wrapper1
7177 _wrapper1:
7178 b _leaf
7179
7180 .private_extern _wrapper2
7181 _wrapper2:
7182 b _leaf
7183
7184 .private_extern _leaf
7185 _leaf:
7186 mov w0, #5
7187 ret
7188 .subsections_via_symbols
7189 "#;
7190 if let Err(e) = assemble(src, &obj) {
7191 eprintln!("skipping: assemble failed: {e}");
7192 return;
7193 }
7194
7195 let baseline_opts = LinkOptions {
7196 inputs: vec![obj.clone()],
7197 output: Some(baseline_out.clone()),
7198 kind: OutputKind::Executable,
7199 ..LinkOptions::default()
7200 };
7201 Linker::run(&baseline_opts).unwrap();
7202
7203 let opts = LinkOptions {
7204 inputs: vec![obj.clone()],
7205 output: Some(our_out.clone()),
7206 kind: OutputKind::Executable,
7207 icf_mode: afs_ld::IcfMode::Safe,
7208 ..LinkOptions::default()
7209 };
7210 Linker::run(&opts).unwrap();
7211
7212 let baseline_bytes = fs::read(&baseline_out).unwrap();
7213 let our_bytes = fs::read(&our_out).unwrap();
7214 let baseline_symbols = symbol_values(&baseline_bytes);
7215 let our_symbols = symbol_values(&our_bytes);
7216 let baseline_text = output_section(&baseline_bytes, "__TEXT", "__text")
7217 .unwrap()
7218 .1;
7219 let our_text = output_section(&our_bytes, "__TEXT", "__text").unwrap().1;
7220
7221 assert_ne!(
7222 baseline_symbols.get("_wrapper1"),
7223 baseline_symbols.get("_wrapper2"),
7224 "baseline link should keep identical wrappers separate"
7225 );
7226 assert_eq!(
7227 our_symbols.get("_wrapper1"),
7228 our_symbols.get("_wrapper2"),
7229 "matching branch relocations should fold under -icf=safe"
7230 );
7231 assert_eq!(
7232 Command::new(&our_out).status().unwrap().code(),
7233 Some(10),
7234 "folded branch-reloc executable should preserve runtime behavior"
7235 );
7236 assert!(
7237 our_text.len() < baseline_text.len(),
7238 "expected matching branch-reloc wrappers to shrink under -icf=safe"
7239 );
7240
7241 let _ = fs::remove_file(obj);
7242 let _ = fs::remove_file(baseline_out);
7243 let _ = fs::remove_file(our_out);
7244 }
7245
7246 #[test]
7247 fn linker_run_icf_safe_keeps_distinct_branch_targets_unfolded() {
7248 if !have_xcrun() {
7249 eprintln!("skipping: xcrun unavailable");
7250 return;
7251 };
7252
7253 let obj = scratch("icf-branch-distinct.o");
7254 let baseline_out = scratch("icf-branch-distinct-baseline.out");
7255 let our_out = scratch("icf-branch-distinct-ours.out");
7256 let src = r#"
7257 .section __TEXT,__text,regular,pure_instructions
7258 .globl _main
7259 _main:
7260 stp x29, x30, [sp, #-32]!
7261 mov x29, sp
7262 bl _wrapper1
7263 str w0, [sp, #16]
7264 bl _wrapper2
7265 ldr w8, [sp, #16]
7266 add w0, w8, w0
7267 ldp x29, x30, [sp], #32
7268 ret
7269
7270 .private_extern _wrapper1
7271 _wrapper1:
7272 b _leaf1
7273
7274 .private_extern _wrapper2
7275 _wrapper2:
7276 b _leaf2
7277
7278 .private_extern _leaf1
7279 _leaf1:
7280 mov w0, #3
7281 ret
7282
7283 .private_extern _leaf2
7284 _leaf2:
7285 mov w0, #5
7286 ret
7287 .subsections_via_symbols
7288 "#;
7289 if let Err(e) = assemble(src, &obj) {
7290 eprintln!("skipping: assemble failed: {e}");
7291 return;
7292 }
7293
7294 let baseline_opts = LinkOptions {
7295 inputs: vec![obj.clone()],
7296 output: Some(baseline_out.clone()),
7297 kind: OutputKind::Executable,
7298 ..LinkOptions::default()
7299 };
7300 Linker::run(&baseline_opts).unwrap();
7301
7302 let opts = LinkOptions {
7303 inputs: vec![obj.clone()],
7304 output: Some(our_out.clone()),
7305 kind: OutputKind::Executable,
7306 icf_mode: afs_ld::IcfMode::Safe,
7307 ..LinkOptions::default()
7308 };
7309 Linker::run(&opts).unwrap();
7310
7311 let baseline_bytes = fs::read(&baseline_out).unwrap();
7312 let our_bytes = fs::read(&our_out).unwrap();
7313 let baseline_symbols = symbol_values(&baseline_bytes);
7314 let our_symbols = symbol_values(&our_bytes);
7315 let baseline_text = output_section(&baseline_bytes, "__TEXT", "__text")
7316 .unwrap()
7317 .1;
7318 let our_text = output_section(&our_bytes, "__TEXT", "__text").unwrap().1;
7319
7320 assert_ne!(
7321 baseline_symbols.get("_wrapper1"),
7322 baseline_symbols.get("_wrapper2"),
7323 "baseline link should keep distinct wrappers separate"
7324 );
7325 assert_ne!(
7326 our_symbols.get("_wrapper1"),
7327 our_symbols.get("_wrapper2"),
7328 "wrappers targeting different leaves must not fold under -icf=safe"
7329 );
7330 assert_eq!(
7331 Command::new(&our_out).status().unwrap().code(),
7332 Some(8),
7333 "distinct branch-target executable should preserve runtime behavior"
7334 );
7335 assert_eq!(
7336 our_text.len(),
7337 baseline_text.len(),
7338 "distinct branch-target wrappers should not shrink under -icf=safe"
7339 );
7340
7341 let _ = fs::remove_file(obj);
7342 let _ = fs::remove_file(baseline_out);
7343 let _ = fs::remove_file(our_out);
7344 }
7345
7346 #[test]
7347 fn linker_run_icf_safe_folds_identical_private_const_data() {
7348 if !have_xcrun() {
7349 eprintln!("skipping: xcrun unavailable");
7350 return;
7351 };
7352
7353 let obj = scratch("icf-const-fold.o");
7354 let baseline_out = scratch("icf-const-fold-baseline.out");
7355 let our_out = scratch("icf-const-fold-ours.out");
7356 let src = r#"
7357 .section __TEXT,__text,regular,pure_instructions
7358 .globl _main
7359 _main:
7360 mov w0, #0
7361 ret
7362
7363 .section __TEXT,__const
7364 .p2align 3
7365 .private_extern _const1
7366 _const1:
7367 .quad 0x1122334455667788
7368 .p2align 3
7369 .private_extern _const2
7370 _const2:
7371 .quad 0x1122334455667788
7372 .subsections_via_symbols
7373 "#;
7374 if let Err(e) = assemble(src, &obj) {
7375 eprintln!("skipping: assemble failed: {e}");
7376 return;
7377 }
7378
7379 let baseline_opts = LinkOptions {
7380 inputs: vec![obj.clone()],
7381 output: Some(baseline_out.clone()),
7382 kind: OutputKind::Executable,
7383 ..LinkOptions::default()
7384 };
7385 Linker::run(&baseline_opts).unwrap();
7386
7387 let opts = LinkOptions {
7388 inputs: vec![obj.clone()],
7389 output: Some(our_out.clone()),
7390 kind: OutputKind::Executable,
7391 icf_mode: afs_ld::IcfMode::Safe,
7392 ..LinkOptions::default()
7393 };
7394 Linker::run(&opts).unwrap();
7395
7396 let baseline_bytes = fs::read(&baseline_out).unwrap();
7397 let our_bytes = fs::read(&our_out).unwrap();
7398 let baseline_symbols = symbol_values(&baseline_bytes);
7399 let our_symbols = symbol_values(&our_bytes);
7400 let baseline_const = output_section(&baseline_bytes, "__TEXT", "__const")
7401 .unwrap()
7402 .1;
7403 let our_const = output_section(&our_bytes, "__TEXT", "__const").unwrap().1;
7404
7405 assert_ne!(
7406 baseline_symbols.get("_const1"),
7407 baseline_symbols.get("_const2"),
7408 "baseline link should keep identical private const atoms separate"
7409 );
7410 assert_eq!(
7411 our_symbols.get("_const1"),
7412 our_symbols.get("_const2"),
7413 "expected afs-ld -icf=safe to coalesce identical private const atoms"
7414 );
7415 assert!(
7416 our_const.len() < baseline_const.len(),
7417 "expected -icf=safe to reduce const section size on identical atoms"
7418 );
7419
7420 let _ = fs::remove_file(obj);
7421 let _ = fs::remove_file(baseline_out);
7422 let _ = fs::remove_file(our_out);
7423 }
7424
7425 #[test]
7426 fn linker_run_icf_safe_folds_identical_private_cstrings() {
7427 if !have_xcrun() {
7428 eprintln!("skipping: xcrun unavailable");
7429 return;
7430 };
7431
7432 let obj = scratch("icf-cstring-fold.o");
7433 let baseline_out = scratch("icf-cstring-fold-baseline.out");
7434 let our_out = scratch("icf-cstring-fold-ours.out");
7435 let src = r#"
7436 .section __TEXT,__text,regular,pure_instructions
7437 .globl _main
7438 _main:
7439 mov w0, #0
7440 ret
7441
7442 .section __TEXT,__cstring,cstring_literals
7443 .private_extern _str1
7444 _str1:
7445 .asciz "fold me"
7446 .private_extern _str2
7447 _str2:
7448 .asciz "fold me"
7449 .subsections_via_symbols
7450 "#;
7451 if let Err(e) = assemble(src, &obj) {
7452 eprintln!("skipping: assemble failed: {e}");
7453 return;
7454 }
7455
7456 let baseline_opts = LinkOptions {
7457 inputs: vec![obj.clone()],
7458 output: Some(baseline_out.clone()),
7459 kind: OutputKind::Executable,
7460 ..LinkOptions::default()
7461 };
7462 Linker::run(&baseline_opts).unwrap();
7463
7464 let opts = LinkOptions {
7465 inputs: vec![obj.clone()],
7466 output: Some(our_out.clone()),
7467 kind: OutputKind::Executable,
7468 icf_mode: afs_ld::IcfMode::Safe,
7469 ..LinkOptions::default()
7470 };
7471 Linker::run(&opts).unwrap();
7472
7473 let baseline_bytes = fs::read(&baseline_out).unwrap();
7474 let our_bytes = fs::read(&our_out).unwrap();
7475 let baseline_symbols = symbol_values(&baseline_bytes);
7476 let our_symbols = symbol_values(&our_bytes);
7477 let baseline_cstrings = output_section(&baseline_bytes, "__TEXT", "__cstring")
7478 .unwrap()
7479 .1;
7480 let our_cstrings = output_section(&our_bytes, "__TEXT", "__cstring").unwrap().1;
7481
7482 assert_ne!(
7483 baseline_symbols.get("_str1"),
7484 baseline_symbols.get("_str2"),
7485 "baseline link should keep identical private cstrings separate"
7486 );
7487 assert_eq!(
7488 our_symbols.get("_str1"),
7489 our_symbols.get("_str2"),
7490 "expected afs-ld -icf=safe to coalesce identical private cstrings"
7491 );
7492 assert!(
7493 our_cstrings.len() < baseline_cstrings.len(),
7494 "expected -icf=safe to reduce cstring section size on identical literals"
7495 );
7496
7497 let _ = fs::remove_file(obj);
7498 let _ = fs::remove_file(baseline_out);
7499 let _ = fs::remove_file(our_out);
7500 }
7501
7502 #[test]
7503 fn linker_run_icf_safe_folds_identical_private_literal16() {
7504 if !have_xcrun() {
7505 eprintln!("skipping: xcrun unavailable");
7506 return;
7507 };
7508
7509 let obj = scratch("icf-literal16-fold.o");
7510 let baseline_out = scratch("icf-literal16-fold-baseline.out");
7511 let our_out = scratch("icf-literal16-fold-ours.out");
7512 let src = r#"
7513 .section __TEXT,__text,regular,pure_instructions
7514 .globl _main
7515 _main:
7516 mov w0, #0
7517 ret
7518
7519 .section __TEXT,__literal16,16byte_literals
7520 .private_extern _lit1
7521 _lit1:
7522 .quad 0x1122334455667788
7523 .quad 0x99aabbccddeeff00
7524 .private_extern _lit2
7525 _lit2:
7526 .quad 0x1122334455667788
7527 .quad 0x99aabbccddeeff00
7528 .subsections_via_symbols
7529 "#;
7530 if let Err(e) = assemble(src, &obj) {
7531 eprintln!("skipping: assemble failed: {e}");
7532 return;
7533 }
7534
7535 let baseline_opts = LinkOptions {
7536 inputs: vec![obj.clone()],
7537 output: Some(baseline_out.clone()),
7538 kind: OutputKind::Executable,
7539 ..LinkOptions::default()
7540 };
7541 Linker::run(&baseline_opts).unwrap();
7542
7543 let opts = LinkOptions {
7544 inputs: vec![obj.clone()],
7545 output: Some(our_out.clone()),
7546 kind: OutputKind::Executable,
7547 icf_mode: afs_ld::IcfMode::Safe,
7548 ..LinkOptions::default()
7549 };
7550 Linker::run(&opts).unwrap();
7551
7552 let baseline_bytes = fs::read(&baseline_out).unwrap();
7553 let our_bytes = fs::read(&our_out).unwrap();
7554 let baseline_symbols = symbol_values(&baseline_bytes);
7555 let our_symbols = symbol_values(&our_bytes);
7556 let baseline_literals = output_section(&baseline_bytes, "__TEXT", "__literal16")
7557 .unwrap()
7558 .1;
7559 let our_literals = output_section(&our_bytes, "__TEXT", "__literal16").unwrap().1;
7560
7561 assert_ne!(
7562 baseline_symbols.get("_lit1"),
7563 baseline_symbols.get("_lit2"),
7564 "baseline link should keep identical private literal16 atoms separate"
7565 );
7566 assert_eq!(
7567 our_symbols.get("_lit1"),
7568 our_symbols.get("_lit2"),
7569 "expected afs-ld -icf=safe to coalesce identical private literal16 atoms"
7570 );
7571 assert!(
7572 our_literals.len() < baseline_literals.len(),
7573 "expected -icf=safe to reduce literal16 section size on identical atoms"
7574 );
7575
7576 let _ = fs::remove_file(obj);
7577 let _ = fs::remove_file(baseline_out);
7578 let _ = fs::remove_file(our_out);
7579 }
7580
7581 #[test]
7582 fn linker_run_icf_safe_folds_identical_private_data_const_atoms() {
7583 if !have_xcrun() {
7584 eprintln!("skipping: xcrun unavailable");
7585 return;
7586 };
7587
7588 let obj = scratch("icf-data-const-fold.o");
7589 let baseline_out = scratch("icf-data-const-fold-baseline.out");
7590 let our_out = scratch("icf-data-const-fold-ours.out");
7591 let src = r#"
7592 .section __TEXT,__text,regular,pure_instructions
7593 .globl _main
7594 _main:
7595 mov w0, #0
7596 ret
7597
7598 .section __DATA_CONST,__const
7599 .p2align 3
7600 .private_extern _const1
7601 _const1:
7602 .quad 0x0123456789abcdef
7603 .p2align 3
7604 .private_extern _const2
7605 _const2:
7606 .quad 0x0123456789abcdef
7607 .subsections_via_symbols
7608 "#;
7609 if let Err(e) = assemble(src, &obj) {
7610 eprintln!("skipping: assemble failed: {e}");
7611 return;
7612 }
7613
7614 let baseline_opts = LinkOptions {
7615 inputs: vec![obj.clone()],
7616 output: Some(baseline_out.clone()),
7617 kind: OutputKind::Executable,
7618 ..LinkOptions::default()
7619 };
7620 Linker::run(&baseline_opts).unwrap();
7621
7622 let opts = LinkOptions {
7623 inputs: vec![obj.clone()],
7624 output: Some(our_out.clone()),
7625 kind: OutputKind::Executable,
7626 icf_mode: afs_ld::IcfMode::Safe,
7627 ..LinkOptions::default()
7628 };
7629 Linker::run(&opts).unwrap();
7630
7631 let baseline_bytes = fs::read(&baseline_out).unwrap();
7632 let our_bytes = fs::read(&our_out).unwrap();
7633 let baseline_symbols = symbol_values(&baseline_bytes);
7634 let our_symbols = symbol_values(&our_bytes);
7635 let baseline_const = output_section(&baseline_bytes, "__DATA_CONST", "__const")
7636 .unwrap()
7637 .1;
7638 let our_const = output_section(&our_bytes, "__DATA_CONST", "__const")
7639 .unwrap()
7640 .1;
7641
7642 assert_ne!(
7643 baseline_symbols.get("_const1"),
7644 baseline_symbols.get("_const2"),
7645 "baseline link should keep identical private __DATA_CONST atoms separate"
7646 );
7647 assert_eq!(
7648 our_symbols.get("_const1"),
7649 our_symbols.get("_const2"),
7650 "expected afs-ld -icf=safe to coalesce identical private __DATA_CONST atoms"
7651 );
7652 assert!(
7653 our_const.len() < baseline_const.len(),
7654 "expected -icf=safe to reduce __DATA_CONST,__const size on identical atoms"
7655 );
7656
7657 let _ = fs::remove_file(obj);
7658 let _ = fs::remove_file(baseline_out);
7659 let _ = fs::remove_file(our_out);
7660 }
7661
7662 #[test]
7663 fn linker_run_icf_safe_reaches_fixed_point_through_folded_targets() {
7664 if !have_xcrun() {
7665 eprintln!("skipping: xcrun unavailable");
7666 return;
7667 };
7668
7669 let obj = scratch("icf-fixed-point.o");
7670 let baseline_out = scratch("icf-fixed-point-baseline.out");
7671 let our_out = scratch("icf-fixed-point-ours.out");
7672 let src = r#"
7673 .section __TEXT,__text,regular,pure_instructions
7674 .globl _main
7675 _main:
7676 stp x29, x30, [sp, #-32]!
7677 mov x29, sp
7678 bl _wrapper1
7679 str w0, [sp, #16]
7680 bl _wrapper2
7681 ldr w8, [sp, #16]
7682 add w0, w8, w0
7683 ldp x29, x30, [sp], #32
7684 ret
7685
7686 .private_extern _wrapper1
7687 _wrapper1:
7688 b _leaf1
7689
7690 .private_extern _wrapper2
7691 _wrapper2:
7692 b _leaf2
7693
7694 .private_extern _leaf1
7695 _leaf1:
7696 mov w0, #6
7697 ret
7698
7699 .private_extern _leaf2
7700 _leaf2:
7701 mov w0, #6
7702 ret
7703 .subsections_via_symbols
7704 "#;
7705 if let Err(e) = assemble(src, &obj) {
7706 eprintln!("skipping: assemble failed: {e}");
7707 return;
7708 }
7709
7710 let baseline_opts = LinkOptions {
7711 inputs: vec![obj.clone()],
7712 output: Some(baseline_out.clone()),
7713 kind: OutputKind::Executable,
7714 ..LinkOptions::default()
7715 };
7716 Linker::run(&baseline_opts).unwrap();
7717
7718 let opts = LinkOptions {
7719 inputs: vec![obj.clone()],
7720 output: Some(our_out.clone()),
7721 kind: OutputKind::Executable,
7722 icf_mode: afs_ld::IcfMode::Safe,
7723 ..LinkOptions::default()
7724 };
7725 Linker::run(&opts).unwrap();
7726
7727 let baseline_bytes = fs::read(&baseline_out).unwrap();
7728 let our_bytes = fs::read(&our_out).unwrap();
7729 let baseline_symbols = symbol_values(&baseline_bytes);
7730 let our_symbols = symbol_values(&our_bytes);
7731 let baseline_text = output_section(&baseline_bytes, "__TEXT", "__text")
7732 .unwrap()
7733 .1;
7734 let our_text = output_section(&our_bytes, "__TEXT", "__text").unwrap().1;
7735
7736 assert_ne!(
7737 baseline_symbols.get("_leaf1"),
7738 baseline_symbols.get("_leaf2"),
7739 "baseline link should keep equivalent leaves separate"
7740 );
7741 assert_ne!(
7742 baseline_symbols.get("_wrapper1"),
7743 baseline_symbols.get("_wrapper2"),
7744 "baseline link should keep wrappers separate"
7745 );
7746 assert_eq!(
7747 our_symbols.get("_leaf1"),
7748 our_symbols.get("_leaf2"),
7749 "equivalent leaves should fold under -icf=safe"
7750 );
7751 assert_eq!(
7752 our_symbols.get("_wrapper1"),
7753 our_symbols.get("_wrapper2"),
7754 "wrappers should fold once their targets converge to the same winner"
7755 );
7756 assert_eq!(
7757 Command::new(&our_out).status().unwrap().code(),
7758 Some(12),
7759 "fixed-point folded executable should preserve runtime behavior"
7760 );
7761 assert!(
7762 our_text.len() < baseline_text.len(),
7763 "expected fixed-point folding to reduce text size"
7764 );
7765
7766 let _ = fs::remove_file(obj);
7767 let _ = fs::remove_file(baseline_out);
7768 let _ = fs::remove_file(our_out);
7769 }
7770
7771 #[test]
7772 fn linker_run_icf_safe_prefers_earlier_input_order_winner() {
7773 if !have_xcrun() {
7774 eprintln!("skipping: xcrun unavailable");
7775 return;
7776 };
7777
7778 let main_obj = scratch("icf-order-main.o");
7779 let first_obj = scratch("icf-order-first.o");
7780 let second_obj = scratch("icf-order-second.o");
7781 let our_out = scratch("icf-order-ours.out");
7782 let map = scratch("icf-order.map");
7783 let main_src = r#"
7784 .section __TEXT,__text,regular,pure_instructions
7785 .globl _main
7786 _main:
7787 stp x29, x30, [sp, #-32]!
7788 mov x29, sp
7789 bl _helper_a
7790 str w0, [sp, #16]
7791 bl _helper_b
7792 ldr w8, [sp, #16]
7793 add w0, w8, w0
7794 ldp x29, x30, [sp], #32
7795 ret
7796 .subsections_via_symbols
7797 "#;
7798 let helper_a_src = r#"
7799 .section __TEXT,__text,regular,pure_instructions
7800 .private_extern _helper_a
7801 .globl _helper_a
7802 _helper_a:
7803 mov w0, #4
7804 ret
7805 .subsections_via_symbols
7806 "#;
7807 let helper_b_src = r#"
7808 .section __TEXT,__text,regular,pure_instructions
7809 .private_extern _helper_b
7810 .globl _helper_b
7811 _helper_b:
7812 mov w0, #4
7813 ret
7814 .subsections_via_symbols
7815 "#;
7816 if let Err(e) = assemble(main_src, &main_obj) {
7817 eprintln!("skipping: assemble failed: {e}");
7818 return;
7819 }
7820 if let Err(e) = assemble(helper_a_src, &first_obj) {
7821 eprintln!("skipping: assemble failed: {e}");
7822 let _ = fs::remove_file(main_obj);
7823 return;
7824 }
7825 if let Err(e) = assemble(helper_b_src, &second_obj) {
7826 eprintln!("skipping: assemble failed: {e}");
7827 let _ = fs::remove_file(main_obj);
7828 let _ = fs::remove_file(first_obj);
7829 return;
7830 }
7831
7832 let opts = LinkOptions {
7833 inputs: vec![main_obj.clone(), first_obj.clone(), second_obj.clone()],
7834 output: Some(our_out.clone()),
7835 map: Some(map.clone()),
7836 kind: OutputKind::Executable,
7837 icf_mode: afs_ld::IcfMode::Safe,
7838 ..LinkOptions::default()
7839 };
7840 Linker::run(&opts).unwrap();
7841
7842 let our_bytes = fs::read(&our_out).unwrap();
7843 let our_symbols = symbol_values(&our_bytes);
7844 let map_text = fs::read_to_string(&map).unwrap();
7845
7846 assert_eq!(
7847 our_symbols.get("_helper_a"),
7848 our_symbols.get("_helper_b"),
7849 "equivalent helpers should fold to the same winner"
7850 );
7851 assert!(
7852 map_text.contains("_helper_b folded to _helper_a"),
7853 "earlier input should win safe-ICF ties:\n{map_text}"
7854 );
7855 assert_eq!(
7856 Command::new(&our_out).status().unwrap().code(),
7857 Some(8),
7858 "input-order folded executable should preserve runtime behavior"
7859 );
7860
7861 let _ = fs::remove_file(main_obj);
7862 let _ = fs::remove_file(first_obj);
7863 let _ = fs::remove_file(second_obj);
7864 let _ = fs::remove_file(our_out);
7865 let _ = fs::remove_file(map);
7866 }
7867