Rust · 160552 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, LC_BUILD_VERSION, LC_DATA_IN_CODE,
20 LC_DYLD_INFO_ONLY, LC_DYSYMTAB, LC_FUNCTION_STARTS, LC_SEGMENT_64, LC_SYMTAB,
21 REBASE_IMMEDIATE_MASK, REBASE_OPCODE_ADD_ADDR_IMM_SCALED, REBASE_OPCODE_ADD_ADDR_ULEB,
22 REBASE_OPCODE_DONE, REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB, REBASE_OPCODE_DO_REBASE_IMM_TIMES,
23 REBASE_OPCODE_DO_REBASE_ULEB_TIMES, REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB,
24 REBASE_OPCODE_MASK, REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB, REBASE_OPCODE_SET_TYPE_IMM,
25 REBASE_TYPE_POINTER, SG_READ_ONLY,
26 };
27 use afs_ld::macho::dylib::DylibFile;
28 use afs_ld::macho::exports::{ExportKind, Exports};
29 use afs_ld::macho::reader::{parse_commands, parse_header, u32_le, LoadCommand, Section64Header};
30 use afs_ld::string_table::StringTable;
31 use afs_ld::symbol::{parse_nlist_table, SymKind};
32 use afs_ld::synth::unwind::decode_unwind_info;
33 use afs_ld::{LinkError, LinkOptions, Linker, OutputKind};
34 use common::harness::diff_macho;
35
36 fn have_xcrun() -> bool {
37 Command::new("xcrun")
38 .arg("-f")
39 .arg("as")
40 .output()
41 .map(|o| o.status.success())
42 .unwrap_or(false)
43 }
44
45 fn sdk_path() -> Option<String> {
46 let out = Command::new("xcrun")
47 .args(["--sdk", "macosx", "--show-sdk-path"])
48 .output()
49 .ok()?;
50 if !out.status.success() {
51 return None;
52 }
53 Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
54 }
55
56 fn sdk_version() -> Option<String> {
57 let out = Command::new("xcrun")
58 .args(["--sdk", "macosx", "--show-sdk-version"])
59 .output()
60 .ok()?;
61 if !out.status.success() {
62 return None;
63 }
64 Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
65 }
66
67 fn have_xcrun_tool(tool: &str) -> bool {
68 Command::new("xcrun")
69 .arg("-f")
70 .arg(tool)
71 .output()
72 .map(|o| o.status.success())
73 .unwrap_or(false)
74 }
75
76 fn have_tool(tool: &str) -> bool {
77 Command::new(tool)
78 .arg("--version")
79 .output()
80 .map(|o| o.status.success() || !o.stderr.is_empty())
81 .unwrap_or(false)
82 }
83
84 fn assemble(src: &str, out: &PathBuf) -> Result<(), String> {
85 let tmp = std::env::temp_dir().join(format!(
86 "afs-ld-linker-run-{}-{}.s",
87 std::process::id(),
88 out.file_stem().and_then(|s| s.to_str()).unwrap_or("t")
89 ));
90 fs::write(&tmp, src).map_err(|e| format!("write: {e}"))?;
91 let output = Command::new("xcrun")
92 .args(["--sdk", "macosx", "as", "-arch", "arm64"])
93 .arg(&tmp)
94 .arg("-o")
95 .arg(out)
96 .output()
97 .map_err(|e| format!("spawn xcrun as: {e}"))?;
98 let _ = fs::remove_file(&tmp);
99 if !output.status.success() {
100 return Err(format!(
101 "xcrun as failed: {}",
102 String::from_utf8_lossy(&output.stderr)
103 ));
104 }
105 Ok(())
106 }
107
108 fn compile_c(src: &str, out: &PathBuf) -> Result<(), String> {
109 let tmp = std::env::temp_dir().join(format!(
110 "afs-ld-linker-run-{}-{}.c",
111 std::process::id(),
112 out.file_stem().and_then(|s| s.to_str()).unwrap_or("t")
113 ));
114 fs::write(&tmp, src).map_err(|e| format!("write: {e}"))?;
115 let output = Command::new("xcrun")
116 .args(["--sdk", "macosx", "clang", "-arch", "arm64", "-c"])
117 .arg(&tmp)
118 .arg("-o")
119 .arg(out)
120 .output()
121 .map_err(|e| format!("spawn xcrun clang: {e}"))?;
122 let _ = fs::remove_file(&tmp);
123 if !output.status.success() {
124 return Err(format!(
125 "xcrun clang failed: {}",
126 String::from_utf8_lossy(&output.stderr)
127 ));
128 }
129 Ok(())
130 }
131
132 fn compile_cxx(src: &str, out: &PathBuf) -> Result<(), String> {
133 let tmp = std::env::temp_dir().join(format!(
134 "afs-ld-linker-run-{}-{}.cc",
135 std::process::id(),
136 out.file_stem().and_then(|s| s.to_str()).unwrap_or("t")
137 ));
138 fs::write(&tmp, src).map_err(|e| format!("write: {e}"))?;
139 let output = Command::new("xcrun")
140 .args(["--sdk", "macosx", "clang++", "-arch", "arm64", "-c"])
141 .arg(&tmp)
142 .arg("-o")
143 .arg(out)
144 .output()
145 .map_err(|e| format!("spawn xcrun clang++: {e}"))?;
146 let _ = fs::remove_file(&tmp);
147 if !output.status.success() {
148 return Err(format!(
149 "xcrun clang++ failed: {}",
150 String::from_utf8_lossy(&output.stderr)
151 ));
152 }
153 Ok(())
154 }
155
156 fn compile_dylib_c(src: &str, out: &PathBuf) -> Result<(), String> {
157 let tmp = std::env::temp_dir().join(format!(
158 "afs-ld-linker-run-{}-{}.c",
159 std::process::id(),
160 out.file_stem().and_then(|s| s.to_str()).unwrap_or("lib")
161 ));
162 fs::write(&tmp, src).map_err(|e| format!("write: {e}"))?;
163 let install_name = out.to_string_lossy().to_string();
164 let output = Command::new("xcrun")
165 .args(["--sdk", "macosx", "clang", "-arch", "arm64", "-dynamiclib"])
166 .arg(&tmp)
167 .arg(format!("-Wl,-install_name,{install_name}"))
168 .arg("-o")
169 .arg(out)
170 .output()
171 .map_err(|e| format!("spawn xcrun clang dylib: {e}"))?;
172 let _ = fs::remove_file(&tmp);
173 if !output.status.success() {
174 return Err(format!(
175 "xcrun clang dylib failed: {}",
176 String::from_utf8_lossy(&output.stderr)
177 ));
178 }
179 Ok(())
180 }
181
182 fn scratch(name: &str) -> PathBuf {
183 std::env::temp_dir().join(format!("afs-ld-linker-run-{}-{name}", std::process::id()))
184 }
185
186 fn output_section(bytes: &[u8], segname: &str, sectname: &str) -> Option<(u64, Vec<u8>)> {
187 let header = parse_header(bytes).ok()?;
188 let commands = parse_commands(&header, bytes).ok()?;
189 for cmd in commands {
190 if let LoadCommand::Segment64(seg) = cmd {
191 for section in seg.sections {
192 if section.segname_str() == segname && section.sectname_str() == sectname {
193 let data = if section.offset == 0 {
194 Vec::new()
195 } else {
196 let start = section.offset as usize;
197 let end = start + section.size as usize;
198 bytes.get(start..end)?.to_vec()
199 };
200 return Some((section.addr, data));
201 }
202 }
203 }
204 }
205 None
206 }
207
208 fn output_section_header(bytes: &[u8], segname: &str, sectname: &str) -> Option<Section64Header> {
209 let header = parse_header(bytes).ok()?;
210 let commands = parse_commands(&header, bytes).ok()?;
211 for cmd in commands {
212 if let LoadCommand::Segment64(seg) = cmd {
213 for section in seg.sections {
214 if section.segname_str() == segname && section.sectname_str() == sectname {
215 return Some(section);
216 }
217 }
218 }
219 }
220 None
221 }
222
223 fn segment_flags(bytes: &[u8], segname: &str) -> Option<u32> {
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 if seg.segname_str() == segname {
229 return Some(seg.flags);
230 }
231 }
232 }
233 None
234 }
235
236 fn segment_vmaddr(bytes: &[u8], segname: &str) -> Option<u64> {
237 let header = parse_header(bytes).ok()?;
238 let commands = parse_commands(&header, bytes).ok()?;
239 for cmd in commands {
240 if let LoadCommand::Segment64(seg) = cmd {
241 if seg.segname_str() == segname {
242 return Some(seg.vmaddr);
243 }
244 }
245 }
246 None
247 }
248
249 fn symtab_and_dysymtab(
250 bytes: &[u8],
251 ) -> (
252 afs_ld::macho::reader::SymtabCmd,
253 afs_ld::macho::reader::DysymtabCmd,
254 ) {
255 let header = parse_header(bytes).unwrap();
256 let commands = parse_commands(&header, bytes).unwrap();
257 let mut symtab = None;
258 let mut dysymtab = None;
259 for cmd in commands {
260 match cmd {
261 LoadCommand::Symtab(cmd) => symtab = Some(cmd),
262 LoadCommand::Dysymtab(cmd) => dysymtab = Some(cmd),
263 _ => {}
264 }
265 }
266 (symtab.unwrap(), dysymtab.unwrap())
267 }
268
269 fn symbol_partition_names(bytes: &[u8]) -> (Vec<String>, Vec<String>, Vec<String>) {
270 let (symtab, dysymtab) = symtab_and_dysymtab(bytes);
271 let symbols = parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).unwrap();
272 let strings = StringTable::from_file(bytes, symtab.stroff, symtab.strsize).unwrap();
273 let names_for = |start: u32, count: u32| -> Vec<String> {
274 symbols[start as usize..(start + count) as usize]
275 .iter()
276 .map(|symbol| strings.get(symbol.strx()).unwrap().to_string())
277 .collect()
278 };
279 (
280 names_for(dysymtab.ilocalsym, dysymtab.nlocalsym),
281 names_for(dysymtab.iextdefsym, dysymtab.nextdefsym),
282 names_for(dysymtab.iundefsym, dysymtab.nundefsym),
283 )
284 }
285
286 #[derive(Debug, Clone, PartialEq, Eq)]
287 struct CanonicalSymbolRecord {
288 name: String,
289 n_type: u8,
290 n_sect: u8,
291 n_desc: u16,
292 value: u64,
293 }
294
295 fn section_addrs(bytes: &[u8]) -> Vec<u64> {
296 let header = parse_header(bytes).unwrap();
297 let commands = parse_commands(&header, bytes).unwrap();
298 let mut out = Vec::new();
299 for cmd in commands {
300 if let LoadCommand::Segment64(seg) = cmd {
301 for section in seg.sections {
302 out.push(section.addr);
303 }
304 }
305 }
306 out
307 }
308
309 fn canonical_symbol_records(bytes: &[u8]) -> Vec<CanonicalSymbolRecord> {
310 let (symtab, _) = symtab_and_dysymtab(bytes);
311 let symbols = parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).unwrap();
312 let strings = StringTable::from_file(bytes, symtab.stroff, symtab.strsize).unwrap();
313 let section_addrs = section_addrs(bytes);
314 symbols
315 .iter()
316 .map(|symbol| {
317 let value = if symbol.kind() == SymKind::Sect && symbol.sect_idx() != 0 {
318 let section_addr = section_addrs[symbol.sect_idx() as usize - 1];
319 if symbol.value() >= section_addr {
320 symbol.value() - section_addr
321 } else {
322 symbol.value()
323 }
324 } else {
325 symbol.value()
326 };
327 CanonicalSymbolRecord {
328 name: strings.get(symbol.strx()).unwrap().to_string(),
329 n_type: symbol.raw.n_type,
330 n_sect: symbol.raw.n_sect,
331 n_desc: symbol.raw.n_desc,
332 value,
333 }
334 })
335 .collect()
336 }
337
338 #[derive(Debug, Clone, PartialEq, Eq)]
339 enum CanonicalExportKind {
340 Regular(u64),
341 ThreadLocal(u64),
342 Absolute(u64),
343 Reexport { ordinal: u32, imported_name: String },
344 StubAndResolver { stub: u64, resolver: u64 },
345 }
346
347 #[derive(Debug, Clone, PartialEq, Eq)]
348 struct CanonicalExportRecord {
349 name: String,
350 flags: u64,
351 kind: CanonicalExportKind,
352 }
353
354 fn canonical_export_records(bytes: &[u8]) -> Vec<CanonicalExportRecord> {
355 let dylib = DylibFile::parse("/tmp/canonical.dylib", bytes).unwrap();
356 let symbol_values: HashMap<String, u64> = canonical_symbol_records(bytes)
357 .into_iter()
358 .map(|record| (record.name, record.value))
359 .collect();
360 let mut out = dylib
361 .exports
362 .entries()
363 .unwrap()
364 .into_iter()
365 .map(|entry| {
366 let kind = match entry.kind {
367 ExportKind::Regular { .. } => {
368 CanonicalExportKind::Regular(*symbol_values.get(&entry.name).unwrap())
369 }
370 ExportKind::ThreadLocal { .. } => {
371 CanonicalExportKind::ThreadLocal(*symbol_values.get(&entry.name).unwrap())
372 }
373 ExportKind::Absolute { .. } => {
374 CanonicalExportKind::Absolute(*symbol_values.get(&entry.name).unwrap())
375 }
376 ExportKind::Reexport {
377 ordinal,
378 imported_name,
379 } => CanonicalExportKind::Reexport {
380 ordinal,
381 imported_name,
382 },
383 ExportKind::StubAndResolver { stub, resolver } => {
384 CanonicalExportKind::StubAndResolver { stub, resolver }
385 }
386 };
387 CanonicalExportRecord {
388 name: entry.name,
389 flags: entry.flags,
390 kind,
391 }
392 })
393 .collect::<Vec<_>>();
394 out.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
395 out
396 }
397
398 fn dyld_info_export_names(bytes: &[u8]) -> Result<Vec<String>, String> {
399 let trie = dyld_info_stream(bytes, DyldInfoStreamKind::Export)?;
400 if trie.is_empty() {
401 return Ok(Vec::new());
402 }
403 let mut out = Exports::from_trie_bytes(&trie)
404 .entries()
405 .map_err(|e| format!("decode export trie: {e}"))?
406 .into_iter()
407 .map(|entry| entry.name)
408 .collect::<Vec<_>>();
409 out.sort();
410 Ok(out)
411 }
412
413 fn raw_string_table(bytes: &[u8]) -> Vec<u8> {
414 let (symtab, _) = symtab_and_dysymtab(bytes);
415 let start = symtab.stroff as usize;
416 let end = start + symtab.strsize as usize;
417 bytes[start..end].to_vec()
418 }
419
420 fn symbol_name_offsets(bytes: &[u8]) -> HashMap<String, u32> {
421 let (symtab, _) = symtab_and_dysymtab(bytes);
422 let symbols = parse_nlist_table(bytes, symtab.symoff, symtab.nsyms).unwrap();
423 let strings = StringTable::from_file(bytes, symtab.stroff, symtab.strsize).unwrap();
424 symbols
425 .iter()
426 .map(|symbol| {
427 (
428 strings.get(symbol.strx()).unwrap().to_string(),
429 symbol.strx(),
430 )
431 })
432 .collect()
433 }
434
435 fn indirect_symbol_table(bytes: &[u8]) -> Vec<u32> {
436 let (_, dysymtab) = symtab_and_dysymtab(bytes);
437 if dysymtab.nindirectsyms == 0 {
438 return Vec::new();
439 }
440 let start = dysymtab.indirectsymoff as usize;
441 let end = start + dysymtab.nindirectsyms as usize * 4;
442 bytes[start..end]
443 .chunks_exact(4)
444 .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
445 .collect()
446 }
447
448 fn raw_linkedit_data_cmd(bytes: &[u8], expected_cmd: u32) -> (u32, u32) {
449 let header = parse_header(bytes).unwrap();
450 let commands = parse_commands(&header, bytes).unwrap();
451 for cmd in commands {
452 if let LoadCommand::Raw { cmd, data, .. } = cmd {
453 if cmd == expected_cmd {
454 return (u32_le(&data[0..4]), u32_le(&data[4..8]));
455 }
456 }
457 }
458 panic!("missing raw linkedit command 0x{expected_cmd:x}");
459 }
460
461 fn linkedit_payload(bytes: &[u8], cmd: u32) -> Vec<u8> {
462 let (dataoff, datasize) = raw_linkedit_data_cmd(bytes, cmd);
463 if datasize == 0 {
464 return Vec::new();
465 }
466 bytes[dataoff as usize..(dataoff + datasize) as usize].to_vec()
467 }
468
469 fn decode_function_starts(bytes: &[u8]) -> Vec<u64> {
470 let payload = linkedit_payload(bytes, LC_FUNCTION_STARTS);
471 let mut offsets = Vec::new();
472 let mut cursor = 0usize;
473 let mut current = 0u64;
474 while cursor < payload.len() {
475 let (delta, used) = read_uleb(&payload[cursor..]).unwrap();
476 cursor += used;
477 if delta == 0 {
478 break;
479 }
480 current += delta;
481 offsets.push(current);
482 }
483 offsets
484 }
485
486 fn command_ids(bytes: &[u8]) -> Vec<u32> {
487 let header = parse_header(bytes).unwrap();
488 let commands = parse_commands(&header, bytes).unwrap();
489 commands
490 .into_iter()
491 .map(|cmd| match cmd {
492 LoadCommand::Segment64(_) => LC_SEGMENT_64,
493 LoadCommand::Symtab(_) => LC_SYMTAB,
494 LoadCommand::Dysymtab(_) => LC_DYSYMTAB,
495 LoadCommand::BuildVersion(_) => LC_BUILD_VERSION,
496 LoadCommand::Dylib(d) => d.cmd,
497 LoadCommand::DyldInfoOnly(_) => LC_DYLD_INFO_ONLY,
498 LoadCommand::Raw { cmd, .. } => cmd,
499 other => panic!("unexpected load command in command_ids helper: {other:?}"),
500 })
501 .collect()
502 }
503
504 fn normalize_function_start_offsets(starts: &[u64]) -> Vec<u64> {
505 let Some(&base) = starts.first() else {
506 return Vec::new();
507 };
508 starts.iter().map(|offset| offset - base).collect()
509 }
510
511 #[derive(Debug, Clone, PartialEq, Eq)]
512 struct DataInCodeRecord {
513 offset: u32,
514 length: u16,
515 kind: u16,
516 }
517
518 fn rebased_unwind_bytes(bytes: &[u8]) -> Vec<u8> {
519 let header_base = segment_vmaddr(bytes, "__TEXT").unwrap_or(0);
520 let text_base = output_section(bytes, "__TEXT", "__text").unwrap().0 - header_base;
521 let got_range = output_section(bytes, "__DATA_CONST", "__got")
522 .map(|(addr, data)| (addr - header_base, addr - header_base + data.len() as u64));
523 let lsda_base =
524 output_section(bytes, "__TEXT", "__gcc_except_tab").map(|(addr, _)| addr - header_base);
525 let (_, unwind) = output_section(bytes, "__TEXT", "__unwind_info").unwrap();
526 let mut out = unwind;
527 if out.len() < 28 {
528 return out;
529 }
530
531 let personalities_offset = u32_le(&out[12..16]) as usize;
532 let personalities_count = u32_le(&out[16..20]) as usize;
533 let indices_offset = u32_le(&out[20..24]) as usize;
534 let indices_count = u32_le(&out[24..28]) as usize;
535
536 for idx in 0..personalities_count {
537 let off = personalities_offset + idx * 4;
538 let value = u32_le(&out[off..off + 4]) as u64;
539 let rebased = if let Some((got_start, got_end)) = got_range {
540 if got_start <= value && value < got_end {
541 value - got_start
542 } else if value >= text_base {
543 value - text_base
544 } else {
545 value
546 }
547 } else if value >= text_base {
548 value - text_base
549 } else {
550 value
551 };
552 out[off..off + 4].copy_from_slice(&(rebased as u32).to_le_bytes());
553 }
554
555 let mut lsda_offsets = Vec::with_capacity(indices_count);
556 for idx in 0..indices_count {
557 let entry_off = indices_offset + idx * 12;
558 let function_offset = u32_le(&out[entry_off..entry_off + 4]) as u64;
559 let rebased = function_offset.saturating_sub(text_base);
560 out[entry_off..entry_off + 4].copy_from_slice(&(rebased as u32).to_le_bytes());
561 lsda_offsets.push(u32_le(&out[entry_off + 8..entry_off + 12]) as usize);
562 }
563
564 if let (Some(lsda_base), Some(&start), Some(&end)) =
565 (lsda_base, lsda_offsets.first(), lsda_offsets.last())
566 {
567 let mut entry_off = start;
568 while entry_off < end {
569 let function_offset = u32_le(&out[entry_off..entry_off + 4]) as u64;
570 let lsda_offset = u32_le(&out[entry_off + 4..entry_off + 8]) as u64;
571 out[entry_off..entry_off + 4]
572 .copy_from_slice(&(function_offset.saturating_sub(text_base) as u32).to_le_bytes());
573 out[entry_off + 4..entry_off + 8]
574 .copy_from_slice(&(lsda_offset.saturating_sub(lsda_base) as u32).to_le_bytes());
575 entry_off += 8;
576 }
577 }
578
579 out
580 }
581
582 fn normalized_eh_frame_dump(path: &PathBuf, text_base: u64) -> Result<String, String> {
583 let output = Command::new("xcrun")
584 .args(["dwarfdump", "--eh-frame"])
585 .arg(path)
586 .output()
587 .map_err(|e| format!("spawn xcrun dwarfdump: {e}"))?;
588 if !output.status.success() {
589 return Err(format!(
590 "xcrun dwarfdump failed: {}",
591 String::from_utf8_lossy(&output.stderr)
592 ));
593 }
594 let mut normalized = Vec::new();
595 for line in String::from_utf8_lossy(&output.stdout).lines() {
596 let trimmed = line.trim();
597 if trimmed.starts_with("0x") && trimmed.contains(": CFA=") {
598 let (addr, rest) = trimmed.split_once(':').unwrap();
599 let value = u64::from_str_radix(addr.trim_start_matches("0x"), 16).unwrap();
600 normalized.push(format!("0x{:x}:{}", value - text_base, rest));
601 continue;
602 }
603 if let Some(pc_idx) = trimmed.find("pc=") {
604 let prefix = &trimmed[..pc_idx + 3];
605 let range = &trimmed[pc_idx + 3..];
606 if let Some((start, end)) = range.split_once("...") {
607 let start = u64::from_str_radix(start, 16).unwrap();
608 let end = u64::from_str_radix(end, 16).unwrap();
609 normalized.push(format!(
610 "{}0x{:x}...0x{:x}",
611 prefix,
612 start - text_base,
613 end - text_base
614 ));
615 continue;
616 }
617 }
618 if trimmed.is_empty()
619 || trimmed.starts_with(".debug_frame")
620 || trimmed.starts_with(".eh_frame")
621 || trimmed.ends_with("file format Mach-O arm64")
622 {
623 continue;
624 }
625 normalized.push(rebase_hex_addresses(trimmed, text_base));
626 }
627 Ok(normalized.join("\n"))
628 }
629
630 fn canonical_unwind_info(bytes: &[u8]) -> afs_ld::synth::unwind::DecodedUnwindInfo {
631 let (_, unwind) = output_section(bytes, "__TEXT", "__unwind_info").unwrap();
632 let mut decoded = decode_unwind_info(&unwind).unwrap();
633 let header_base = segment_vmaddr(bytes, "__TEXT").unwrap_or(0);
634 let text_base = output_section(bytes, "__TEXT", "__text").unwrap().0 - header_base;
635 for record in &mut decoded.records {
636 record.function_offset -= text_base as u32;
637 }
638 if let Some((lsda_addr, _)) = output_section(bytes, "__TEXT", "__gcc_except_tab") {
639 let lsda_base = lsda_addr - header_base;
640 for record in &mut decoded.lsdas {
641 record.function_offset -= text_base as u32;
642 record.lsda_offset -= lsda_base as u32;
643 }
644 }
645 if let Some((got_addr, got)) = output_section(bytes, "__DATA_CONST", "__got") {
646 let got_base = got_addr - header_base;
647 let got_end = got_base + got.len() as u64;
648 for personality in &mut decoded.personalities {
649 let offset = *personality as u64;
650 if got_base <= offset && offset < got_end {
651 *personality -= got_base as u32;
652 }
653 }
654 }
655 decoded
656 }
657
658 fn rebase_hex_addresses(line: &str, text_base: u64) -> String {
659 let bytes = line.as_bytes();
660 let mut out = String::new();
661 let mut idx = 0;
662 while idx < bytes.len() {
663 if idx + 2 <= bytes.len() && bytes[idx] == b'0' && bytes[idx + 1] == b'x' {
664 let mut end = idx + 2;
665 while end < bytes.len() && bytes[end].is_ascii_hexdigit() {
666 end += 1;
667 }
668 let token = &line[idx + 2..end];
669 let value = u64::from_str_radix(token, 16).unwrap();
670 if value >= text_base {
671 out.push_str(&format!("0x{:x}", value - text_base));
672 } else {
673 out.push_str(&line[idx..end]);
674 }
675 idx = end;
676 continue;
677 }
678 out.push(bytes[idx] as char);
679 idx += 1;
680 }
681 out
682 }
683
684 fn decode_data_in_code(bytes: &[u8]) -> Vec<DataInCodeRecord> {
685 let payload = linkedit_payload(bytes, LC_DATA_IN_CODE);
686 payload
687 .chunks_exact(8)
688 .map(|chunk| DataInCodeRecord {
689 offset: u32::from_le_bytes(chunk[0..4].try_into().unwrap()),
690 length: u16::from_le_bytes(chunk[4..6].try_into().unwrap()),
691 kind: u16::from_le_bytes(chunk[6..8].try_into().unwrap()),
692 })
693 .collect()
694 }
695
696 fn canonical_data_in_code(bytes: &[u8]) -> Vec<DataInCodeRecord> {
697 let text = output_section_header(bytes, "__TEXT", "__text").unwrap();
698 decode_data_in_code(bytes)
699 .into_iter()
700 .map(|record| DataInCodeRecord {
701 offset: record.offset - text.offset,
702 length: record.length,
703 kind: record.kind,
704 })
705 .collect()
706 }
707
708 fn assert_strtab_within_five_percent(ours: &[u8], apple: &[u8]) {
709 let delta = ours.len().abs_diff(apple.len());
710 assert!(
711 delta * 20 <= apple.len(),
712 "string table length drifted too far from Apple ld: ours={} apple={}",
713 ours.len(),
714 apple.len()
715 );
716 }
717
718 fn apple_link(
719 obj: &PathBuf,
720 out: &PathBuf,
721 entry: &str,
722 syslibroot: &str,
723 platform_version: &str,
724 ) -> Result<(), String> {
725 apple_link_with_args(obj, out, entry, syslibroot, platform_version, &[])
726 }
727
728 fn apple_link_with_args(
729 obj: &PathBuf,
730 out: &PathBuf,
731 entry: &str,
732 syslibroot: &str,
733 platform_version: &str,
734 extra_args: &[&str],
735 ) -> Result<(), String> {
736 let output = Command::new("xcrun")
737 .args([
738 "ld",
739 "-arch",
740 "arm64",
741 "-platform_version",
742 "macos",
743 platform_version,
744 platform_version,
745 "-syslibroot",
746 syslibroot,
747 "-lSystem",
748 "-e",
749 entry,
750 ])
751 .args(extra_args)
752 .arg("-o")
753 .arg(out)
754 .arg(obj)
755 .output()
756 .map_err(|e| format!("spawn xcrun ld: {e}"))?;
757 if !output.status.success() {
758 return Err(format!(
759 "xcrun ld failed: {}",
760 String::from_utf8_lossy(&output.stderr)
761 ));
762 }
763 Ok(())
764 }
765
766 fn apple_link_classic_lazy(
767 obj: &PathBuf,
768 out: &PathBuf,
769 entry: &str,
770 syslibroot: &str,
771 platform_version: &str,
772 ) -> Result<(), String> {
773 apple_link_with_args(
774 obj,
775 out,
776 entry,
777 syslibroot,
778 platform_version,
779 &["-no_fixup_chains"],
780 )
781 }
782
783 fn apple_link_dylib_classic(
784 obj: &PathBuf,
785 out: &PathBuf,
786 install_name: &str,
787 syslibroot: &str,
788 platform_version: &str,
789 ) -> Result<(), String> {
790 let output = Command::new("xcrun")
791 .args([
792 "ld",
793 "-dylib",
794 "-arch",
795 "arm64",
796 "-platform_version",
797 "macos",
798 platform_version,
799 platform_version,
800 "-syslibroot",
801 syslibroot,
802 "-lSystem",
803 "-install_name",
804 install_name,
805 "-no_fixup_chains",
806 ])
807 .arg("-o")
808 .arg(out)
809 .arg(obj)
810 .output()
811 .map_err(|e| format!("spawn xcrun ld -dylib: {e}"))?;
812 if !output.status.success() {
813 return Err(format!(
814 "xcrun ld -dylib failed: {}",
815 String::from_utf8_lossy(&output.stderr)
816 ));
817 }
818 Ok(())
819 }
820
821 fn apple_link_cxx_classic(obj: &PathBuf, out: &PathBuf) -> Result<(), String> {
822 let output = Command::new("xcrun")
823 .args([
824 "--sdk",
825 "macosx",
826 "clang++",
827 "-arch",
828 "arm64",
829 "-Wl,-no_fixup_chains",
830 "-o",
831 ])
832 .arg(out)
833 .arg(obj)
834 .output()
835 .map_err(|e| format!("spawn xcrun clang++ link: {e}"))?;
836 if !output.status.success() {
837 return Err(format!(
838 "xcrun clang++ link failed: {}",
839 String::from_utf8_lossy(&output.stderr)
840 ));
841 }
842 Ok(())
843 }
844
845 #[derive(Debug, Clone, PartialEq, Eq)]
846 struct RebaseRecord {
847 segment: String,
848 section: String,
849 section_offset: u64,
850 rebase_type: u8,
851 }
852
853 #[derive(Debug, Clone, PartialEq, Eq)]
854 struct BindRecord {
855 segment: String,
856 section: String,
857 section_offset: u64,
858 ordinal: u16,
859 symbol: String,
860 weak_import: bool,
861 }
862
863 #[derive(Debug, Clone)]
864 struct SegmentView {
865 name: String,
866 vm_addr: u64,
867 vm_size: u64,
868 sections: Vec<Section64Header>,
869 }
870
871 fn dyld_info_command(bytes: &[u8]) -> Result<afs_ld::macho::reader::DyldInfoCmd, String> {
872 let header = parse_header(bytes).map_err(|e| e.to_string())?;
873 let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
874 commands
875 .into_iter()
876 .find_map(|cmd| match cmd {
877 LoadCommand::DyldInfoOnly(cmd) => Some(cmd),
878 _ => None,
879 })
880 .ok_or_else(|| "missing LC_DYLD_INFO_ONLY".to_string())
881 }
882
883 #[derive(Clone, Copy)]
884 enum DyldInfoStreamKind {
885 Rebase,
886 Bind,
887 WeakBind,
888 LazyBind,
889 Export,
890 }
891
892 fn dyld_info_stream(bytes: &[u8], kind: DyldInfoStreamKind) -> Result<Vec<u8>, String> {
893 let dyld_info = dyld_info_command(bytes)?;
894 let (off, size) = match kind {
895 DyldInfoStreamKind::Rebase => (dyld_info.rebase_off, dyld_info.rebase_size),
896 DyldInfoStreamKind::Bind => (dyld_info.bind_off, dyld_info.bind_size),
897 DyldInfoStreamKind::WeakBind => (dyld_info.weak_bind_off, dyld_info.weak_bind_size),
898 DyldInfoStreamKind::LazyBind => (dyld_info.lazy_bind_off, dyld_info.lazy_bind_size),
899 DyldInfoStreamKind::Export => (dyld_info.export_off, dyld_info.export_size),
900 };
901 if size == 0 {
902 return Ok(Vec::new());
903 }
904 let start = off as usize;
905 let end = start + size as usize;
906 bytes
907 .get(start..end)
908 .map(|slice| slice.to_vec())
909 .ok_or_else(|| "dyld-info stream out of bounds".to_string())
910 }
911
912 fn canonical_lazy_bind_stream(bytes: &[u8]) -> Result<Vec<u8>, String> {
913 let mut stream = dyld_info_stream(bytes, DyldInfoStreamKind::LazyBind)?;
914 while stream.len() >= 2
915 && stream[stream.len() - 1] == BIND_OPCODE_DONE
916 && stream[stream.len() - 2] == BIND_OPCODE_DONE
917 {
918 stream.pop();
919 }
920 Ok(stream)
921 }
922
923 fn segment_views(bytes: &[u8]) -> Result<Vec<SegmentView>, String> {
924 let header = parse_header(bytes).map_err(|e| e.to_string())?;
925 let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
926 Ok(commands
927 .into_iter()
928 .filter_map(|cmd| match cmd {
929 LoadCommand::Segment64(seg) => Some(SegmentView {
930 name: seg.segname_str().to_string(),
931 vm_addr: seg.vmaddr,
932 vm_size: seg.vmsize,
933 sections: seg.sections,
934 }),
935 _ => None,
936 })
937 .collect())
938 }
939
940 fn read_cstr(bytes: &[u8], cursor: &mut usize) -> Result<String, String> {
941 let start = *cursor;
942 let end = bytes[start..]
943 .iter()
944 .position(|b| *b == 0)
945 .map(|len| start + len)
946 .ok_or_else(|| "unterminated dyld-info string".to_string())?;
947 *cursor = end + 1;
948 std::str::from_utf8(&bytes[start..end])
949 .map(|s| s.to_string())
950 .map_err(|e| format!("dyld-info string is not UTF-8: {e}"))
951 }
952
953 fn locate_section(
954 segments: &[SegmentView],
955 segment_index: u8,
956 segment_offset: u64,
957 ) -> Result<(String, String, u64), String> {
958 let segment = segments
959 .get(segment_index as usize)
960 .ok_or_else(|| format!("segment index {segment_index} out of range"))?;
961 let addr = segment.vm_addr + segment_offset;
962 for section in &segment.sections {
963 if addr >= section.addr && addr < section.addr + section.size {
964 return Ok((
965 section.segname_str().to_string(),
966 section.sectname_str().to_string(),
967 addr - section.addr,
968 ));
969 }
970 }
971 if segment_offset <= segment.vm_size {
972 return Ok((segment.name.clone(), String::new(), segment_offset));
973 }
974 Err(format!(
975 "address 0x{addr:x} does not land in any section of {}",
976 segment.name
977 ))
978 }
979
980 fn decode_rebase_records(bytes: &[u8]) -> Result<Vec<RebaseRecord>, String> {
981 let dyld_info = dyld_info_command(bytes)?;
982 if dyld_info.rebase_size == 0 {
983 return Ok(Vec::new());
984 }
985 let segments = segment_views(bytes)?;
986 let start = dyld_info.rebase_off as usize;
987 let end = start + dyld_info.rebase_size as usize;
988 let stream = bytes
989 .get(start..end)
990 .ok_or_else(|| "rebase stream out of bounds".to_string())?;
991
992 let mut out = Vec::new();
993 let mut cursor = 0usize;
994 let mut segment_index = 0u8;
995 let mut segment_offset = 0u64;
996 let mut rebase_type = 0u8;
997 while cursor < stream.len() {
998 let byte = stream[cursor];
999 cursor += 1;
1000 let opcode = byte & REBASE_OPCODE_MASK;
1001 let imm = byte & REBASE_IMMEDIATE_MASK;
1002 match opcode {
1003 REBASE_OPCODE_DONE => break,
1004 REBASE_OPCODE_SET_TYPE_IMM => rebase_type = imm,
1005 REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB => {
1006 segment_index = imm;
1007 let (offset, len) =
1008 read_uleb(&stream[cursor..]).map_err(|e| format!("rebase ULEB: {e}"))?;
1009 cursor += len;
1010 segment_offset = offset;
1011 }
1012 REBASE_OPCODE_ADD_ADDR_ULEB => {
1013 let (delta, len) =
1014 read_uleb(&stream[cursor..]).map_err(|e| format!("rebase ULEB: {e}"))?;
1015 cursor += len;
1016 segment_offset += delta;
1017 }
1018 REBASE_OPCODE_ADD_ADDR_IMM_SCALED => {
1019 segment_offset += (imm as u64) * 8;
1020 }
1021 REBASE_OPCODE_DO_REBASE_IMM_TIMES => {
1022 for _ in 0..imm {
1023 let (segment, section, section_offset) =
1024 locate_section(&segments, segment_index, segment_offset)?;
1025 out.push(RebaseRecord {
1026 segment,
1027 section,
1028 section_offset,
1029 rebase_type,
1030 });
1031 segment_offset += 8;
1032 }
1033 }
1034 REBASE_OPCODE_DO_REBASE_ULEB_TIMES => {
1035 let (count, len) =
1036 read_uleb(&stream[cursor..]).map_err(|e| format!("rebase ULEB: {e}"))?;
1037 cursor += len;
1038 for _ in 0..count {
1039 let (segment, section, section_offset) =
1040 locate_section(&segments, segment_index, segment_offset)?;
1041 out.push(RebaseRecord {
1042 segment,
1043 section,
1044 section_offset,
1045 rebase_type,
1046 });
1047 segment_offset += 8;
1048 }
1049 }
1050 REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB => {
1051 let (segment, section, section_offset) =
1052 locate_section(&segments, segment_index, segment_offset)?;
1053 out.push(RebaseRecord {
1054 segment,
1055 section,
1056 section_offset,
1057 rebase_type,
1058 });
1059 segment_offset += 8;
1060 let (delta, len) =
1061 read_uleb(&stream[cursor..]).map_err(|e| format!("rebase ULEB: {e}"))?;
1062 cursor += len;
1063 segment_offset += delta;
1064 }
1065 REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB => {
1066 let (count, count_len) =
1067 read_uleb(&stream[cursor..]).map_err(|e| format!("rebase ULEB: {e}"))?;
1068 cursor += count_len;
1069 let (skip, skip_len) =
1070 read_uleb(&stream[cursor..]).map_err(|e| format!("rebase ULEB: {e}"))?;
1071 cursor += skip_len;
1072 for _ in 0..count {
1073 let (segment, section, section_offset) =
1074 locate_section(&segments, segment_index, segment_offset)?;
1075 out.push(RebaseRecord {
1076 segment,
1077 section,
1078 section_offset,
1079 rebase_type,
1080 });
1081 segment_offset += 8 + skip;
1082 }
1083 }
1084 _ => return Err(format!("unsupported rebase opcode 0x{byte:02x}")),
1085 }
1086 }
1087 Ok(out)
1088 }
1089
1090 fn decode_bind_records(bytes: &[u8], lazy: bool) -> Result<Vec<BindRecord>, String> {
1091 let dyld_info = dyld_info_command(bytes)?;
1092 let (off, size) = if lazy {
1093 (dyld_info.lazy_bind_off, dyld_info.lazy_bind_size)
1094 } else {
1095 (dyld_info.bind_off, dyld_info.bind_size)
1096 };
1097 if size == 0 {
1098 return Ok(Vec::new());
1099 }
1100 let segments = segment_views(bytes)?;
1101 let start = off as usize;
1102 let end = start + size as usize;
1103 let stream = bytes
1104 .get(start..end)
1105 .ok_or_else(|| "bind stream out of bounds".to_string())?;
1106
1107 let mut out = Vec::new();
1108 let mut cursor = 0usize;
1109 let mut segment_index = 0u8;
1110 let mut segment_offset = 0u64;
1111 let mut ordinal = 0u16;
1112 let mut symbol = String::new();
1113 let mut weak_import = false;
1114 while cursor < stream.len() {
1115 let byte = stream[cursor];
1116 cursor += 1;
1117 let opcode = byte & BIND_OPCODE_MASK;
1118 let imm = byte & BIND_IMMEDIATE_MASK;
1119 match opcode {
1120 BIND_OPCODE_DONE => {
1121 if lazy {
1122 symbol.clear();
1123 weak_import = false;
1124 } else {
1125 break;
1126 }
1127 }
1128 BIND_OPCODE_SET_DYLIB_ORDINAL_IMM => ordinal = imm as u16,
1129 BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB => {
1130 let (value, len) =
1131 read_uleb(&stream[cursor..]).map_err(|e| format!("bind ULEB: {e}"))?;
1132 cursor += len;
1133 ordinal = value as u16;
1134 }
1135 BIND_OPCODE_SET_DYLIB_SPECIAL_IMM => {
1136 let signed = ((imm as i8) << 4) >> 4;
1137 ordinal = signed as i16 as u16;
1138 }
1139 BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM => {
1140 weak_import = (imm & BIND_SYMBOL_FLAGS_WEAK_IMPORT) != 0;
1141 symbol = read_cstr(stream, &mut cursor)?;
1142 }
1143 BIND_OPCODE_SET_TYPE_IMM => {}
1144 BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB => {
1145 segment_index = imm;
1146 let (offset, len) =
1147 read_uleb(&stream[cursor..]).map_err(|e| format!("bind ULEB: {e}"))?;
1148 cursor += len;
1149 segment_offset = offset;
1150 }
1151 BIND_OPCODE_ADD_ADDR_ULEB => {
1152 let (delta, len) =
1153 read_uleb(&stream[cursor..]).map_err(|e| format!("bind ULEB: {e}"))?;
1154 cursor += len;
1155 segment_offset += delta;
1156 }
1157 BIND_OPCODE_DO_BIND => {
1158 let (segment, section, section_offset) =
1159 locate_section(&segments, segment_index, segment_offset)?;
1160 out.push(BindRecord {
1161 segment,
1162 section,
1163 section_offset,
1164 ordinal,
1165 symbol: symbol.clone(),
1166 weak_import,
1167 });
1168 segment_offset += 8;
1169 }
1170 BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB => {
1171 let (segment, section, section_offset) =
1172 locate_section(&segments, segment_index, segment_offset)?;
1173 out.push(BindRecord {
1174 segment,
1175 section,
1176 section_offset,
1177 ordinal,
1178 symbol: symbol.clone(),
1179 weak_import,
1180 });
1181 segment_offset += 8;
1182 let (delta, len) =
1183 read_uleb(&stream[cursor..]).map_err(|e| format!("bind ULEB: {e}"))?;
1184 cursor += len;
1185 segment_offset += delta;
1186 }
1187 BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED => {
1188 let (segment, section, section_offset) =
1189 locate_section(&segments, segment_index, segment_offset)?;
1190 out.push(BindRecord {
1191 segment,
1192 section,
1193 section_offset,
1194 ordinal,
1195 symbol: symbol.clone(),
1196 weak_import,
1197 });
1198 segment_offset += 8 + (imm as u64) * 8;
1199 }
1200 BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB => {
1201 let (count, count_len) =
1202 read_uleb(&stream[cursor..]).map_err(|e| format!("bind ULEB: {e}"))?;
1203 cursor += count_len;
1204 let (skip, skip_len) =
1205 read_uleb(&stream[cursor..]).map_err(|e| format!("bind ULEB: {e}"))?;
1206 cursor += skip_len;
1207 for _ in 0..count {
1208 let (segment, section, section_offset) =
1209 locate_section(&segments, segment_index, segment_offset)?;
1210 out.push(BindRecord {
1211 segment,
1212 section,
1213 section_offset,
1214 ordinal,
1215 symbol: symbol.clone(),
1216 weak_import,
1217 });
1218 segment_offset += 8 + skip;
1219 }
1220 }
1221 _ => return Err(format!("unsupported bind opcode 0x{byte:02x}")),
1222 }
1223 }
1224 Ok(out)
1225 }
1226
1227 fn load_dylib_names(bytes: &[u8]) -> Result<Vec<String>, String> {
1228 let header = parse_header(bytes).map_err(|e| e.to_string())?;
1229 let commands = parse_commands(&header, bytes).map_err(|e| e.to_string())?;
1230 Ok(commands
1231 .into_iter()
1232 .filter_map(|cmd| match cmd {
1233 LoadCommand::Dylib(cmd) if cmd.cmd == afs_ld::macho::constants::LC_LOAD_DYLIB => {
1234 Some(cmd.name)
1235 }
1236 _ => None,
1237 })
1238 .collect())
1239 }
1240
1241 #[derive(Clone, Copy)]
1242 struct SectionCase {
1243 segname: &'static str,
1244 sectname: &'static str,
1245 }
1246
1247 #[derive(Clone, Copy)]
1248 enum PageRefKind {
1249 Add,
1250 Load,
1251 }
1252
1253 enum ParityCheck {
1254 ExactSections(&'static [SectionCase]),
1255 PageRef {
1256 section: SectionCase,
1257 site_offset: u64,
1258 target_offset: u64,
1259 kind: PageRefKind,
1260 },
1261 }
1262
1263 struct ParityCase {
1264 name: &'static str,
1265 src: &'static str,
1266 check: ParityCheck,
1267 }
1268
1269 struct ExportParityCase {
1270 name: &'static str,
1271 src: &'static str,
1272 }
1273
1274 struct ClassicLazyParityCase {
1275 name: &'static str,
1276 src: &'static str,
1277 }
1278
1279 struct DirectBindParityCase {
1280 name: &'static str,
1281 dylib_src: &'static str,
1282 main_src: &'static str,
1283 }
1284
1285 fn assert_case_matches_apple_ld(case: &ParityCase, sdk: &str, sdk_ver: &str) -> Result<(), String> {
1286 let obj = scratch(&format!("parity-{}.o", case.name));
1287 let our_out = scratch(&format!("parity-{}-ours.out", case.name));
1288 let apple_out = scratch(&format!("parity-{}-apple.out", case.name));
1289
1290 assemble(case.src, &obj)?;
1291
1292 let opts = LinkOptions {
1293 inputs: vec![obj.clone()],
1294 output: Some(our_out.clone()),
1295 kind: OutputKind::Executable,
1296 ..LinkOptions::default()
1297 };
1298 Linker::run(&opts).map_err(|e| format!("afs-ld link failed for {}: {e}", case.name))?;
1299 apple_link(&obj, &apple_out, "_main", sdk, sdk_ver)?;
1300
1301 let our_bytes = fs::read(&our_out).map_err(|e| format!("read our output: {e}"))?;
1302 let apple_bytes = fs::read(&apple_out).map_err(|e| format!("read apple output: {e}"))?;
1303
1304 match case.check {
1305 ParityCheck::ExactSections(sections) => {
1306 for section in sections {
1307 let (_, ours) = output_section(&our_bytes, section.segname, section.sectname)
1308 .ok_or_else(|| {
1309 format!(
1310 "missing our section {},{}",
1311 section.segname, section.sectname
1312 )
1313 })?;
1314 let (_, theirs) = output_section(&apple_bytes, section.segname, section.sectname)
1315 .ok_or_else(|| {
1316 format!(
1317 "missing apple section {},{}",
1318 section.segname, section.sectname
1319 )
1320 })?;
1321 let diff = diff_macho(&ours, &theirs);
1322 if !diff.is_clean() {
1323 return Err(format!(
1324 "{}: section {},{} diverged from Apple ld: {:#?}",
1325 case.name, section.segname, section.sectname, diff.critical
1326 ));
1327 }
1328 }
1329 }
1330 ParityCheck::PageRef {
1331 section,
1332 site_offset,
1333 target_offset,
1334 kind,
1335 } => {
1336 let (our_addr, our_bytes_sec) =
1337 output_section(&our_bytes, section.segname, section.sectname).ok_or_else(|| {
1338 format!(
1339 "missing our section {},{}",
1340 section.segname, section.sectname
1341 )
1342 })?;
1343 let (apple_addr, apple_bytes_sec) =
1344 output_section(&apple_bytes, section.segname, section.sectname).ok_or_else(
1345 || {
1346 format!(
1347 "missing apple section {},{}",
1348 section.segname, section.sectname
1349 )
1350 },
1351 )?;
1352 let our_target = decode_page_reference(&our_bytes_sec, our_addr, site_offset, &kind)?;
1353 let apple_target =
1354 decode_page_reference(&apple_bytes_sec, apple_addr, site_offset, &kind)?;
1355 let our_offset = our_target - our_addr;
1356 let apple_offset = apple_target - apple_addr;
1357 if our_offset != target_offset || apple_offset != target_offset {
1358 return Err(format!(
1359 "{}: decoded target offset mismatch (ours={our_offset:#x}, apple={apple_offset:#x}, expected={target_offset:#x})",
1360 case.name,
1361 ));
1362 }
1363 }
1364 }
1365
1366 let _ = fs::remove_file(obj);
1367 let _ = fs::remove_file(our_out);
1368 let _ = fs::remove_file(apple_out);
1369 Ok(())
1370 }
1371
1372 fn assert_dylib_export_case_matches_apple_ld(
1373 case: &ExportParityCase,
1374 sdk: &str,
1375 sdk_ver: &str,
1376 ) -> Result<(), String> {
1377 let obj = scratch(&format!("export-parity-{}.o", case.name));
1378 let our_out = scratch(&format!("export-parity-{}-ours.dylib", case.name));
1379 let apple_out = scratch(&format!("export-parity-{}-apple.dylib", case.name));
1380
1381 assemble(case.src, &obj)?;
1382
1383 let opts = LinkOptions {
1384 inputs: vec![obj.clone()],
1385 output: Some(our_out.clone()),
1386 kind: OutputKind::Dylib,
1387 ..LinkOptions::default()
1388 };
1389 Linker::run(&opts).map_err(|e| format!("afs-ld dylib link failed for {}: {e}", case.name))?;
1390 apple_link_dylib_classic(
1391 &obj,
1392 &apple_out,
1393 &format!("@rpath/{}.dylib", case.name),
1394 sdk,
1395 sdk_ver,
1396 )?;
1397
1398 let our_bytes = fs::read(&our_out).map_err(|e| format!("read our output: {e}"))?;
1399 let apple_bytes = fs::read(&apple_out).map_err(|e| format!("read apple output: {e}"))?;
1400 if canonical_export_records(&our_bytes) != canonical_export_records(&apple_bytes) {
1401 return Err(format!(
1402 "{}: canonical export records diverged:\nours={:#?}\napple={:#?}",
1403 case.name,
1404 canonical_export_records(&our_bytes),
1405 canonical_export_records(&apple_bytes)
1406 ));
1407 }
1408 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::WeakBind)
1409 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::WeakBind)
1410 {
1411 return Err(format!(
1412 "{}: weak-bind stream diverged from Apple ld",
1413 case.name
1414 ));
1415 }
1416 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::Export)
1417 .map_err(|e| format!("read our export stream: {e}"))?
1418 .is_empty()
1419 {
1420 return Err(format!("{}: expected non-empty export trie", case.name));
1421 }
1422
1423 let _ = fs::remove_file(obj);
1424 let _ = fs::remove_file(our_out);
1425 let _ = fs::remove_file(apple_out);
1426 Ok(())
1427 }
1428
1429 fn assert_classic_lazy_case_matches_apple_ld(
1430 case: &ClassicLazyParityCase,
1431 sdk: &str,
1432 sdk_ver: &str,
1433 ) -> Result<(), String> {
1434 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
1435 if !tbd.exists() {
1436 return Err(format!("no libSystem.tbd at {}", tbd.display()));
1437 }
1438
1439 let obj = scratch(&format!("classic-lazy-{}.o", case.name));
1440 let our_out = scratch(&format!("classic-lazy-{}-ours.out", case.name));
1441 let apple_out = scratch(&format!("classic-lazy-{}-apple.out", case.name));
1442
1443 assemble(case.src, &obj)?;
1444
1445 let opts = LinkOptions {
1446 inputs: vec![obj.clone(), tbd],
1447 output: Some(our_out.clone()),
1448 kind: OutputKind::Executable,
1449 ..LinkOptions::default()
1450 };
1451 Linker::run(&opts)
1452 .map_err(|e| format!("afs-ld classic-lazy link failed for {}: {e}", case.name))?;
1453 apple_link_classic_lazy(&obj, &apple_out, "_main", sdk, sdk_ver)?;
1454
1455 let our_bytes = fs::read(&our_out).map_err(|e| format!("read our output: {e}"))?;
1456 let apple_bytes = fs::read(&apple_out).map_err(|e| format!("read apple output: {e}"))?;
1457
1458 for (segname, sectname) in [("__TEXT", "__stubs"), ("__TEXT", "__stub_helper")] {
1459 let (_, ours) = output_section(&our_bytes, segname, sectname)
1460 .ok_or_else(|| format!("{}: missing our section {segname},{sectname}", case.name))?;
1461 let (_, theirs) = output_section(&apple_bytes, segname, sectname)
1462 .ok_or_else(|| format!("{}: missing apple section {segname},{sectname}", case.name))?;
1463 let diff = diff_macho(&ours, &theirs);
1464 if !diff.is_clean() {
1465 return Err(format!(
1466 "{}: section {},{} diverged from Apple ld: {:#?}",
1467 case.name, segname, sectname, diff.critical
1468 ));
1469 }
1470 }
1471
1472 if load_dylib_names(&our_bytes).map_err(|e| format!("our dylibs: {e}"))?
1473 != load_dylib_names(&apple_bytes).map_err(|e| format!("apple dylibs: {e}"))?
1474 {
1475 return Err(format!(
1476 "{}: LC_LOAD_DYLIB set diverged from Apple ld",
1477 case.name
1478 ));
1479 }
1480 if segment_flags(&our_bytes, "__DATA_CONST") != segment_flags(&apple_bytes, "__DATA_CONST") {
1481 return Err(format!(
1482 "{}: __DATA_CONST flags diverged from Apple ld",
1483 case.name
1484 ));
1485 }
1486 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::Rebase)
1487 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::Rebase)
1488 {
1489 return Err(format!(
1490 "{}: rebase stream diverged from Apple ld",
1491 case.name
1492 ));
1493 }
1494 if decode_rebase_records(&our_bytes).map_err(|e| format!("our rebases: {e}"))?
1495 != decode_rebase_records(&apple_bytes).map_err(|e| format!("apple rebases: {e}"))?
1496 {
1497 return Err(format!(
1498 "{}: rebase records diverged from Apple ld",
1499 case.name
1500 ));
1501 }
1502 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::Bind)
1503 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::Bind)
1504 {
1505 return Err(format!("{}: bind stream diverged from Apple ld", case.name));
1506 }
1507 if decode_bind_records(&our_bytes, false).map_err(|e| format!("our binds: {e}"))?
1508 != decode_bind_records(&apple_bytes, false).map_err(|e| format!("apple binds: {e}"))?
1509 {
1510 return Err(format!(
1511 "{}: bind records diverged from Apple ld",
1512 case.name
1513 ));
1514 }
1515 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::WeakBind)
1516 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::WeakBind)
1517 {
1518 return Err(format!(
1519 "{}: weak-bind stream diverged from Apple ld",
1520 case.name
1521 ));
1522 }
1523 if dyld_info_export_names(&our_bytes).map_err(|e| format!("our executable exports: {e}"))?
1524 != dyld_info_export_names(&apple_bytes)
1525 .map_err(|e| format!("apple executable exports: {e}"))?
1526 {
1527 return Err(format!(
1528 "{}: executable export trie diverged from Apple ld",
1529 case.name
1530 ));
1531 }
1532 if decode_bind_records(&our_bytes, true).map_err(|e| format!("our lazy binds: {e}"))?
1533 != decode_bind_records(&apple_bytes, true).map_err(|e| format!("apple lazy binds: {e}"))?
1534 {
1535 return Err(format!(
1536 "{}: lazy bind records diverged from Apple ld",
1537 case.name
1538 ));
1539 }
1540 if canonical_lazy_bind_stream(&our_bytes).map_err(|e| format!("our lazy stream: {e}"))?
1541 != canonical_lazy_bind_stream(&apple_bytes)
1542 .map_err(|e| format!("apple lazy stream: {e}"))?
1543 {
1544 return Err(format!(
1545 "{}: canonical lazy-bind stream diverged from Apple ld",
1546 case.name
1547 ));
1548 }
1549 if indirect_symbol_table(&our_bytes) != indirect_symbol_table(&apple_bytes) {
1550 return Err(format!(
1551 "{}: indirect symbol table diverged from Apple ld",
1552 case.name
1553 ));
1554 }
1555
1556 let _ = fs::remove_file(obj);
1557 let _ = fs::remove_file(our_out);
1558 let _ = fs::remove_file(apple_out);
1559 Ok(())
1560 }
1561
1562 fn assert_direct_bind_case_matches_apple_ld(
1563 case: &DirectBindParityCase,
1564 sdk: &str,
1565 sdk_ver: &str,
1566 ) -> Result<(), String> {
1567 let dylib = scratch(&format!("direct-bind-{}.dylib", case.name));
1568 let obj = scratch(&format!("direct-bind-{}.o", case.name));
1569 let our_out = scratch(&format!("direct-bind-{}-ours.out", case.name));
1570 let apple_out = scratch(&format!("direct-bind-{}-apple.out", case.name));
1571
1572 compile_dylib_c(case.dylib_src, &dylib)?;
1573 compile_c(case.main_src, &obj)?;
1574
1575 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
1576 if !tbd.exists() {
1577 return Err(format!("no libSystem.tbd at {}", tbd.display()));
1578 }
1579
1580 let opts = LinkOptions {
1581 inputs: vec![obj.clone(), tbd, dylib.clone()],
1582 output: Some(our_out.clone()),
1583 kind: OutputKind::Executable,
1584 ..LinkOptions::default()
1585 };
1586 Linker::run(&opts)
1587 .map_err(|e| format!("afs-ld direct-bind link failed for {}: {e}", case.name))?;
1588 let apple = Command::new("xcrun")
1589 .args([
1590 "ld",
1591 "-arch",
1592 "arm64",
1593 "-platform_version",
1594 "macos",
1595 sdk_ver,
1596 sdk_ver,
1597 "-syslibroot",
1598 sdk,
1599 "-no_fixup_chains",
1600 "-lSystem",
1601 "-e",
1602 "_main",
1603 "-o",
1604 ])
1605 .arg(&apple_out)
1606 .arg(&obj)
1607 .arg(&dylib)
1608 .output()
1609 .map_err(|e| format!("spawn xcrun ld: {e}"))?;
1610 if !apple.status.success() {
1611 return Err(format!(
1612 "xcrun ld failed for {}: {}",
1613 case.name,
1614 String::from_utf8_lossy(&apple.stderr)
1615 ));
1616 }
1617
1618 let our_bytes = fs::read(&our_out).map_err(|e| format!("read our output: {e}"))?;
1619 let apple_bytes = fs::read(&apple_out).map_err(|e| format!("read apple output: {e}"))?;
1620
1621 if load_dylib_names(&our_bytes).map_err(|e| format!("our dylibs: {e}"))?
1622 != load_dylib_names(&apple_bytes).map_err(|e| format!("apple dylibs: {e}"))?
1623 {
1624 return Err(format!(
1625 "{}: LC_LOAD_DYLIB set diverged from Apple ld",
1626 case.name
1627 ));
1628 }
1629 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::Rebase)
1630 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::Rebase)
1631 {
1632 return Err(format!(
1633 "{}: rebase stream diverged from Apple ld",
1634 case.name
1635 ));
1636 }
1637 if decode_rebase_records(&our_bytes).map_err(|e| format!("our rebases: {e}"))?
1638 != decode_rebase_records(&apple_bytes).map_err(|e| format!("apple rebases: {e}"))?
1639 {
1640 return Err(format!(
1641 "{}: rebase records diverged from Apple ld",
1642 case.name
1643 ));
1644 }
1645 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::Bind)
1646 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::Bind)
1647 {
1648 return Err(format!("{}: bind stream diverged from Apple ld", case.name));
1649 }
1650 if decode_bind_records(&our_bytes, false).map_err(|e| format!("our binds: {e}"))?
1651 != decode_bind_records(&apple_bytes, false).map_err(|e| format!("apple binds: {e}"))?
1652 {
1653 return Err(format!(
1654 "{}: bind records diverged from Apple ld",
1655 case.name
1656 ));
1657 }
1658 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::WeakBind)
1659 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::WeakBind)
1660 {
1661 return Err(format!(
1662 "{}: weak-bind stream diverged from Apple ld",
1663 case.name
1664 ));
1665 }
1666 if dyld_info_export_names(&our_bytes).map_err(|e| format!("our executable exports: {e}"))?
1667 != dyld_info_export_names(&apple_bytes)
1668 .map_err(|e| format!("apple executable exports: {e}"))?
1669 {
1670 return Err(format!(
1671 "{}: executable export trie diverged from Apple ld",
1672 case.name
1673 ));
1674 }
1675 if dyld_info_stream(&our_bytes, DyldInfoStreamKind::LazyBind)
1676 != dyld_info_stream(&apple_bytes, DyldInfoStreamKind::LazyBind)
1677 {
1678 return Err(format!(
1679 "{}: lazy-bind stream diverged from Apple ld",
1680 case.name
1681 ));
1682 }
1683
1684 let _ = fs::remove_file(dylib);
1685 let _ = fs::remove_file(obj);
1686 let _ = fs::remove_file(our_out);
1687 let _ = fs::remove_file(apple_out);
1688 Ok(())
1689 }
1690
1691 fn decode_page_reference(
1692 bytes: &[u8],
1693 section_addr: u64,
1694 site_offset: u64,
1695 kind: &PageRefKind,
1696 ) -> Result<u64, String> {
1697 let start = site_offset as usize;
1698 let adrp = read_insn(bytes, start)?;
1699 let second = read_insn(bytes, start + 4)?;
1700 let place = section_addr + site_offset;
1701 let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
1702 let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
1703 let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
1704 let adrp_base = ((place as i64) & !0xfff) + (adrp_pages << 12);
1705 let low = match kind {
1706 PageRefKind::Add => ((second >> 10) & 0xfff) as u64,
1707 PageRefKind::Load => {
1708 let shift = ((second >> 30) & 0b11) as u64;
1709 (((second >> 10) & 0xfff) as u64) << shift
1710 }
1711 };
1712 Ok((adrp_base as u64) + low)
1713 }
1714
1715 fn decode_branch_target(bytes: &[u8], section_addr: u64, site_offset: u64) -> Result<u64, String> {
1716 let insn = read_insn(bytes, site_offset as usize)?;
1717 let imm26 = (insn & 0x03ff_ffff) as i64;
1718 let imm = sign_extend_26(imm26) << 2;
1719 Ok(section_addr
1720 .wrapping_add(site_offset)
1721 .wrapping_add_signed(imm))
1722 }
1723
1724 fn read_insn(bytes: &[u8], start: usize) -> Result<u32, String> {
1725 let end = start + 4;
1726 let slice = bytes
1727 .get(start..end)
1728 .ok_or_else(|| format!("instruction read OOB at 0x{start:x}"))?;
1729 Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
1730 }
1731
1732 fn sign_extend_26(value: i64) -> i64 {
1733 if value & (1 << 25) != 0 {
1734 value | !0x03ff_ffff
1735 } else {
1736 value
1737 }
1738 }
1739
1740 #[test]
1741 fn linker_run_emits_non_empty_executable_from_real_object() {
1742 if !have_xcrun() || !have_tool("codesign") {
1743 eprintln!("skipping: xcrun as or codesign unavailable");
1744 return;
1745 }
1746 let Some(sdk) = sdk_path() else {
1747 eprintln!("skipping: xcrun --show-sdk-path unavailable");
1748 return;
1749 };
1750 let Some(sdk_ver) = sdk_version() else {
1751 eprintln!("skipping: xcrun --show-sdk-version unavailable");
1752 return;
1753 };
1754
1755 let obj = scratch("main.o");
1756 let out = scratch("a.out");
1757 let apple_out = scratch("a-apple.out");
1758 let src = r#"
1759 .section __TEXT,__text,regular,pure_instructions
1760 .globl _main
1761 _main:
1762 mov x0, #0
1763 ret
1764 .subsections_via_symbols
1765 "#;
1766 if let Err(e) = assemble(src, &obj) {
1767 eprintln!("skipping: assemble failed: {e}");
1768 return;
1769 }
1770
1771 let opts = LinkOptions {
1772 inputs: vec![obj.clone()],
1773 output: Some(out.clone()),
1774 kind: OutputKind::Executable,
1775 ..LinkOptions::default()
1776 };
1777 Linker::run(&opts).unwrap();
1778 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
1779
1780 let bytes = fs::read(&out).unwrap();
1781 let apple_bytes = fs::read(&apple_out).unwrap();
1782 let header = parse_header(&bytes).unwrap();
1783 let commands = parse_commands(&header, &bytes).unwrap();
1784 let mut text_size = 0u64;
1785 let mut has_dylinker = false;
1786 let mut has_uuid = false;
1787 let mut has_source_version = false;
1788 for cmd in commands {
1789 match cmd {
1790 LoadCommand::Segment64(seg) => {
1791 for section in seg.sections {
1792 if section.sectname_str() == "__text" {
1793 text_size = section.size;
1794 }
1795 }
1796 }
1797 LoadCommand::Raw { cmd, data, .. }
1798 if cmd == afs_ld::macho::constants::LC_LOAD_DYLINKER =>
1799 {
1800 has_dylinker = data
1801 .windows(b"/usr/lib/dyld\0".len())
1802 .any(|window| window == b"/usr/lib/dyld\0");
1803 }
1804 LoadCommand::Raw { cmd, data, .. } if cmd == afs_ld::macho::constants::LC_UUID => {
1805 has_uuid = data.len() == 16 && data.iter().any(|byte| *byte != 0);
1806 }
1807 LoadCommand::Raw { cmd, .. } if cmd == afs_ld::macho::constants::LC_SOURCE_VERSION => {
1808 has_source_version = true;
1809 }
1810 _ => {}
1811 }
1812 }
1813 assert!(text_size > 0, "expected non-empty __text output");
1814 assert!(
1815 has_dylinker,
1816 "expected LC_LOAD_DYLINKER in executable output"
1817 );
1818 assert!(has_uuid, "expected LC_UUID in executable output");
1819 assert!(
1820 has_source_version,
1821 "expected LC_SOURCE_VERSION in executable output"
1822 );
1823 let our_cmds: Vec<u32> = command_ids(&bytes)
1824 .into_iter()
1825 .filter(|cmd| *cmd != afs_ld::macho::constants::LC_LOAD_DYLIB)
1826 .collect();
1827 let apple_cmds: Vec<u32> = command_ids(&apple_bytes)
1828 .into_iter()
1829 .filter(|cmd| *cmd != afs_ld::macho::constants::LC_LOAD_DYLIB)
1830 .collect();
1831 assert_eq!(our_cmds, apple_cmds);
1832 assert!(
1833 fs::metadata(&out).unwrap().permissions().mode() & 0o111 != 0,
1834 "expected executable output mode"
1835 );
1836 let verify = Command::new("codesign")
1837 .arg("-v")
1838 .arg(&out)
1839 .output()
1840 .unwrap();
1841 assert!(
1842 verify.status.success(),
1843 "codesign verify failed: {}",
1844 String::from_utf8_lossy(&verify.stderr)
1845 );
1846 let status = Command::new(&out).status().unwrap();
1847 assert_eq!(status.code(), Some(0), "expected executable to exit 0");
1848
1849 let _ = fs::remove_file(obj);
1850 let _ = fs::remove_file(out);
1851 let _ = fs::remove_file(apple_out);
1852 }
1853
1854 #[test]
1855 fn linker_run_emits_minimal_dylib_from_real_object() {
1856 if !have_xcrun() {
1857 eprintln!("skipping: xcrun as unavailable");
1858 return;
1859 }
1860
1861 let obj = scratch("lib.o");
1862 let out = scratch("libtiny.dylib");
1863 let src = r#"
1864 .section __TEXT,__text,regular,pure_instructions
1865 .globl _exported
1866 _exported:
1867 ret
1868 .subsections_via_symbols
1869 "#;
1870 if let Err(e) = assemble(src, &obj) {
1871 eprintln!("skipping: assemble failed: {e}");
1872 return;
1873 }
1874
1875 let opts = LinkOptions {
1876 inputs: vec![obj.clone()],
1877 output: Some(out.clone()),
1878 kind: OutputKind::Dylib,
1879 ..LinkOptions::default()
1880 };
1881 Linker::run(&opts).unwrap();
1882
1883 let bytes = fs::read(&out).unwrap();
1884 let header = parse_header(&bytes).unwrap();
1885 let commands = parse_commands(&header, &bytes).unwrap();
1886 let dyld_info = commands.iter().find_map(|cmd| match cmd {
1887 LoadCommand::DyldInfoOnly(cmd) => Some(*cmd),
1888 _ => None,
1889 });
1890 assert_eq!(header.filetype, afs_ld::macho::constants::MH_DYLIB);
1891 assert!(commands.iter().any(
1892 |cmd| matches!(cmd, LoadCommand::Dylib(d) if d.cmd == afs_ld::macho::constants::LC_ID_DYLIB)
1893 ));
1894 let dyld_info = dyld_info.expect("expected LC_DYLD_INFO_ONLY in dylib output");
1895 assert!(dyld_info.export_size > 0, "expected non-empty export trie");
1896
1897 let dylib = DylibFile::parse(&out, &bytes).unwrap();
1898 let mut exports = dylib.exports.entries().unwrap();
1899 exports.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
1900 assert!(
1901 exports.iter().any(|entry| entry.name == "_exported"),
1902 "expected _exported in export trie, got {:?}",
1903 exports
1904 .iter()
1905 .map(|entry| entry.name.as_str())
1906 .collect::<Vec<_>>()
1907 );
1908
1909 let _ = fs::remove_file(obj);
1910 let _ = fs::remove_file(out);
1911 }
1912
1913 #[test]
1914 fn dylib_export_surfaces_match_apple_ld() {
1915 if !have_xcrun() || !have_xcrun_tool("ld") {
1916 eprintln!("skipping: xcrun as/ld unavailable");
1917 return;
1918 }
1919 let Some(sdk) = sdk_path() else {
1920 eprintln!("skipping: xcrun --show-sdk-path unavailable");
1921 return;
1922 };
1923 let Some(sdk_ver) = sdk_version() else {
1924 eprintln!("skipping: xcrun --show-sdk-version unavailable");
1925 return;
1926 };
1927
1928 let case = ExportParityCase {
1929 name: "export-parity",
1930 src: r#"
1931 .section __TEXT,__text,regular,pure_instructions
1932 .globl _exported
1933 _exported:
1934 ret
1935 .subsections_via_symbols
1936 "#,
1937 };
1938 assert_dylib_export_case_matches_apple_ld(&case, &sdk, &sdk_ver).unwrap();
1939 }
1940
1941 #[test]
1942 fn dylib_export_surfaces_match_apple_ld_with_shared_prefixes() {
1943 if !have_xcrun() || !have_xcrun_tool("ld") {
1944 eprintln!("skipping: xcrun as/ld unavailable");
1945 return;
1946 }
1947 let Some(sdk) = sdk_path() else {
1948 eprintln!("skipping: xcrun --show-sdk-path unavailable");
1949 return;
1950 };
1951 let Some(sdk_ver) = sdk_version() else {
1952 eprintln!("skipping: xcrun --show-sdk-version unavailable");
1953 return;
1954 };
1955
1956 let case = ExportParityCase {
1957 name: "export-prefix-parity",
1958 src: r#"
1959 .section __TEXT,__text,regular,pure_instructions
1960 .globl _alpha
1961 _alpha:
1962 ret
1963 .globl _alphabet
1964 _alphabet:
1965 ret
1966 .globl _alphanumeric
1967 _alphanumeric:
1968 ret
1969 .subsections_via_symbols
1970 "#,
1971 };
1972 assert_dylib_export_case_matches_apple_ld(&case, &sdk, &sdk_ver).unwrap();
1973 }
1974
1975 #[test]
1976 fn dylib_export_surfaces_match_apple_ld_across_fixture_matrix() {
1977 if !have_xcrun() || !have_xcrun_tool("ld") {
1978 eprintln!("skipping: xcrun as/ld unavailable");
1979 return;
1980 }
1981 let Some(sdk) = sdk_path() else {
1982 eprintln!("skipping: xcrun --show-sdk-path unavailable");
1983 return;
1984 };
1985 let Some(sdk_ver) = sdk_version() else {
1986 eprintln!("skipping: xcrun --show-sdk-version unavailable");
1987 return;
1988 };
1989
1990 let cases = [
1991 ExportParityCase {
1992 name: "export-ordering",
1993 src: r#"
1994 .section __TEXT,__text,regular,pure_instructions
1995 .globl _zeta
1996 _zeta:
1997 ret
1998 .globl _alpha
1999 _alpha:
2000 ret
2001 .globl _middle
2002 _middle:
2003 ret
2004 .subsections_via_symbols
2005 "#,
2006 },
2007 ExportParityCase {
2008 name: "export-text-data",
2009 src: r#"
2010 .section __TEXT,__text,regular,pure_instructions
2011 .globl _code_symbol
2012 _code_symbol:
2013 ret
2014 .section __DATA,__data
2015 .p2align 3
2016 .globl _data_symbol
2017 _data_symbol:
2018 .quad 0x1234
2019 .globl _more_data
2020 _more_data:
2021 .long 7
2022 .subsections_via_symbols
2023 "#,
2024 },
2025 ExportParityCase {
2026 name: "export-text-const",
2027 src: r#"
2028 .section __TEXT,__text,regular,pure_instructions
2029 .globl _entry
2030 _entry:
2031 ret
2032 .section __TEXT,__const
2033 .p2align 3
2034 .globl _ro_value
2035 _ro_value:
2036 .quad 0xfeedface
2037 .subsections_via_symbols
2038 "#,
2039 },
2040 ExportParityCase {
2041 name: "export-bss",
2042 src: r#"
2043 .section __TEXT,__text,regular,pure_instructions
2044 .globl _touch
2045 _touch:
2046 ret
2047 .zerofill __DATA,__bss,_global_bss,16,3
2048 .subsections_via_symbols
2049 "#,
2050 },
2051 ExportParityCase {
2052 name: "export-prefix-fanout",
2053 src: r#"
2054 .section __TEXT,__text,regular,pure_instructions
2055 .globl _pre
2056 _pre:
2057 ret
2058 .globl _prefix
2059 _prefix:
2060 ret
2061 .globl _prefix_long
2062 _prefix_long:
2063 ret
2064 .globl _prefix_lone
2065 _prefix_lone:
2066 ret
2067 .subsections_via_symbols
2068 "#,
2069 },
2070 ExportParityCase {
2071 name: "export-shared-data-prefix",
2072 src: r#"
2073 .section __DATA,__data
2074 .p2align 3
2075 .globl _alpha_data
2076 _alpha_data:
2077 .quad 1
2078 .globl _alphabet_data
2079 _alphabet_data:
2080 .quad 2
2081 .globl _alphanumeric_data
2082 _alphanumeric_data:
2083 .quad 3
2084 .subsections_via_symbols
2085 "#,
2086 },
2087 ];
2088
2089 let mut failures = Vec::new();
2090 for case in &cases {
2091 if let Err(err) = assert_dylib_export_case_matches_apple_ld(case, &sdk, &sdk_ver) {
2092 failures.push(err);
2093 }
2094 }
2095
2096 assert!(
2097 failures.is_empty(),
2098 "Apple ld dylib export parity failures ({} cases):\n{}",
2099 failures.len(),
2100 failures.join("\n\n")
2101 );
2102 }
2103
2104 #[test]
2105 fn linker_run_reports_unresolved_symbol() {
2106 if !have_xcrun() {
2107 eprintln!("skipping: xcrun as unavailable");
2108 return;
2109 }
2110
2111 let obj = scratch("missing.o");
2112 let src = r#"
2113 .section __TEXT,__text,regular,pure_instructions
2114 .globl _main
2115 _main:
2116 bl _missing
2117 ret
2118 .subsections_via_symbols
2119 "#;
2120 if let Err(e) = assemble(src, &obj) {
2121 eprintln!("skipping: assemble failed: {e}");
2122 return;
2123 }
2124
2125 let opts = LinkOptions {
2126 inputs: vec![obj.clone()],
2127 output: Some(scratch("missing.out")),
2128 kind: OutputKind::Executable,
2129 ..LinkOptions::default()
2130 };
2131 let err = Linker::run(&opts).unwrap_err();
2132 match err {
2133 LinkError::UndefinedSymbols(msg) => {
2134 assert!(msg.contains("undefined symbol: _missing"), "{msg}");
2135 }
2136 other => panic!("expected UndefinedSymbols, got {other:?}"),
2137 }
2138
2139 let _ = fs::remove_file(obj);
2140 }
2141
2142 #[test]
2143 fn linker_run_reports_duplicate_from_fetched_archive_member() {
2144 if !have_xcrun() {
2145 eprintln!("skipping: xcrun as unavailable");
2146 return;
2147 }
2148
2149 let main_obj = scratch("dup-main.o");
2150 let dup_obj = scratch("dup-member.o");
2151 let archive = scratch("dup.a");
2152
2153 let main_src = r#"
2154 .section __TEXT,__text,regular,pure_instructions
2155 .globl _main
2156 .globl _dup
2157 _main:
2158 bl _archive_sym
2159 ret
2160 _dup:
2161 ret
2162 .subsections_via_symbols
2163 "#;
2164 let dup_src = r#"
2165 .section __TEXT,__text,regular,pure_instructions
2166 .globl _archive_sym
2167 .globl _dup
2168 _archive_sym:
2169 ret
2170 _dup:
2171 ret
2172 .subsections_via_symbols
2173 "#;
2174
2175 for (src, out) in [(&main_src, &main_obj), (&dup_src, &dup_obj)] {
2176 if let Err(e) = assemble(src, out) {
2177 eprintln!("skipping: assemble failed: {e}");
2178 return;
2179 }
2180 }
2181
2182 let ar = Command::new("ar")
2183 .arg("rcs")
2184 .arg(&archive)
2185 .arg(&dup_obj)
2186 .output()
2187 .unwrap();
2188 if !ar.status.success() {
2189 eprintln!(
2190 "skipping: ar failed: {}",
2191 String::from_utf8_lossy(&ar.stderr)
2192 );
2193 return;
2194 }
2195
2196 let opts = LinkOptions {
2197 inputs: vec![main_obj.clone(), archive.clone()],
2198 output: Some(scratch("dup.out")),
2199 kind: OutputKind::Executable,
2200 ..LinkOptions::default()
2201 };
2202 let err = Linker::run(&opts).unwrap_err();
2203 match err {
2204 LinkError::DuplicateSymbols(msg) => {
2205 assert!(msg.contains("duplicate symbol _dup"), "{msg}");
2206 }
2207 other => panic!("expected DuplicateSymbols, got {other:?}"),
2208 }
2209
2210 let _ = fs::remove_file(main_obj);
2211 let _ = fs::remove_file(dup_obj);
2212 let _ = fs::remove_file(archive);
2213 }
2214
2215 #[test]
2216 fn fetched_archive_member_undefined_reports_member_referrer() {
2217 if !have_xcrun() {
2218 eprintln!("skipping: xcrun as unavailable");
2219 return;
2220 }
2221
2222 let main_obj = scratch("member-main.o");
2223 let member_obj = scratch("member-undef.o");
2224 let archive = scratch("member.a");
2225
2226 let main_src = r#"
2227 .section __TEXT,__text,regular,pure_instructions
2228 .globl _main
2229 _main:
2230 bl _archive_sym
2231 ret
2232 .subsections_via_symbols
2233 "#;
2234 let member_src = r#"
2235 .section __TEXT,__text,regular,pure_instructions
2236 .globl _archive_sym
2237 _archive_sym:
2238 bl _missing_from_member
2239 ret
2240 .subsections_via_symbols
2241 "#;
2242
2243 for (src, out) in [(&main_src, &main_obj), (&member_src, &member_obj)] {
2244 if let Err(e) = assemble(src, out) {
2245 eprintln!("skipping: assemble failed: {e}");
2246 return;
2247 }
2248 }
2249
2250 let ar = Command::new("ar")
2251 .arg("rcs")
2252 .arg(&archive)
2253 .arg(&member_obj)
2254 .output()
2255 .unwrap();
2256 if !ar.status.success() {
2257 eprintln!(
2258 "skipping: ar failed: {}",
2259 String::from_utf8_lossy(&ar.stderr)
2260 );
2261 return;
2262 }
2263
2264 let opts = LinkOptions {
2265 inputs: vec![main_obj.clone(), archive.clone()],
2266 output: Some(scratch("member.out")),
2267 kind: OutputKind::Executable,
2268 ..LinkOptions::default()
2269 };
2270 let err = Linker::run(&opts).unwrap_err();
2271 match err {
2272 LinkError::UndefinedSymbols(msg) => {
2273 assert!(
2274 msg.contains("undefined symbol: _missing_from_member"),
2275 "{msg}"
2276 );
2277 assert!(
2278 msg.contains(&format!("referenced by {}(", archive.display())),
2279 "expected archive-member referrer in:\n{msg}"
2280 );
2281 }
2282 other => panic!("expected UndefinedSymbols, got {other:?}"),
2283 }
2284
2285 let _ = fs::remove_file(main_obj);
2286 let _ = fs::remove_file(member_obj);
2287 let _ = fs::remove_file(archive);
2288 }
2289
2290 #[test]
2291 fn linker_run_carries_tbd_inputs_into_load_commands() {
2292 if !have_xcrun() {
2293 eprintln!("skipping: xcrun unavailable");
2294 return;
2295 }
2296 let Some(sdk) = sdk_path() else {
2297 eprintln!("skipping: xcrun --show-sdk-path unavailable");
2298 return;
2299 };
2300 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
2301 if !tbd.exists() {
2302 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
2303 return;
2304 }
2305
2306 let obj = scratch("tbd-main.o");
2307 let out = scratch("tbd-a.out");
2308 let src = r#"
2309 .section __TEXT,__text,regular,pure_instructions
2310 .globl _main
2311 _main:
2312 mov x0, #0
2313 ret
2314 .subsections_via_symbols
2315 "#;
2316 if let Err(e) = assemble(src, &obj) {
2317 eprintln!("skipping: assemble failed: {e}");
2318 return;
2319 }
2320
2321 let opts = LinkOptions {
2322 inputs: vec![obj.clone(), tbd.clone()],
2323 output: Some(out.clone()),
2324 kind: OutputKind::Executable,
2325 ..LinkOptions::default()
2326 };
2327 Linker::run(&opts).unwrap();
2328
2329 let bytes = fs::read(&out).unwrap();
2330 let header = parse_header(&bytes).unwrap();
2331 let commands = parse_commands(&header, &bytes).unwrap();
2332 assert!(
2333 commands.iter().any(|cmd| matches!(
2334 cmd,
2335 LoadCommand::Dylib(d) if d.cmd == afs_ld::macho::constants::LC_LOAD_DYLIB
2336 )),
2337 "expected at least one LC_LOAD_DYLIB in output"
2338 );
2339
2340 let _ = fs::remove_file(obj);
2341 let _ = fs::remove_file(out);
2342 }
2343
2344 #[test]
2345 fn linker_run_handles_non_standard_segment_without_panicking() {
2346 if !have_xcrun() {
2347 eprintln!("skipping: xcrun as unavailable");
2348 return;
2349 }
2350
2351 let obj = scratch("custom-segment.o");
2352 let out = scratch("custom-segment.out");
2353 let src = r#"
2354 .section __FOO,__bar
2355 .globl _custom
2356 _custom:
2357 .quad 1
2358 .subsections_via_symbols
2359 "#;
2360 if let Err(e) = assemble(src, &obj) {
2361 eprintln!("skipping: assemble failed: {e}");
2362 return;
2363 }
2364
2365 let opts = LinkOptions {
2366 inputs: vec![obj.clone()],
2367 output: Some(out.clone()),
2368 kind: OutputKind::Executable,
2369 ..LinkOptions::default()
2370 };
2371 Linker::run(&opts).unwrap();
2372
2373 let bytes = fs::read(&out).unwrap();
2374 let header = parse_header(&bytes).unwrap();
2375 let commands = parse_commands(&header, &bytes).unwrap();
2376 assert!(commands.iter().any(|cmd| match cmd {
2377 LoadCommand::Segment64(seg) => seg.segname_str() == "__FOO",
2378 _ => false,
2379 }));
2380
2381 let _ = fs::remove_file(obj);
2382 let _ = fs::remove_file(out);
2383 }
2384
2385 #[test]
2386 fn linker_run_uses_requested_entry_symbol() {
2387 if !have_xcrun() {
2388 eprintln!("skipping: xcrun as unavailable");
2389 return;
2390 }
2391
2392 let obj = scratch("entry.o");
2393 let out = scratch("entry.out");
2394 let src = r#"
2395 .section __TEXT,__text,regular,pure_instructions
2396 .globl _main
2397 _main:
2398 ret
2399 .globl _alt
2400 _alt:
2401 mov x0, #1
2402 ret
2403 .subsections_via_symbols
2404 "#;
2405 if let Err(e) = assemble(src, &obj) {
2406 eprintln!("skipping: assemble failed: {e}");
2407 return;
2408 }
2409
2410 let opts = LinkOptions {
2411 inputs: vec![obj.clone()],
2412 output: Some(out.clone()),
2413 entry: Some("_alt".into()),
2414 kind: OutputKind::Executable,
2415 ..LinkOptions::default()
2416 };
2417 Linker::run(&opts).unwrap();
2418
2419 let bytes = fs::read(&out).unwrap();
2420 let header = parse_header(&bytes).unwrap();
2421 let commands = parse_commands(&header, &bytes).unwrap();
2422 let mut text_offset = None;
2423 let mut main_entryoff = None;
2424 for cmd in commands {
2425 match cmd {
2426 LoadCommand::Segment64(seg) => {
2427 for section in seg.sections {
2428 if section.sectname_str() == "__text" {
2429 text_offset = Some(section.offset as u64);
2430 }
2431 }
2432 }
2433 LoadCommand::Raw { cmd, data, .. } if cmd == afs_ld::macho::constants::LC_MAIN => {
2434 let mut buf = [0u8; 8];
2435 buf.copy_from_slice(&data[0..8]);
2436 main_entryoff = Some(u64::from_le_bytes(buf));
2437 }
2438 _ => {}
2439 }
2440 }
2441
2442 let text_offset = text_offset.expect("text section offset");
2443 let main_entryoff = main_entryoff.expect("LC_MAIN entryoff");
2444 assert!(
2445 main_entryoff > text_offset,
2446 "expected custom entry to land after start of __text: text={text_offset}, entry={main_entryoff}"
2447 );
2448
2449 let _ = fs::remove_file(obj);
2450 let _ = fs::remove_file(out);
2451 }
2452
2453 #[test]
2454 fn linker_run_defaults_entry_to_main_symbol() {
2455 if !have_xcrun() {
2456 eprintln!("skipping: xcrun as unavailable");
2457 return;
2458 }
2459
2460 let obj = scratch("default-entry.o");
2461 let out = scratch("default-entry.out");
2462 let src = r#"
2463 .section __TEXT,__text,regular,pure_instructions
2464 .globl _helper
2465 _helper:
2466 mov w0, #7
2467 ret
2468 .globl _main
2469 _main:
2470 mov w0, #0
2471 ret
2472 .subsections_via_symbols
2473 "#;
2474 if let Err(e) = assemble(src, &obj) {
2475 eprintln!("skipping: assemble failed: {e}");
2476 return;
2477 }
2478
2479 let opts = LinkOptions {
2480 inputs: vec![obj.clone()],
2481 output: Some(out.clone()),
2482 kind: OutputKind::Executable,
2483 ..LinkOptions::default()
2484 };
2485 Linker::run(&opts).unwrap();
2486
2487 let status = Command::new(&out).status().unwrap();
2488 assert_eq!(
2489 status.code(),
2490 Some(0),
2491 "default executable entry should prefer _main over the first text atom"
2492 );
2493
2494 let _ = fs::remove_file(obj);
2495 let _ = fs::remove_file(out);
2496 }
2497
2498 #[test]
2499 fn linker_run_applies_core_arm64_relocations() {
2500 if !have_xcrun() {
2501 eprintln!("skipping: xcrun as unavailable");
2502 return;
2503 }
2504
2505 let obj = scratch("relocs.o");
2506 let out = scratch("relocs.out");
2507 let src = r#"
2508 .section __TEXT,__text,regular,pure_instructions
2509 .globl _main
2510 .globl _helper
2511 _main:
2512 adrp x0, _target@PAGE
2513 add x0, x0, _target@PAGEOFF
2514 bl _helper
2515 ret
2516 _helper:
2517 ret
2518
2519 .section __DATA,__data
2520 .p2align 3
2521 _target:
2522 .quad _helper
2523
2524 .section __TEXT,__const
2525 .p2align 3
2526 _delta:
2527 .quad _helper - _main
2528 .subsections_via_symbols
2529 "#;
2530 if let Err(e) = assemble(src, &obj) {
2531 eprintln!("skipping: assemble failed: {e}");
2532 return;
2533 }
2534
2535 let opts = LinkOptions {
2536 inputs: vec![obj.clone()],
2537 output: Some(out.clone()),
2538 kind: OutputKind::Executable,
2539 ..LinkOptions::default()
2540 };
2541 Linker::run(&opts).unwrap();
2542
2543 let bytes = fs::read(&out).unwrap();
2544 let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").expect("text section");
2545 let (data_addr, data) = output_section(&bytes, "__DATA", "__data").expect("data section");
2546 let (_, cdata) = output_section(&bytes, "__TEXT", "__const").expect("const section");
2547
2548 let adrp = u32::from_le_bytes(text[0..4].try_into().unwrap());
2549 let add = u32::from_le_bytes(text[4..8].try_into().unwrap());
2550 let branch = u32::from_le_bytes(text[8..12].try_into().unwrap());
2551 let data_ptr = u64::from_le_bytes(data[0..8].try_into().unwrap());
2552 let delta = u64::from_le_bytes(cdata[0..8].try_into().unwrap());
2553
2554 let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
2555 let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
2556 let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
2557 let adrp_base = ((text_addr as i64) & !0xfff) + (adrp_pages << 12);
2558 let add_imm = ((add >> 10) & 0xfff) as u64;
2559 let reconstructed_target = (adrp_base as u64) + add_imm;
2560
2561 assert_eq!(
2562 reconstructed_target, data_addr,
2563 "ADRP+ADD should resolve _target"
2564 );
2565 assert_eq!(
2566 branch & 0x03ff_ffff,
2567 0x2,
2568 "BL should branch forward 8 bytes"
2569 );
2570 assert_eq!(
2571 data_ptr,
2572 text_addr + 16,
2573 ".quad _helper should point at helper"
2574 );
2575 assert_eq!(delta, 16, "_helper - _main should fold through SUBTRACTOR");
2576
2577 let _ = fs::remove_file(obj);
2578 let _ = fs::remove_file(out);
2579 }
2580
2581 fn sign_extend_21(value: i64) -> i64 {
2582 if value & (1 << 20) != 0 {
2583 value | !0x1f_ffff
2584 } else {
2585 value
2586 }
2587 }
2588
2589 #[test]
2590 fn linker_run_applies_scaled_pageoff12_for_ldr_x() {
2591 if !have_xcrun() {
2592 eprintln!("skipping: xcrun as unavailable");
2593 return;
2594 }
2595
2596 let obj = scratch("scaled-ldr.o");
2597 let out = scratch("scaled-ldr.out");
2598 let src = r#"
2599 .section __TEXT,__text,regular,pure_instructions
2600 .globl _main
2601 _main:
2602 adrp x0, _target@PAGE
2603 ldr x1, [x0, _target@PAGEOFF]
2604 ret
2605
2606 .section __DATA,__data
2607 .space 0x3f8
2608 .p2align 3
2609 .globl _target
2610 _target:
2611 .quad 0x1122334455667788
2612 .subsections_via_symbols
2613 "#;
2614 if let Err(e) = assemble(src, &obj) {
2615 eprintln!("skipping: assemble failed: {e}");
2616 return;
2617 }
2618
2619 let opts = LinkOptions {
2620 inputs: vec![obj.clone()],
2621 output: Some(out.clone()),
2622 kind: OutputKind::Executable,
2623 ..LinkOptions::default()
2624 };
2625 Linker::run(&opts).unwrap();
2626
2627 let bytes = fs::read(&out).unwrap();
2628 let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").expect("text section");
2629 let (data_addr, data) = output_section(&bytes, "__DATA", "__data").expect("data section");
2630
2631 let adrp = u32::from_le_bytes(text[0..4].try_into().unwrap());
2632 let ldr = u32::from_le_bytes(text[4..8].try_into().unwrap());
2633 let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
2634 let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
2635 let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
2636 let adrp_base = ((text_addr as i64) & !0xfff) + (adrp_pages << 12);
2637 let ldr_shift = ((ldr >> 30) & 0b11) as u64;
2638 let ldr_imm = ((ldr >> 10) & 0xfff) as u64;
2639 let reconstructed_target = (adrp_base as u64) + (ldr_imm << ldr_shift);
2640
2641 assert_eq!(ldr_shift, 3, "expected 64-bit LDR scale");
2642 assert_eq!(ldr_imm, 0x7f, "scaled imm12 should store 0x3f8 >> 3");
2643 assert_eq!(reconstructed_target, data_addr + 0x3f8);
2644 assert_eq!(
2645 u64::from_le_bytes(data[0x3f8..0x400].try_into().unwrap()),
2646 0x1122334455667788
2647 );
2648
2649 let _ = fs::remove_file(obj);
2650 let _ = fs::remove_file(out);
2651 }
2652
2653 #[test]
2654 fn relocated_sections_match_apple_ld_across_fixture_matrix() {
2655 if !have_xcrun() || !have_xcrun_tool("ld") {
2656 eprintln!("skipping: xcrun as/ld unavailable");
2657 return;
2658 }
2659 let Some(sdk) = sdk_path() else {
2660 eprintln!("skipping: xcrun --show-sdk-path unavailable");
2661 return;
2662 };
2663 let Some(sdk_ver) = sdk_version() else {
2664 eprintln!("skipping: xcrun --show-sdk-version unavailable");
2665 return;
2666 };
2667 const TEXT: SectionCase = SectionCase {
2668 segname: "__TEXT",
2669 sectname: "__text",
2670 };
2671 const CONST: SectionCase = SectionCase {
2672 segname: "__TEXT",
2673 sectname: "__const",
2674 };
2675
2676 let cases = [
2677 ParityCase {
2678 name: "branch-forward",
2679 src: r#"
2680 .section __TEXT,__text,regular,pure_instructions
2681 .globl _main
2682 _main:
2683 bl _helper
2684 ret
2685 _helper:
2686 ret
2687 .subsections_via_symbols
2688 "#,
2689 check: ParityCheck::ExactSections(&[TEXT]),
2690 },
2691 ParityCase {
2692 name: "branch-backward",
2693 src: r#"
2694 .section __TEXT,__text,regular,pure_instructions
2695 .globl _helper
2696 _helper:
2697 ret
2698 .globl _main
2699 _main:
2700 bl _helper
2701 ret
2702 .subsections_via_symbols
2703 "#,
2704 check: ParityCheck::ExactSections(&[TEXT]),
2705 },
2706 ParityCase {
2707 name: "adrp-add-intra-text-forward",
2708 src: r#"
2709 .section __TEXT,__text,regular,pure_instructions
2710 .globl _main
2711 _main:
2712 adrp x0, _target@PAGE
2713 add x0, x0, _target@PAGEOFF
2714 ret
2715 .space 0x4ff4
2716 _target:
2717 .quad 0
2718 .subsections_via_symbols
2719 "#,
2720 check: ParityCheck::PageRef {
2721 section: TEXT,
2722 site_offset: 0,
2723 target_offset: 0x5000,
2724 kind: PageRefKind::Add,
2725 },
2726 },
2727 ParityCase {
2728 name: "adrp-add-intra-text-backward",
2729 src: r#"
2730 .section __TEXT,__text,regular,pure_instructions
2731 _target:
2732 .quad 0x55
2733 .space 0x4ff8
2734 .globl _main
2735 _main:
2736 adrp x0, _target@PAGE
2737 add x0, x0, _target@PAGEOFF
2738 ret
2739 .subsections_via_symbols
2740 "#,
2741 check: ParityCheck::PageRef {
2742 section: TEXT,
2743 site_offset: 0x5000,
2744 target_offset: 0,
2745 kind: PageRefKind::Add,
2746 },
2747 },
2748 ParityCase {
2749 name: "adrp-ldr-x-intra-text",
2750 src: r#"
2751 .section __TEXT,__text,regular,pure_instructions
2752 .globl _main
2753 _main:
2754 adrp x0, _target@PAGE
2755 ldr x1, [x0, _target@PAGEOFF]
2756 ret
2757 .space 0x3f4
2758 _target:
2759 .quad 0x1122334455667788
2760 .subsections_via_symbols
2761 "#,
2762 check: ParityCheck::PageRef {
2763 section: TEXT,
2764 site_offset: 0,
2765 target_offset: 0x400,
2766 kind: PageRefKind::Load,
2767 },
2768 },
2769 ParityCase {
2770 name: "adrp-ldr-w-intra-text",
2771 src: r#"
2772 .section __TEXT,__text,regular,pure_instructions
2773 .globl _main
2774 _main:
2775 adrp x0, _target@PAGE
2776 ldr w1, [x0, _target@PAGEOFF]
2777 ret
2778 .space 0x2f4
2779 _target:
2780 .long 0x11223344
2781 .subsections_via_symbols
2782 "#,
2783 check: ParityCheck::PageRef {
2784 section: TEXT,
2785 site_offset: 0,
2786 target_offset: 0x300,
2787 kind: PageRefKind::Load,
2788 },
2789 },
2790 ParityCase {
2791 name: "adrp-ldrh-intra-text",
2792 src: r#"
2793 .section __TEXT,__text,regular,pure_instructions
2794 .globl _main
2795 _main:
2796 adrp x0, _target@PAGE
2797 ldrh w1, [x0, _target@PAGEOFF]
2798 ret
2799 .space 0x1f4
2800 _target:
2801 .hword 0x3344
2802 .subsections_via_symbols
2803 "#,
2804 check: ParityCheck::PageRef {
2805 section: TEXT,
2806 site_offset: 0,
2807 target_offset: 0x200,
2808 kind: PageRefKind::Load,
2809 },
2810 },
2811 ParityCase {
2812 name: "adrp-ldrb-intra-text",
2813 src: r#"
2814 .section __TEXT,__text,regular,pure_instructions
2815 .globl _main
2816 _main:
2817 adrp x0, _target@PAGE
2818 ldrb w1, [x0, _target@PAGEOFF]
2819 ret
2820 .space 0xf4
2821 _target:
2822 .byte 0x44
2823 .subsections_via_symbols
2824 "#,
2825 check: ParityCheck::PageRef {
2826 section: TEXT,
2827 site_offset: 0,
2828 target_offset: 0x100,
2829 kind: PageRefKind::Load,
2830 },
2831 },
2832 ParityCase {
2833 name: "mixed-branch-adrp-text",
2834 src: r#"
2835 .section __TEXT,__text,regular,pure_instructions
2836 .globl _main
2837 .globl _helper
2838 _main:
2839 adrp x0, _target@PAGE
2840 add x0, x0, _target@PAGEOFF
2841 bl _helper
2842 ret
2843 _helper:
2844 ret
2845 .space 0xff0
2846 _target:
2847 .quad 0x99
2848 .subsections_via_symbols
2849 "#,
2850 check: ParityCheck::PageRef {
2851 section: TEXT,
2852 site_offset: 0,
2853 target_offset: 0x1004,
2854 kind: PageRefKind::Add,
2855 },
2856 },
2857 ParityCase {
2858 name: "subtractor-positive",
2859 src: r#"
2860 .section __TEXT,__text,regular,pure_instructions
2861 .globl _helper
2862 _helper:
2863 ret
2864 .globl _main
2865 _main:
2866 bl _helper
2867 ret
2868 .section __TEXT,__const
2869 .p2align 3
2870 _delta:
2871 .quad _helper - _main
2872 .subsections_via_symbols
2873 "#,
2874 check: ParityCheck::ExactSections(&[CONST]),
2875 },
2876 ParityCase {
2877 name: "subtractor-negative",
2878 src: r#"
2879 .section __TEXT,__text,regular,pure_instructions
2880 .globl _helper
2881 _helper:
2882 ret
2883 .globl _main
2884 _main:
2885 ret
2886 .section __TEXT,__const
2887 .p2align 3
2888 _delta:
2889 .quad _main - _helper
2890 .subsections_via_symbols
2891 "#,
2892 check: ParityCheck::ExactSections(&[CONST]),
2893 },
2894 ParityCase {
2895 name: "branch-and-subtractor",
2896 src: r#"
2897 .section __TEXT,__text,regular,pure_instructions
2898 .globl _helper
2899 _helper:
2900 ret
2901 .globl _main
2902 _main:
2903 bl _helper
2904 ret
2905 .section __TEXT,__const
2906 .p2align 3
2907 _delta:
2908 .quad _main - _helper
2909 .subsections_via_symbols
2910 "#,
2911 check: ParityCheck::ExactSections(&[TEXT, CONST]),
2912 },
2913 ];
2914
2915 let mut failures = Vec::new();
2916 for case in &cases {
2917 if let Err(err) = assert_case_matches_apple_ld(case, &sdk, &sdk_ver) {
2918 failures.push(err);
2919 }
2920 }
2921
2922 assert!(
2923 failures.is_empty(),
2924 "Apple ld parity failures ({} cases):\n{}",
2925 failures.len(),
2926 failures.join("\n\n")
2927 );
2928 }
2929
2930 #[test]
2931 fn linker_run_rejects_out_of_range_branch26() {
2932 if !have_xcrun() {
2933 eprintln!("skipping: xcrun as unavailable");
2934 return;
2935 }
2936
2937 let obj = scratch("branch26-range.o");
2938 let out = scratch("branch26-range.out");
2939 let src = r#"
2940 .section __TEXT,__text,regular,pure_instructions
2941 .globl _main
2942 _main:
2943 bl _helper
2944 ret
2945
2946 .zerofill __DATA,__bss,_gap,0x9000000,0
2947
2948 .section __FAR,__text,regular,pure_instructions
2949 .globl _helper
2950 _helper:
2951 ret
2952 .subsections_via_symbols
2953 "#;
2954 if let Err(e) = assemble(src, &obj) {
2955 eprintln!("skipping: assemble failed: {e}");
2956 return;
2957 }
2958
2959 let opts = LinkOptions {
2960 inputs: vec![obj.clone()],
2961 output: Some(out),
2962 kind: OutputKind::Executable,
2963 ..LinkOptions::default()
2964 };
2965 let err = Linker::run(&opts).unwrap_err();
2966 match err {
2967 LinkError::Reloc(err) => {
2968 let msg = err.to_string();
2969 assert!(msg.contains("Branch26"), "{msg}");
2970 assert!(msg.contains("out of BRANCH26 range"), "{msg}");
2971 assert!(msg.contains("_helper"), "{msg}");
2972 }
2973 other => panic!("expected Reloc error, got {other:?}"),
2974 }
2975
2976 let _ = fs::remove_file(obj);
2977 }
2978
2979 #[test]
2980 fn linker_run_routes_dylib_imports_through_synthetic_sections() {
2981 if !have_xcrun() {
2982 eprintln!("skipping: xcrun unavailable");
2983 return;
2984 }
2985 let Some(sdk) = sdk_path() else {
2986 eprintln!("skipping: xcrun --show-sdk-path unavailable");
2987 return;
2988 };
2989 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
2990 if !tbd.exists() {
2991 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
2992 return;
2993 }
2994
2995 let obj = scratch("import-reloc.o");
2996 let out = scratch("import-reloc.out");
2997 let src = r#"
2998 .section __TEXT,__text,regular,pure_instructions
2999 .globl _main
3000 _main:
3001 adrp x0, _write@GOTPAGE
3002 ldr x0, [x0, _write@GOTPAGEOFF]
3003 bl _write
3004 ret
3005 .subsections_via_symbols
3006 "#;
3007 if let Err(e) = assemble(src, &obj) {
3008 eprintln!("skipping: assemble failed: {e}");
3009 return;
3010 }
3011
3012 let opts = LinkOptions {
3013 inputs: vec![obj.clone(), tbd.clone()],
3014 output: Some(out.clone()),
3015 kind: OutputKind::Executable,
3016 ..LinkOptions::default()
3017 };
3018 Linker::run(&opts).unwrap();
3019
3020 let bytes = fs::read(&out).unwrap();
3021 let header = parse_header(&bytes).unwrap();
3022 let commands = parse_commands(&header, &bytes).unwrap();
3023 let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
3024 let (stubs_addr, stubs) = output_section(&bytes, "__TEXT", "__stubs").unwrap();
3025 let (helper_addr, helper) = output_section(&bytes, "__TEXT", "__stub_helper").unwrap();
3026 let (got_addr, got) = output_section(&bytes, "__DATA_CONST", "__got").unwrap();
3027 let (lazy_addr, lazy) = output_section(&bytes, "__DATA", "__la_symbol_ptr").unwrap();
3028 let (dyld_private_addr, _) = output_section(&bytes, "__DATA", "__data").unwrap();
3029 let stubs_hdr = output_section_header(&bytes, "__TEXT", "__stubs").unwrap();
3030 let got_hdr = output_section_header(&bytes, "__DATA_CONST", "__got").unwrap();
3031 let lazy_hdr = output_section_header(&bytes, "__DATA", "__la_symbol_ptr").unwrap();
3032
3033 let symtab = commands
3034 .iter()
3035 .find_map(|cmd| match cmd {
3036 LoadCommand::Symtab(cmd) => Some(*cmd),
3037 _ => None,
3038 })
3039 .unwrap();
3040 let dysymtab = commands
3041 .iter()
3042 .find_map(|cmd| match cmd {
3043 LoadCommand::Dysymtab(cmd) => Some(*cmd),
3044 _ => None,
3045 })
3046 .unwrap();
3047 let dyld_info = commands
3048 .iter()
3049 .find_map(|cmd| match cmd {
3050 LoadCommand::DyldInfoOnly(cmd) => Some(*cmd),
3051 _ => None,
3052 })
3053 .unwrap();
3054 let libsystem_load = commands
3055 .iter()
3056 .find_map(|cmd| match cmd {
3057 LoadCommand::Dylib(cmd)
3058 if cmd.cmd == afs_ld::macho::constants::LC_LOAD_DYLIB
3059 && cmd.name == "/usr/lib/libSystem.B.dylib" =>
3060 {
3061 Some(cmd.clone())
3062 }
3063 _ => None,
3064 })
3065 .unwrap();
3066 let symbols = parse_nlist_table(&bytes, symtab.symoff, symtab.nsyms).unwrap();
3067 let strings = StringTable::from_file(&bytes, symtab.stroff, symtab.strsize).unwrap();
3068 let symbol_names: Vec<&str> = symbols
3069 .iter()
3070 .map(|symbol| strings.get(symbol.strx()).unwrap())
3071 .collect();
3072
3073 assert_eq!(got.len(), 16);
3074 assert_eq!(stubs.len(), 12);
3075 assert_eq!(helper.len(), 36);
3076 assert_eq!(lazy.len(), 8);
3077 assert_eq!(symtab.nsyms, 5);
3078 assert_eq!(dysymtab.nlocalsym, 1);
3079 assert_eq!(dysymtab.nextdefsym, 2);
3080 assert_eq!(dysymtab.nundefsym, 2);
3081 assert_eq!(dysymtab.nindirectsyms, 4);
3082 assert_eq!(stubs_hdr.reserved1, 0);
3083 assert_eq!(got_hdr.reserved1, 1);
3084 assert_eq!(lazy_hdr.reserved1, 3);
3085 assert_eq!(stubs_hdr.reserved2, 12);
3086 assert!(libsystem_load.current_version >= (1 << 16));
3087 assert_eq!(libsystem_load.compatibility_version, 1 << 16);
3088 assert!(dyld_info.rebase_size > 0);
3089 assert!(dyld_info.bind_size > 0);
3090 assert!(dyld_info.lazy_bind_size > 0);
3091 assert_eq!(
3092 decode_page_reference(&text, text_addr, 0, &PageRefKind::Load).unwrap(),
3093 got_addr
3094 );
3095 assert_eq!(
3096 decode_branch_target(&text, text_addr, 8).unwrap(),
3097 stubs_addr
3098 );
3099 assert_eq!(
3100 decode_page_reference(&stubs, stubs_addr, 0, &PageRefKind::Load).unwrap(),
3101 lazy_addr
3102 );
3103 assert_eq!(read_insn(&stubs, 8).unwrap(), 0xd61f0200);
3104 assert_eq!(
3105 u64::from_le_bytes(lazy[0..8].try_into().unwrap()),
3106 helper_addr + 24
3107 );
3108 assert_eq!(
3109 decode_page_reference(&helper, helper_addr, 0, &PageRefKind::Add).unwrap(),
3110 dyld_private_addr
3111 );
3112 assert_eq!(
3113 decode_page_reference(&helper, helper_addr, 12, &PageRefKind::Load).unwrap(),
3114 got_addr + 8
3115 );
3116 assert_eq!(read_insn(&helper, 20).unwrap(), 0xd61f0200);
3117 assert_eq!(read_insn(&helper, 24).unwrap(), 0x1800_0050);
3118 assert_eq!(
3119 decode_branch_target(&helper, helper_addr, 28).unwrap(),
3120 helper_addr
3121 );
3122 assert_eq!(u32::from_le_bytes(helper[32..36].try_into().unwrap()), 0);
3123 let (locals, extdefs, undefs) = symbol_partition_names(&bytes);
3124 assert_eq!(locals, vec!["__dyld_private".to_string()]);
3125 assert_eq!(
3126 extdefs,
3127 vec!["__mh_execute_header".to_string(), "_main".to_string()]
3128 );
3129 assert_eq!(
3130 undefs,
3131 vec!["_write".to_string(), "dyld_stub_binder".to_string()]
3132 );
3133 assert!(symbol_names.contains(&"__dyld_private"));
3134 assert!(symbols[dysymtab.iundefsym as usize..]
3135 .iter()
3136 .all(|symbol| symbol.kind() == SymKind::Undef));
3137 assert!(symbols[dysymtab.iundefsym as usize..]
3138 .iter()
3139 .all(|symbol| symbol.library_ordinal().unwrap() > 0));
3140 assert!(symbol_names.contains(&"_write"));
3141 assert!(symbol_names.contains(&"dyld_stub_binder"));
3142
3143 let _ = fs::remove_file(out);
3144 let _ = fs::remove_file(obj);
3145 }
3146
3147 #[test]
3148 fn synthetic_import_surfaces_match_apple_ld_classic_lazy_model() {
3149 if !have_xcrun() || !have_xcrun_tool("ld") {
3150 eprintln!("skipping: xcrun as/ld unavailable");
3151 return;
3152 }
3153 let Some(sdk) = sdk_path() else {
3154 eprintln!("skipping: xcrun --show-sdk-path unavailable");
3155 return;
3156 };
3157 let Some(sdk_ver) = sdk_version() else {
3158 eprintln!("skipping: xcrun --show-sdk-version unavailable");
3159 return;
3160 };
3161 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
3162 if !tbd.exists() {
3163 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
3164 return;
3165 }
3166
3167 let obj = scratch("import-parity.o");
3168 let our_out = scratch("import-parity-ours.out");
3169 let apple_out = scratch("import-parity-apple.out");
3170 let src = r#"
3171 .section __TEXT,__text,regular,pure_instructions
3172 .globl _main
3173 _main:
3174 adrp x0, _write@GOTPAGE
3175 ldr x0, [x0, _write@GOTPAGEOFF]
3176 bl _write
3177 ret
3178 .subsections_via_symbols
3179 "#;
3180 if let Err(e) = assemble(src, &obj) {
3181 eprintln!("skipping: assemble failed: {e}");
3182 return;
3183 }
3184
3185 let opts = LinkOptions {
3186 inputs: vec![obj.clone(), tbd],
3187 output: Some(our_out.clone()),
3188 kind: OutputKind::Executable,
3189 ..LinkOptions::default()
3190 };
3191 Linker::run(&opts).unwrap();
3192 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
3193
3194 let our_bytes = fs::read(&our_out).unwrap();
3195 let apple_bytes = fs::read(&apple_out).unwrap();
3196
3197 for (segname, sectname) in [("__TEXT", "__stubs"), ("__TEXT", "__stub_helper")] {
3198 let (_, ours) = output_section(&our_bytes, segname, sectname).unwrap();
3199 let (_, apple) = output_section(&apple_bytes, segname, sectname).unwrap();
3200 let diff = diff_macho(&ours, &apple);
3201 assert!(
3202 diff.is_clean(),
3203 "{segname},{sectname} diverged from Apple ld: {:#?}",
3204 diff.critical
3205 );
3206 }
3207
3208 let (our_helper_addr, _) = output_section(&our_bytes, "__TEXT", "__stub_helper").unwrap();
3209 let (apple_helper_addr, _) = output_section(&apple_bytes, "__TEXT", "__stub_helper").unwrap();
3210 let (_, our_lazy) = output_section(&our_bytes, "__DATA", "__la_symbol_ptr").unwrap();
3211 let (_, apple_lazy) = output_section(&apple_bytes, "__DATA", "__la_symbol_ptr").unwrap();
3212 assert_eq!(
3213 u64::from_le_bytes(our_lazy[0..8].try_into().unwrap()) - our_helper_addr,
3214 24
3215 );
3216 assert_eq!(
3217 u64::from_le_bytes(apple_lazy[0..8].try_into().unwrap()) - apple_helper_addr,
3218 24
3219 );
3220
3221 assert_eq!(
3222 load_dylib_names(&our_bytes).unwrap(),
3223 load_dylib_names(&apple_bytes).unwrap()
3224 );
3225 assert_eq!(
3226 segment_flags(&our_bytes, "__DATA_CONST"),
3227 Some(SG_READ_ONLY)
3228 );
3229 assert_eq!(
3230 segment_flags(&our_bytes, "__DATA_CONST"),
3231 segment_flags(&apple_bytes, "__DATA_CONST")
3232 );
3233
3234 let our_rebases = decode_rebase_records(&our_bytes).unwrap();
3235 let apple_rebases = decode_rebase_records(&apple_bytes).unwrap();
3236 assert!(our_rebases
3237 .iter()
3238 .all(|record| record.rebase_type == REBASE_TYPE_POINTER));
3239 assert_eq!(our_rebases, apple_rebases);
3240 assert_eq!(
3241 decode_bind_records(&our_bytes, false).unwrap(),
3242 decode_bind_records(&apple_bytes, false).unwrap()
3243 );
3244 assert_eq!(
3245 dyld_info_stream(&our_bytes, DyldInfoStreamKind::WeakBind).unwrap(),
3246 dyld_info_stream(&apple_bytes, DyldInfoStreamKind::WeakBind).unwrap()
3247 );
3248 assert_eq!(
3249 decode_bind_records(&our_bytes, true).unwrap(),
3250 decode_bind_records(&apple_bytes, true).unwrap()
3251 );
3252 assert_eq!(
3253 canonical_lazy_bind_stream(&our_bytes).unwrap(),
3254 canonical_lazy_bind_stream(&apple_bytes).unwrap()
3255 );
3256 assert_eq!(
3257 indirect_symbol_table(&our_bytes),
3258 indirect_symbol_table(&apple_bytes)
3259 );
3260
3261 let _ = fs::remove_file(apple_out);
3262 let _ = fs::remove_file(our_out);
3263 let _ = fs::remove_file(obj);
3264 }
3265
3266 #[test]
3267 fn classic_lazy_surfaces_match_apple_ld_across_fixture_matrix() {
3268 if !have_xcrun() || !have_xcrun_tool("ld") {
3269 eprintln!("skipping: xcrun as/ld unavailable");
3270 return;
3271 }
3272 let Some(sdk) = sdk_path() else {
3273 eprintln!("skipping: xcrun --show-sdk-path unavailable");
3274 return;
3275 };
3276 let Some(sdk_ver) = sdk_version() else {
3277 eprintln!("skipping: xcrun --show-sdk-version unavailable");
3278 return;
3279 };
3280
3281 let cases = [
3282 ClassicLazyParityCase {
3283 name: "single-got-and-call",
3284 src: r#"
3285 .section __TEXT,__text,regular,pure_instructions
3286 .globl _main
3287 _main:
3288 adrp x0, _write@GOTPAGE
3289 ldr x0, [x0, _write@GOTPAGEOFF]
3290 bl _write
3291 ret
3292 .subsections_via_symbols
3293 "#,
3294 },
3295 ClassicLazyParityCase {
3296 name: "batched-got-and-calls",
3297 src: r#"
3298 .section __TEXT,__text,regular,pure_instructions
3299 .globl _main
3300 _main:
3301 adrp x0, _write@GOTPAGE
3302 ldr x0, [x0, _write@GOTPAGEOFF]
3303 bl _write
3304 adrp x1, _close@GOTPAGE
3305 ldr x1, [x1, _close@GOTPAGEOFF]
3306 bl _close
3307 adrp x2, _read@GOTPAGE
3308 ldr x2, [x2, _read@GOTPAGEOFF]
3309 bl _read
3310 ret
3311 .subsections_via_symbols
3312 "#,
3313 },
3314 ClassicLazyParityCase {
3315 name: "branch-only-calls",
3316 src: r#"
3317 .section __TEXT,__text,regular,pure_instructions
3318 .globl _main
3319 _main:
3320 bl _write
3321 bl _close
3322 bl _read
3323 ret
3324 .subsections_via_symbols
3325 "#,
3326 },
3327 ClassicLazyParityCase {
3328 name: "deduped-import",
3329 src: r#"
3330 .section __TEXT,__text,regular,pure_instructions
3331 .globl _main
3332 _main:
3333 adrp x0, _write@GOTPAGE
3334 ldr x0, [x0, _write@GOTPAGEOFF]
3335 bl _write
3336 bl _write
3337 adrp x1, _write@GOTPAGE
3338 ldr x1, [x1, _write@GOTPAGEOFF]
3339 ret
3340 .subsections_via_symbols
3341 "#,
3342 },
3343 ];
3344
3345 let mut failures = Vec::new();
3346 for case in &cases {
3347 if let Err(err) = assert_classic_lazy_case_matches_apple_ld(case, &sdk, &sdk_ver) {
3348 failures.push(err);
3349 }
3350 }
3351
3352 assert!(
3353 failures.is_empty(),
3354 "Apple ld classic-lazy parity failures ({} cases):\n{}",
3355 failures.len(),
3356 failures.join("\n\n")
3357 );
3358 }
3359
3360 #[test]
3361 fn linker_run_binds_direct_dylib_import_pointers() {
3362 if !have_xcrun() || !have_tool("codesign") {
3363 eprintln!("skipping: xcrun clang or codesign unavailable");
3364 return;
3365 }
3366 let Some(sdk) = sdk_path() else {
3367 eprintln!("skipping: xcrun --show-sdk-path unavailable");
3368 return;
3369 };
3370 let Some(sdk_ver) = sdk_version() else {
3371 eprintln!("skipping: xcrun --show-sdk-version unavailable");
3372 return;
3373 };
3374 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
3375 if !tbd.exists() {
3376 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
3377 return;
3378 }
3379
3380 let dylib_src = r#"
3381 int ext_data = 5;
3382 "#;
3383 let direct_case = DirectBindParityCase {
3384 name: "direct-data",
3385 dylib_src,
3386 main_src: r#"
3387 extern int ext_data;
3388 int *p = &ext_data;
3389 int main(void) { return *p == 5 ? 0 : 1; }
3390 "#,
3391 };
3392 if let Err(e) = assert_direct_bind_case_matches_apple_ld(&direct_case, &sdk, &sdk_ver) {
3393 panic!("{e}");
3394 }
3395
3396 let dylib = scratch("direct-data.dylib");
3397 let obj = scratch("direct-data.o");
3398 let our_out = scratch("direct-data-ours.out");
3399
3400 if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
3401 eprintln!("skipping: dylib compile failed: {e}");
3402 return;
3403 }
3404
3405 let main_src = r#"
3406 extern int ext_data;
3407 int *p = &ext_data;
3408 int main(void) { return *p == 5 ? 0 : 1; }
3409 "#;
3410 if let Err(e) = compile_c(main_src, &obj) {
3411 eprintln!("skipping: compile failed: {e}");
3412 return;
3413 }
3414
3415 let opts = LinkOptions {
3416 inputs: vec![obj.clone(), tbd.clone(), dylib.clone()],
3417 output: Some(our_out.clone()),
3418 kind: OutputKind::Executable,
3419 ..LinkOptions::default()
3420 };
3421 Linker::run(&opts).unwrap();
3422 let our_bytes = fs::read(&our_out).unwrap();
3423 let binds = decode_bind_records(&our_bytes, false).unwrap();
3424 assert!(
3425 binds.iter().any(|record| {
3426 record.segment == "__DATA"
3427 && record.section == "__data"
3428 && record.section_offset == 0
3429 && record.symbol == "_ext_data"
3430 }),
3431 "missing direct bind for imported data: {binds:#?}"
3432 );
3433 let verify = Command::new("codesign")
3434 .arg("-v")
3435 .arg(&our_out)
3436 .output()
3437 .unwrap();
3438 assert!(
3439 verify.status.success(),
3440 "codesign verify failed: {}",
3441 String::from_utf8_lossy(&verify.stderr)
3442 );
3443 let status = Command::new(&our_out).status().unwrap();
3444 assert_eq!(
3445 status.code(),
3446 Some(0),
3447 "expected direct-import pointer executable to exit 0"
3448 );
3449
3450 let _ = fs::remove_file(dylib);
3451 let _ = fs::remove_file(obj);
3452 let _ = fs::remove_file(our_out);
3453 }
3454
3455 #[test]
3456 fn direct_bind_surfaces_match_apple_ld_across_fixture_matrix() {
3457 if !have_xcrun() || !have_tool("codesign") {
3458 eprintln!("skipping: xcrun clang or codesign unavailable");
3459 return;
3460 }
3461 let Some(sdk) = sdk_path() else {
3462 eprintln!("skipping: xcrun --show-sdk-path unavailable");
3463 return;
3464 };
3465 let Some(sdk_ver) = sdk_version() else {
3466 eprintln!("skipping: xcrun --show-sdk-version unavailable");
3467 return;
3468 };
3469
3470 let cases = [
3471 DirectBindParityCase {
3472 name: "direct-multi-data",
3473 dylib_src: r#"
3474 int ext_data = 5;
3475 int more_data = 9;
3476 "#,
3477 main_src: r#"
3478 extern int ext_data;
3479 extern int more_data;
3480 int *p = &ext_data;
3481 int *q = &more_data;
3482 int main(void) { return (*p == 5 && *q == 9) ? 0 : 1; }
3483 "#,
3484 },
3485 DirectBindParityCase {
3486 name: "direct-and-call-mixed",
3487 dylib_src: r#"
3488 int ext_data = 5;
3489 int ext_fn(void) { return ext_data + 1; }
3490 "#,
3491 main_src: r#"
3492 extern int ext_data;
3493 extern int ext_fn(void);
3494 int *p = &ext_data;
3495 int main(void) { return *p + ext_fn() == 11 ? 0 : 1; }
3496 "#,
3497 },
3498 DirectBindParityCase {
3499 name: "direct-deduped",
3500 dylib_src: r#"
3501 int ext_data = 5;
3502 "#,
3503 main_src: r#"
3504 extern int ext_data;
3505 int *p = &ext_data;
3506 int *q = &ext_data;
3507 int main(void) { return (*p == 5 && *q == 5) ? 0 : 1; }
3508 "#,
3509 },
3510 ];
3511
3512 let mut failures = Vec::new();
3513 for case in &cases {
3514 if let Err(err) = assert_direct_bind_case_matches_apple_ld(case, &sdk, &sdk_ver) {
3515 failures.push(err);
3516 }
3517 }
3518
3519 assert!(
3520 failures.is_empty(),
3521 "Apple ld direct-bind parity failures ({} cases):\n{}",
3522 failures.len(),
3523 failures.join("\n\n")
3524 );
3525 }
3526
3527 #[test]
3528 fn linker_run_rebases_local_absolute_pointers_like_ld() {
3529 if !have_xcrun() {
3530 eprintln!("skipping: xcrun unavailable");
3531 return;
3532 }
3533 let Some(sdk) = sdk_path() else {
3534 eprintln!("skipping: xcrun --show-sdk-path unavailable");
3535 return;
3536 };
3537 let Some(sdk_ver) = sdk_version() else {
3538 eprintln!("skipping: xcrun --show-sdk-version unavailable");
3539 return;
3540 };
3541 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
3542 if !tbd.exists() {
3543 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
3544 return;
3545 }
3546
3547 let obj = scratch("local-rebase.o");
3548 let our_out = scratch("local-rebase-ours.out");
3549 let apple_out = scratch("local-rebase-apple.out");
3550 let src = r#"
3551 int ext = 7;
3552 int *p = &ext;
3553 int main(void) { return *p == 7 ? 0 : 1; }
3554 "#;
3555 if let Err(e) = compile_c(src, &obj) {
3556 eprintln!("skipping: clang compile failed: {e}");
3557 return;
3558 }
3559
3560 let opts = LinkOptions {
3561 inputs: vec![obj.clone(), tbd],
3562 output: Some(our_out.clone()),
3563 kind: OutputKind::Executable,
3564 ..LinkOptions::default()
3565 };
3566 Linker::run(&opts).unwrap();
3567 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
3568
3569 let our_bytes = fs::read(&our_out).unwrap();
3570 let apple_bytes = fs::read(&apple_out).unwrap();
3571 assert!(!dyld_info_stream(&our_bytes, DyldInfoStreamKind::Rebase)
3572 .unwrap()
3573 .is_empty());
3574 assert_eq!(
3575 dyld_info_stream(&our_bytes, DyldInfoStreamKind::Rebase).unwrap(),
3576 dyld_info_stream(&apple_bytes, DyldInfoStreamKind::Rebase).unwrap()
3577 );
3578 assert_eq!(
3579 decode_rebase_records(&our_bytes).unwrap(),
3580 decode_rebase_records(&apple_bytes).unwrap()
3581 );
3582
3583 let our_status = Command::new(&our_out).status().unwrap();
3584 let apple_status = Command::new(&apple_out).status().unwrap();
3585 assert_eq!(our_status.code(), Some(0));
3586 assert_eq!(apple_status.code(), Some(0));
3587
3588 let _ = fs::remove_file(obj);
3589 let _ = fs::remove_file(our_out);
3590 let _ = fs::remove_file(apple_out);
3591 }
3592
3593 #[test]
3594 fn linker_run_partitions_symtab_like_ld() {
3595 if !have_xcrun() {
3596 eprintln!("skipping: xcrun unavailable");
3597 return;
3598 }
3599
3600 let dylib = scratch("symtab-partition.dylib");
3601 let obj = scratch("symtab-partition.o");
3602 let our_out = scratch("symtab-partition-ours.out");
3603 let apple_out = scratch("symtab-partition-apple.out");
3604
3605 let dylib_src = r#"
3606 int ext_data = 5;
3607 "#;
3608 if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
3609 eprintln!("skipping: dylib compile failed: {e}");
3610 return;
3611 }
3612
3613 let asm = r#"
3614 .text
3615 .private_extern _hidden
3616 .globl _visible
3617 .globl _main
3618 .p2align 2
3619 _local:
3620 ret
3621 _hidden:
3622 ret
3623 _visible:
3624 ret
3625 _main:
3626 ret
3627
3628 .data
3629 .quad _ext_data
3630 .subsections_via_symbols
3631 "#;
3632 if let Err(e) = assemble(asm, &obj) {
3633 eprintln!("skipping: assemble failed: {e}");
3634 return;
3635 }
3636
3637 let opts = LinkOptions {
3638 inputs: vec![obj.clone(), dylib.clone()],
3639 output: Some(our_out.clone()),
3640 kind: OutputKind::Executable,
3641 ..LinkOptions::default()
3642 };
3643 Linker::run(&opts).unwrap();
3644
3645 let apple = Command::new("xcrun")
3646 .args(["ld", "-arch", "arm64", "-e", "_main", "-o"])
3647 .arg(&apple_out)
3648 .arg(&obj)
3649 .arg(&dylib)
3650 .output()
3651 .unwrap();
3652 assert!(
3653 apple.status.success(),
3654 "xcrun ld failed: {}",
3655 String::from_utf8_lossy(&apple.stderr)
3656 );
3657
3658 let our_bytes = fs::read(&our_out).unwrap();
3659 let apple_bytes = fs::read(&apple_out).unwrap();
3660 let (our_symtab, our_dysymtab) = symtab_and_dysymtab(&our_bytes);
3661 let (apple_symtab, apple_dysymtab) = symtab_and_dysymtab(&apple_bytes);
3662
3663 assert_eq!(our_symtab.nsyms, apple_symtab.nsyms);
3664 assert_eq!(our_dysymtab.ilocalsym, apple_dysymtab.ilocalsym);
3665 assert_eq!(our_dysymtab.nlocalsym, apple_dysymtab.nlocalsym);
3666 assert_eq!(our_dysymtab.iextdefsym, apple_dysymtab.iextdefsym);
3667 assert_eq!(our_dysymtab.nextdefsym, apple_dysymtab.nextdefsym);
3668 assert_eq!(our_dysymtab.iundefsym, apple_dysymtab.iundefsym);
3669 assert_eq!(our_dysymtab.nundefsym, apple_dysymtab.nundefsym);
3670 assert_eq!(
3671 canonical_symbol_records(&our_bytes),
3672 canonical_symbol_records(&apple_bytes)
3673 );
3674 assert_strtab_within_five_percent(
3675 &raw_string_table(&our_bytes),
3676 &raw_string_table(&apple_bytes),
3677 );
3678
3679 assert_eq!(
3680 symbol_partition_names(&our_bytes),
3681 symbol_partition_names(&apple_bytes)
3682 );
3683
3684 let _ = fs::remove_file(dylib);
3685 let _ = fs::remove_file(obj);
3686 let _ = fs::remove_file(our_out);
3687 let _ = fs::remove_file(apple_out);
3688 }
3689
3690 #[test]
3691 fn linker_run_strips_locals_with_x_like_ld() {
3692 if !have_xcrun() {
3693 eprintln!("skipping: xcrun unavailable");
3694 return;
3695 }
3696
3697 let dylib = scratch("symtab-strip.dylib");
3698 let obj = scratch("symtab-strip.o");
3699 let our_out = scratch("symtab-strip-ours.out");
3700 let apple_out = scratch("symtab-strip-apple.out");
3701
3702 let dylib_src = r#"
3703 int ext_data = 5;
3704 "#;
3705 if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
3706 eprintln!("skipping: dylib compile failed: {e}");
3707 return;
3708 }
3709
3710 let asm = r#"
3711 .text
3712 .private_extern _hidden
3713 .globl _visible
3714 .globl _main
3715 .p2align 2
3716 _local:
3717 ret
3718 _hidden:
3719 ret
3720 _visible:
3721 ret
3722 _main:
3723 ret
3724
3725 .data
3726 .quad _ext_data
3727 .subsections_via_symbols
3728 "#;
3729 if let Err(e) = assemble(asm, &obj) {
3730 eprintln!("skipping: assemble failed: {e}");
3731 return;
3732 }
3733
3734 let opts = LinkOptions {
3735 inputs: vec![obj.clone(), dylib.clone()],
3736 output: Some(our_out.clone()),
3737 kind: OutputKind::Executable,
3738 strip_locals: true,
3739 ..LinkOptions::default()
3740 };
3741 Linker::run(&opts).unwrap();
3742
3743 let apple = Command::new("xcrun")
3744 .args(["ld", "-arch", "arm64", "-x", "-e", "_main", "-o"])
3745 .arg(&apple_out)
3746 .arg(&obj)
3747 .arg(&dylib)
3748 .output()
3749 .unwrap();
3750 assert!(
3751 apple.status.success(),
3752 "xcrun ld failed: {}",
3753 String::from_utf8_lossy(&apple.stderr)
3754 );
3755
3756 let our_bytes = fs::read(&our_out).unwrap();
3757 let apple_bytes = fs::read(&apple_out).unwrap();
3758 let (our_symtab, our_dysymtab) = symtab_and_dysymtab(&our_bytes);
3759 let (apple_symtab, apple_dysymtab) = symtab_and_dysymtab(&apple_bytes);
3760
3761 assert_eq!(our_symtab.nsyms, apple_symtab.nsyms);
3762 assert_eq!(our_dysymtab.ilocalsym, apple_dysymtab.ilocalsym);
3763 assert_eq!(our_dysymtab.nlocalsym, apple_dysymtab.nlocalsym);
3764 assert_eq!(our_dysymtab.iextdefsym, apple_dysymtab.iextdefsym);
3765 assert_eq!(our_dysymtab.nextdefsym, apple_dysymtab.nextdefsym);
3766 assert_eq!(our_dysymtab.iundefsym, apple_dysymtab.iundefsym);
3767 assert_eq!(our_dysymtab.nundefsym, apple_dysymtab.nundefsym);
3768 assert_eq!(
3769 canonical_symbol_records(&our_bytes),
3770 canonical_symbol_records(&apple_bytes)
3771 );
3772
3773 let (locals, extdefs, undefs) = symbol_partition_names(&our_bytes);
3774 assert!(locals.is_empty());
3775 assert_eq!(
3776 extdefs,
3777 vec![
3778 "__mh_execute_header".to_string(),
3779 "_main".to_string(),
3780 "_visible".to_string()
3781 ]
3782 );
3783 assert_eq!(undefs, vec!["_ext_data".to_string()]);
3784
3785 let _ = fs::remove_file(dylib);
3786 let _ = fs::remove_file(obj);
3787 let _ = fs::remove_file(our_out);
3788 let _ = fs::remove_file(apple_out);
3789 }
3790
3791 #[test]
3792 fn linker_run_emits_leaf_unwind_info_like_ld() {
3793 if !have_xcrun() {
3794 eprintln!("skipping: xcrun unavailable");
3795 return;
3796 }
3797 let Some(sdk) = sdk_path() else {
3798 eprintln!("skipping: xcrun --show-sdk-path unavailable");
3799 return;
3800 };
3801 let Some(sdk_ver) = sdk_version() else {
3802 eprintln!("skipping: xcrun --show-sdk-version unavailable");
3803 return;
3804 };
3805
3806 let obj = scratch("unwind-leaf.o");
3807 let our_out = scratch("unwind-leaf-ours.out");
3808 let apple_out = scratch("unwind-leaf-apple.out");
3809 let src = r#"
3810 int main(void) {
3811 return 0;
3812 }
3813 "#;
3814 if let Err(e) = compile_c(src, &obj) {
3815 eprintln!("skipping: clang compile failed: {e}");
3816 return;
3817 }
3818
3819 let opts = LinkOptions {
3820 inputs: vec![obj.clone()],
3821 output: Some(our_out.clone()),
3822 kind: OutputKind::Executable,
3823 ..LinkOptions::default()
3824 };
3825 Linker::run(&opts).unwrap();
3826 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
3827
3828 let our_bytes = fs::read(&our_out).unwrap();
3829 let apple_bytes = fs::read(&apple_out).unwrap();
3830 assert_eq!(
3831 rebased_unwind_bytes(&our_bytes),
3832 rebased_unwind_bytes(&apple_bytes)
3833 );
3834 assert!(output_section(&our_bytes, "__LD", "__compact_unwind").is_none());
3835
3836 let _ = fs::remove_file(obj);
3837 let _ = fs::remove_file(our_out);
3838 let _ = fs::remove_file(apple_out);
3839 }
3840
3841 #[test]
3842 fn linker_run_emits_multi_function_unwind_info_like_ld() {
3843 if !have_xcrun() {
3844 eprintln!("skipping: xcrun unavailable");
3845 return;
3846 }
3847 let Some(sdk) = sdk_path() else {
3848 eprintln!("skipping: xcrun --show-sdk-path unavailable");
3849 return;
3850 };
3851 let Some(sdk_ver) = sdk_version() else {
3852 eprintln!("skipping: xcrun --show-sdk-version unavailable");
3853 return;
3854 };
3855
3856 let obj = scratch("unwind-mixed.o");
3857 let our_out = scratch("unwind-mixed-ours.out");
3858 let apple_out = scratch("unwind-mixed-apple.out");
3859 let src = r#"
3860 int helper(void) {
3861 return 1;
3862 }
3863
3864 int main(void) {
3865 return helper();
3866 }
3867 "#;
3868 if let Err(e) = compile_c(src, &obj) {
3869 eprintln!("skipping: clang compile failed: {e}");
3870 return;
3871 }
3872
3873 let opts = LinkOptions {
3874 inputs: vec![obj.clone()],
3875 output: Some(our_out.clone()),
3876 kind: OutputKind::Executable,
3877 ..LinkOptions::default()
3878 };
3879 Linker::run(&opts).unwrap();
3880 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
3881
3882 let our_bytes = fs::read(&our_out).unwrap();
3883 let apple_bytes = fs::read(&apple_out).unwrap();
3884 assert_eq!(
3885 rebased_unwind_bytes(&our_bytes),
3886 rebased_unwind_bytes(&apple_bytes)
3887 );
3888 assert!(output_section(&our_bytes, "__LD", "__compact_unwind").is_none());
3889
3890 let _ = fs::remove_file(obj);
3891 let _ = fs::remove_file(our_out);
3892 let _ = fs::remove_file(apple_out);
3893 }
3894
3895 #[test]
3896 fn linker_run_handles_large_unwind_function_gaps() {
3897 if !have_xcrun() {
3898 eprintln!("skipping: xcrun unavailable");
3899 return;
3900 }
3901
3902 let obj = scratch("unwind-gap.o");
3903 let out = scratch("unwind-gap-ours.out");
3904 let asm = r#"
3905 .text
3906 .globl _main
3907 .p2align 2
3908 _main:
3909 .cfi_startproc
3910 bl _helper
3911 ret
3912 .cfi_endproc
3913 .space 0x1000010
3914 .globl _helper
3915 .p2align 2
3916 _helper:
3917 .cfi_startproc
3918 ret
3919 .cfi_endproc
3920 .subsections_via_symbols
3921 "#;
3922 if let Err(e) = assemble(asm, &obj) {
3923 eprintln!("skipping: assemble failed: {e}");
3924 return;
3925 }
3926
3927 let opts = LinkOptions {
3928 inputs: vec![obj.clone()],
3929 output: Some(out.clone()),
3930 kind: OutputKind::Executable,
3931 ..LinkOptions::default()
3932 };
3933 Linker::run(&opts).unwrap();
3934
3935 let bytes = fs::read(&out).unwrap();
3936 let (_, unwind) = output_section(&bytes, "__TEXT", "__unwind_info").unwrap();
3937 let decoded = decode_unwind_info(&unwind).unwrap();
3938 assert!(
3939 decoded
3940 .records
3941 .windows(2)
3942 .all(|pair| pair[0].function_offset < pair[1].function_offset),
3943 "expected strictly ascending unwind records after large-gap pagination"
3944 );
3945
3946 let _ = fs::remove_file(obj);
3947 let _ = fs::remove_file(out);
3948 }
3949
3950 #[test]
3951 fn linker_run_preserves_eh_frame_like_ld() {
3952 if !have_xcrun() || !have_xcrun_tool("dwarfdump") {
3953 eprintln!("skipping: xcrun dwarfdump unavailable");
3954 return;
3955 }
3956 let Some(sdk) = sdk_path() else {
3957 eprintln!("skipping: xcrun --show-sdk-path unavailable");
3958 return;
3959 };
3960 let Some(sdk_ver) = sdk_version() else {
3961 eprintln!("skipping: xcrun --show-sdk-version unavailable");
3962 return;
3963 };
3964
3965 let obj = scratch("eh-frame.o");
3966 let our_out = scratch("eh-frame-ours.out");
3967 let apple_out = scratch("eh-frame-apple.out");
3968 let asm = r#"
3969 .text
3970 .globl _main
3971 .p2align 2
3972 _main:
3973 .cfi_startproc
3974 sub sp, sp, #16
3975 .cfi_def_cfa_offset 16
3976 str x30, [sp, #8]
3977 .cfi_offset w30, -8
3978 bl _helper
3979 ldr x30, [sp, #8]
3980 add sp, sp, #16
3981 ret
3982 .cfi_endproc
3983
3984 .globl _helper
3985 .p2align 2
3986 _helper:
3987 .cfi_startproc
3988 ret
3989 .cfi_endproc
3990 .subsections_via_symbols
3991 "#;
3992 if let Err(e) = assemble(asm, &obj) {
3993 eprintln!("skipping: assemble failed: {e}");
3994 return;
3995 }
3996
3997 let opts = LinkOptions {
3998 inputs: vec![obj.clone()],
3999 output: Some(our_out.clone()),
4000 kind: OutputKind::Executable,
4001 ..LinkOptions::default()
4002 };
4003 Linker::run(&opts).unwrap();
4004 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4005
4006 let our_bytes = fs::read(&our_out).unwrap();
4007 let apple_bytes = fs::read(&apple_out).unwrap();
4008 assert!(output_section(&our_bytes, "__TEXT", "__eh_frame").is_some());
4009 assert_eq!(
4010 output_section(&our_bytes, "__TEXT", "__eh_frame")
4011 .unwrap()
4012 .1
4013 .len(),
4014 output_section(&apple_bytes, "__TEXT", "__eh_frame")
4015 .unwrap()
4016 .1
4017 .len()
4018 );
4019 let our_dump = normalized_eh_frame_dump(
4020 &our_out,
4021 output_section(&our_bytes, "__TEXT", "__text").unwrap().0,
4022 )
4023 .unwrap();
4024 let apple_dump = normalized_eh_frame_dump(
4025 &apple_out,
4026 output_section(&apple_bytes, "__TEXT", "__text").unwrap().0,
4027 )
4028 .unwrap();
4029 assert_eq!(our_dump, apple_dump);
4030
4031 let _ = fs::remove_file(obj);
4032 let _ = fs::remove_file(our_out);
4033 let _ = fs::remove_file(apple_out);
4034 }
4035
4036 #[test]
4037 fn linker_run_emits_backtrace_metadata_like_apple_ld() {
4038 if !have_xcrun() {
4039 eprintln!("skipping: xcrun unavailable");
4040 return;
4041 }
4042 let Some(sdk) = sdk_path() else {
4043 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4044 return;
4045 };
4046 let Some(sdk_ver) = sdk_version() else {
4047 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4048 return;
4049 };
4050 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4051 if !tbd.exists() {
4052 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4053 return;
4054 }
4055
4056 let obj = scratch("unwind-backtrace.o");
4057 let our_out = scratch("unwind-backtrace-ours.out");
4058 let apple_out = scratch("unwind-backtrace-apple.out");
4059 let src = r#"
4060 #include <unwind.h>
4061
4062 static _Unwind_Reason_Code cb(struct _Unwind_Context* ctx, void* arg) {
4063 (void)ctx;
4064 int* count = (int*)arg;
4065 (*count)++;
4066 return *count >= 8 ? _URC_END_OF_STACK : _URC_NO_REASON;
4067 }
4068
4069 __attribute__((noinline)) int helper(void) {
4070 int count = 0;
4071 _Unwind_Backtrace(cb, &count);
4072 return count;
4073 }
4074
4075 int main(void) {
4076 return helper() > 1 ? 0 : 1;
4077 }
4078 "#;
4079 if let Err(e) = compile_c(src, &obj) {
4080 eprintln!("skipping: clang compile failed: {e}");
4081 return;
4082 }
4083
4084 let opts = LinkOptions {
4085 inputs: vec![obj.clone(), tbd],
4086 output: Some(our_out.clone()),
4087 kind: OutputKind::Executable,
4088 ..LinkOptions::default()
4089 };
4090 Linker::run(&opts).unwrap();
4091 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4092
4093 let our_bytes = fs::read(&our_out).unwrap();
4094 let apple_bytes = fs::read(&apple_out).unwrap();
4095 assert_eq!(
4096 rebased_unwind_bytes(&our_bytes),
4097 rebased_unwind_bytes(&apple_bytes)
4098 );
4099 assert_eq!(
4100 normalize_function_start_offsets(&decode_function_starts(&our_bytes)),
4101 normalize_function_start_offsets(&decode_function_starts(&apple_bytes))
4102 );
4103
4104 let _ = fs::remove_file(obj);
4105 let _ = fs::remove_file(our_out);
4106 let _ = fs::remove_file(apple_out);
4107 }
4108
4109 #[test]
4110 fn linker_run_preserves_exception_unwind_metadata_like_apple_ld() {
4111 if !have_xcrun() || !have_xcrun_tool("clang++") || !have_tool("codesign") {
4112 eprintln!("skipping: xcrun clang++ or codesign unavailable");
4113 return;
4114 }
4115
4116 let Some(sdk) = sdk_path() else {
4117 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4118 return;
4119 };
4120 let libsystem = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4121 let libcxx = PathBuf::from(format!("{sdk}/usr/lib/libc++.tbd"));
4122 if !libsystem.exists() {
4123 eprintln!("skipping: no libSystem.tbd at {}", libsystem.display());
4124 return;
4125 }
4126 if !libcxx.exists() {
4127 eprintln!("skipping: no libc++.tbd at {}", libcxx.display());
4128 return;
4129 }
4130
4131 let obj = scratch("cxx-exc.o");
4132 let our_out = scratch("cxx-exc-ours.out");
4133 let apple_out = scratch("cxx-exc-apple.out");
4134 let src = r#"
4135 int helper() { throw 7; }
4136 int main() {
4137 try { return helper(); }
4138 catch (...) { return 42; }
4139 }
4140 "#;
4141 if let Err(e) = compile_cxx(src, &obj) {
4142 eprintln!("skipping: clang++ compile failed: {e}");
4143 return;
4144 }
4145
4146 let opts = LinkOptions {
4147 inputs: vec![obj.clone(), libcxx.clone(), libsystem.clone()],
4148 output: Some(our_out.clone()),
4149 kind: OutputKind::Executable,
4150 ..LinkOptions::default()
4151 };
4152 Linker::run(&opts).unwrap();
4153 apple_link_cxx_classic(&obj, &apple_out).unwrap();
4154
4155 let our_bytes = fs::read(&our_out).unwrap();
4156 let apple_bytes = fs::read(&apple_out).unwrap();
4157 assert_eq!(
4158 decode_bind_records(&our_bytes, false).unwrap(),
4159 decode_bind_records(&apple_bytes, false).unwrap()
4160 );
4161 assert_eq!(
4162 decode_bind_records(&our_bytes, true).unwrap(),
4163 decode_bind_records(&apple_bytes, true).unwrap()
4164 );
4165 assert_eq!(
4166 canonical_lazy_bind_stream(&our_bytes).unwrap(),
4167 canonical_lazy_bind_stream(&apple_bytes).unwrap()
4168 );
4169 let our_decoded = canonical_unwind_info(&our_bytes);
4170 let apple_decoded = canonical_unwind_info(&apple_bytes);
4171 assert_eq!(our_decoded, apple_decoded);
4172 assert_eq!(our_decoded.personalities.len(), 1);
4173 assert_eq!(our_decoded.lsdas.len(), 1);
4174 assert!(output_section(&our_bytes, "__TEXT", "__gcc_except_tab").is_some());
4175 let our_status = Command::new(&our_out).status().unwrap();
4176 let apple_status = Command::new(&apple_out).status().unwrap();
4177 assert_eq!(our_status.code(), Some(42));
4178 assert_eq!(apple_status.code(), Some(42));
4179
4180 let _ = fs::remove_file(obj);
4181 let _ = fs::remove_file(our_out);
4182 let _ = fs::remove_file(apple_out);
4183 }
4184
4185 #[test]
4186 fn linker_run_resolves_backtrace_symbols_at_runtime() {
4187 if !have_xcrun() {
4188 eprintln!("skipping: xcrun unavailable");
4189 return;
4190 }
4191 let Some(sdk) = sdk_path() else {
4192 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4193 return;
4194 };
4195 let Some(sdk_ver) = sdk_version() else {
4196 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4197 return;
4198 };
4199 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4200 if !tbd.exists() {
4201 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4202 return;
4203 }
4204
4205 let obj = scratch("execinfo-backtrace.o");
4206 let our_out = scratch("execinfo-backtrace-ours.out");
4207 let apple_out = scratch("execinfo-backtrace-apple.out");
4208 let src = r#"
4209 #include <execinfo.h>
4210 #include <stdio.h>
4211 #include <stdlib.h>
4212 #include <string.h>
4213
4214 __attribute__((noinline)) int helper(void) {
4215 void *frames[8];
4216 int n = backtrace(frames, 8);
4217 char **syms = backtrace_symbols(frames, n);
4218 int saw_helper = 0;
4219 int saw_main = 0;
4220 if (!syms) return 2;
4221 for (int i = 0; i < n; i++) {
4222 puts(syms[i]);
4223 saw_helper |= strstr(syms[i], "helper") != NULL;
4224 saw_main |= strstr(syms[i], "main") != NULL;
4225 }
4226 free(syms);
4227 return (saw_helper && saw_main) ? 0 : 1;
4228 }
4229
4230 int main(void) {
4231 return helper();
4232 }
4233 "#;
4234 if let Err(e) = compile_c(src, &obj) {
4235 eprintln!("skipping: clang compile failed: {e}");
4236 return;
4237 }
4238
4239 let opts = LinkOptions {
4240 inputs: vec![obj.clone(), tbd],
4241 output: Some(our_out.clone()),
4242 kind: OutputKind::Executable,
4243 ..LinkOptions::default()
4244 };
4245 Linker::run(&opts).unwrap();
4246 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4247
4248 let our_output = Command::new(&our_out).output().unwrap();
4249 let apple_output = Command::new(&apple_out).output().unwrap();
4250 let our_stdout = String::from_utf8_lossy(&our_output.stdout);
4251 let apple_stdout = String::from_utf8_lossy(&apple_output.stdout);
4252
4253 assert_eq!(our_output.status.code(), Some(0));
4254 assert_eq!(apple_output.status.code(), Some(0));
4255 assert!(our_stdout.contains("helper"), "expected helper in output: {our_stdout}");
4256 assert!(our_stdout.contains("main"), "expected main in output: {our_stdout}");
4257 assert!(
4258 apple_stdout.contains("helper"),
4259 "expected helper in apple output: {apple_stdout}"
4260 );
4261 assert!(
4262 apple_stdout.contains("main"),
4263 "expected main in apple output: {apple_stdout}"
4264 );
4265
4266 let _ = fs::remove_file(obj);
4267 let _ = fs::remove_file(our_out);
4268 let _ = fs::remove_file(apple_out);
4269 }
4270
4271 #[test]
4272 fn linker_run_emits_function_starts_like_ld() {
4273 if !have_xcrun() {
4274 eprintln!("skipping: xcrun unavailable");
4275 return;
4276 }
4277 let Some(sdk) = sdk_path() else {
4278 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4279 return;
4280 };
4281 let Some(sdk_ver) = sdk_version() else {
4282 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4283 return;
4284 };
4285 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4286 if !tbd.exists() {
4287 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4288 return;
4289 }
4290
4291 let obj = scratch("function-starts.o");
4292 let our_out = scratch("function-starts-ours.out");
4293 let apple_out = scratch("function-starts-apple.out");
4294 let asm = r#"
4295 .section __TEXT,__text,regular,pure_instructions
4296 .globl _main
4297 .p2align 2
4298 _main:
4299 adrp x0, _write@GOTPAGE
4300 ldr x0, [x0, _write@GOTPAGEOFF]
4301 bl _write
4302 ret
4303 .subsections_via_symbols
4304 "#;
4305 if let Err(e) = assemble(asm, &obj) {
4306 eprintln!("skipping: assemble failed: {e}");
4307 return;
4308 }
4309
4310 let opts = LinkOptions {
4311 inputs: vec![obj.clone(), tbd],
4312 output: Some(our_out.clone()),
4313 kind: OutputKind::Executable,
4314 ..LinkOptions::default()
4315 };
4316 Linker::run(&opts).unwrap();
4317 apple_link_classic_lazy(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4318
4319 let our_bytes = fs::read(&our_out).unwrap();
4320 let apple_bytes = fs::read(&apple_out).unwrap();
4321 let our_fstarts = raw_linkedit_data_cmd(&our_bytes, LC_FUNCTION_STARTS);
4322 let apple_fstarts = raw_linkedit_data_cmd(&apple_bytes, LC_FUNCTION_STARTS);
4323 assert_ne!(our_fstarts.0, 0);
4324 assert_eq!(our_fstarts.1, apple_fstarts.1);
4325 assert_eq!(our_fstarts.1, 8);
4326 assert!(output_section(&our_bytes, "__TEXT", "__stubs").is_some());
4327 assert!(output_section(&our_bytes, "__TEXT", "__stub_helper").is_some());
4328 assert_eq!(decode_function_starts(&our_bytes).len(), 1);
4329 assert_eq!(decode_function_starts(&apple_bytes).len(), 1);
4330 let our_text_addr = output_section(&our_bytes, "__TEXT", "__text").unwrap().0;
4331 let apple_text_addr = output_section(&apple_bytes, "__TEXT", "__text").unwrap().0;
4332 let our_text_base = segment_vmaddr(&our_bytes, "__TEXT").unwrap();
4333 let apple_text_base = segment_vmaddr(&apple_bytes, "__TEXT").unwrap();
4334 assert_eq!(
4335 decode_function_starts(&our_bytes),
4336 vec![our_text_addr - our_text_base]
4337 );
4338 assert_eq!(
4339 decode_function_starts(&apple_bytes),
4340 vec![apple_text_addr - apple_text_base]
4341 );
4342
4343 let our_dic = raw_linkedit_data_cmd(&our_bytes, LC_DATA_IN_CODE);
4344 let apple_dic = raw_linkedit_data_cmd(&apple_bytes, LC_DATA_IN_CODE);
4345 assert_ne!(our_dic.0, 0);
4346 assert_eq!(our_dic.1, apple_dic.1);
4347 assert_eq!(our_dic.0, our_fstarts.0 + our_fstarts.1);
4348 assert_eq!(apple_dic.0, apple_fstarts.0 + apple_fstarts.1);
4349
4350 let _ = fs::remove_file(obj);
4351 let _ = fs::remove_file(our_out);
4352 let _ = fs::remove_file(apple_out);
4353 }
4354
4355 #[test]
4356 fn linker_run_emits_function_starts_for_other_text_sections_like_ld() {
4357 if !have_xcrun() {
4358 eprintln!("skipping: xcrun unavailable");
4359 return;
4360 }
4361 let Some(sdk) = sdk_path() else {
4362 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4363 return;
4364 };
4365 let Some(sdk_ver) = sdk_version() else {
4366 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4367 return;
4368 };
4369
4370 let obj = scratch("function-starts-textcoal.o");
4371 let our_out = scratch("function-starts-textcoal-ours.out");
4372 let apple_out = scratch("function-starts-textcoal-apple.out");
4373 let asm = r#"
4374 .section __TEXT,__text,regular,pure_instructions
4375 .globl _main
4376 .p2align 2
4377 _main:
4378 ret
4379
4380 .section __TEXT,__textcoal_nt,regular,pure_instructions
4381 .globl _helper
4382 .p2align 2
4383 _helper:
4384 ret
4385 .subsections_via_symbols
4386 "#;
4387 if let Err(e) = assemble(asm, &obj) {
4388 eprintln!("skipping: assemble failed: {e}");
4389 return;
4390 }
4391
4392 let opts = LinkOptions {
4393 inputs: vec![obj.clone()],
4394 output: Some(our_out.clone()),
4395 kind: OutputKind::Executable,
4396 ..LinkOptions::default()
4397 };
4398 Linker::run(&opts).unwrap();
4399 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4400
4401 let our_bytes = fs::read(&our_out).unwrap();
4402 let apple_bytes = fs::read(&apple_out).unwrap();
4403 assert_eq!(decode_function_starts(&our_bytes).len(), 2);
4404 assert_eq!(decode_function_starts(&apple_bytes).len(), 2);
4405
4406 let our_text_addr = output_section(&our_bytes, "__TEXT", "__text").unwrap().0;
4407 let our_textcoal_addr = output_section(&our_bytes, "__TEXT", "__textcoal_nt")
4408 .unwrap()
4409 .0;
4410 let our_text_base = segment_vmaddr(&our_bytes, "__TEXT").unwrap();
4411 assert_eq!(
4412 decode_function_starts(&our_bytes),
4413 vec![
4414 our_text_addr - our_text_base,
4415 our_textcoal_addr - our_text_base
4416 ]
4417 );
4418
4419 let _ = fs::remove_file(obj);
4420 let _ = fs::remove_file(our_out);
4421 let _ = fs::remove_file(apple_out);
4422 }
4423
4424 #[test]
4425 fn linker_run_remaps_data_in_code_like_ld() {
4426 if !have_xcrun() {
4427 eprintln!("skipping: xcrun unavailable");
4428 return;
4429 }
4430 let Some(sdk) = sdk_path() else {
4431 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4432 return;
4433 };
4434 let Some(sdk_ver) = sdk_version() else {
4435 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4436 return;
4437 };
4438 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4439 if !tbd.exists() {
4440 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4441 return;
4442 }
4443
4444 let obj = scratch("data-in-code.o");
4445 let our_out = scratch("data-in-code-ours.out");
4446 let apple_out = scratch("data-in-code-apple.out");
4447 let asm = r#"
4448 .text
4449 .globl _main
4450 .p2align 2
4451 _main:
4452 mov w0, #0
4453 b Ldispatch
4454 .p2align 2
4455 Ltable:
4456 .data_region jt32
4457 .long Lcase0-Ltable
4458 .long Lcase1-Ltable
4459 .end_data_region
4460 Ldispatch:
4461 cmp w0, #0
4462 b.eq Lcase0
4463 b Lcase1
4464 Lcase0:
4465 mov w0, #1
4466 ret
4467 Lcase1:
4468 mov w0, #2
4469 ret
4470 .subsections_via_symbols
4471 "#;
4472 if let Err(e) = assemble(asm, &obj) {
4473 eprintln!("skipping: assemble failed: {e}");
4474 return;
4475 }
4476
4477 let opts = LinkOptions {
4478 inputs: vec![obj.clone(), tbd],
4479 output: Some(our_out.clone()),
4480 kind: OutputKind::Executable,
4481 ..LinkOptions::default()
4482 };
4483 Linker::run(&opts).unwrap();
4484 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4485
4486 let our_bytes = fs::read(&our_out).unwrap();
4487 let apple_bytes = fs::read(&apple_out).unwrap();
4488 let our_dic = raw_linkedit_data_cmd(&our_bytes, LC_DATA_IN_CODE);
4489 let apple_dic = raw_linkedit_data_cmd(&apple_bytes, LC_DATA_IN_CODE);
4490 assert_ne!(our_dic.1, 0);
4491 assert_eq!(our_dic.1, apple_dic.1);
4492 assert_eq!(decode_data_in_code(&our_bytes).len(), 1);
4493 assert_eq!(
4494 canonical_data_in_code(&our_bytes),
4495 canonical_data_in_code(&apple_bytes)
4496 );
4497 assert_eq!(
4498 canonical_data_in_code(&our_bytes),
4499 vec![DataInCodeRecord {
4500 offset: 8,
4501 length: 8,
4502 kind: DICE_KIND_JUMP_TABLE32,
4503 }]
4504 );
4505
4506 let _ = fs::remove_file(obj);
4507 let _ = fs::remove_file(our_out);
4508 let _ = fs::remove_file(apple_out);
4509 }
4510
4511 #[test]
4512 fn linker_run_remaps_data_in_code_in_later_text_section_like_ld() {
4513 if !have_xcrun() {
4514 eprintln!("skipping: xcrun unavailable");
4515 return;
4516 }
4517 let Some(sdk) = sdk_path() else {
4518 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4519 return;
4520 };
4521 let Some(sdk_ver) = sdk_version() else {
4522 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4523 return;
4524 };
4525 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4526 if !tbd.exists() {
4527 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4528 return;
4529 }
4530
4531 let obj = scratch("data-in-code-late.o");
4532 let our_out = scratch("data-in-code-late-ours.out");
4533 let apple_out = scratch("data-in-code-late-apple.out");
4534 let asm = r#"
4535 .text
4536 .globl _main
4537 .p2align 2
4538 _main:
4539 ret
4540
4541 .section __TEXT,__text2,regular,pure_instructions
4542 .globl _helper
4543 .p2align 2
4544 _helper:
4545 b Ldispatch
4546 .p2align 2
4547 Ltable:
4548 .data_region jt32
4549 .long Lcase0-Ltable
4550 .long Lcase1-Ltable
4551 .end_data_region
4552 Ldispatch:
4553 ret
4554 Lcase0:
4555 ret
4556 Lcase1:
4557 ret
4558 .subsections_via_symbols
4559 "#;
4560 if let Err(e) = assemble(asm, &obj) {
4561 eprintln!("skipping: assemble failed: {e}");
4562 return;
4563 }
4564
4565 let opts = LinkOptions {
4566 inputs: vec![obj.clone(), tbd],
4567 output: Some(our_out.clone()),
4568 kind: OutputKind::Executable,
4569 ..LinkOptions::default()
4570 };
4571 Linker::run(&opts).unwrap();
4572 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4573
4574 let our_bytes = fs::read(&our_out).unwrap();
4575 let apple_bytes = fs::read(&apple_out).unwrap();
4576 assert_eq!(
4577 canonical_data_in_code(&our_bytes),
4578 canonical_data_in_code(&apple_bytes)
4579 );
4580 assert_eq!(
4581 canonical_data_in_code(&our_bytes),
4582 vec![DataInCodeRecord {
4583 offset: 8,
4584 length: 8,
4585 kind: DICE_KIND_JUMP_TABLE32,
4586 }]
4587 );
4588
4589 let _ = fs::remove_file(obj);
4590 let _ = fs::remove_file(our_out);
4591 let _ = fs::remove_file(apple_out);
4592 }
4593
4594 #[test]
4595 fn linker_run_remaps_data_in_code_after_large_first_text_section_like_ld() {
4596 if !have_xcrun() {
4597 eprintln!("skipping: xcrun unavailable");
4598 return;
4599 }
4600 let Some(sdk) = sdk_path() else {
4601 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4602 return;
4603 };
4604 let Some(sdk_ver) = sdk_version() else {
4605 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4606 return;
4607 };
4608 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4609 if !tbd.exists() {
4610 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4611 return;
4612 }
4613
4614 let obj = scratch("data-in-code-large-first.o");
4615 let our_out = scratch("data-in-code-large-first-ours.out");
4616 let apple_out = scratch("data-in-code-large-first-apple.out");
4617 let asm = r#"
4618 .text
4619 .globl _main
4620 .p2align 2
4621 _main:
4622 nop
4623 nop
4624 nop
4625 nop
4626 nop
4627 ret
4628
4629 .section __TEXT,__text2,regular,pure_instructions
4630 .globl _helper
4631 _helper:
4632 b Ldispatch
4633 .p2align 2
4634 Ltable:
4635 .data_region jt32
4636 .long Lcase0-Ltable
4637 .long Lcase1-Ltable
4638 .end_data_region
4639 Ldispatch:
4640 ret
4641 Lcase0:
4642 ret
4643 Lcase1:
4644 ret
4645 .subsections_via_symbols
4646 "#;
4647 if let Err(e) = assemble(asm, &obj) {
4648 eprintln!("skipping: assemble failed: {e}");
4649 return;
4650 }
4651
4652 let opts = LinkOptions {
4653 inputs: vec![obj.clone(), tbd],
4654 output: Some(our_out.clone()),
4655 kind: OutputKind::Executable,
4656 ..LinkOptions::default()
4657 };
4658 Linker::run(&opts).unwrap();
4659 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4660
4661 let our_bytes = fs::read(&our_out).unwrap();
4662 let apple_bytes = fs::read(&apple_out).unwrap();
4663 assert_eq!(
4664 canonical_data_in_code(&our_bytes),
4665 canonical_data_in_code(&apple_bytes)
4666 );
4667 assert_eq!(
4668 canonical_data_in_code(&our_bytes),
4669 vec![DataInCodeRecord {
4670 offset: 28,
4671 length: 8,
4672 kind: DICE_KIND_JUMP_TABLE32,
4673 }]
4674 );
4675
4676 let _ = fs::remove_file(obj);
4677 let _ = fs::remove_file(our_out);
4678 let _ = fs::remove_file(apple_out);
4679 }
4680
4681 #[test]
4682 fn linker_run_dedups_output_strtab_like_ld() {
4683 if !have_xcrun() {
4684 eprintln!("skipping: xcrun unavailable");
4685 return;
4686 }
4687 let Some(sdk) = sdk_path() else {
4688 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4689 return;
4690 };
4691 let Some(sdk_ver) = sdk_version() else {
4692 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4693 return;
4694 };
4695 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4696 if !tbd.exists() {
4697 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4698 return;
4699 }
4700
4701 let obj = scratch("strtab-dedup.o");
4702 let our_out = scratch("strtab-dedup-ours.out");
4703 let apple_out = scratch("strtab-dedup-apple.out");
4704 let mut asm =
4705 String::from(" .text\n .globl _afs_array_sum\n .globl _main\n");
4706 for idx in 0..20 {
4707 let symbol = format!("_pad_symbol_{idx:02}");
4708 asm.push_str(&format!(" .globl {symbol}\n"));
4709 }
4710 asm.push_str(" .p2align 2\n");
4711 asm.push_str(" _array_sum:\n ret\n");
4712 asm.push_str(" _afs_array_sum:\n ret\n");
4713 for idx in 0..20 {
4714 let symbol = format!("_pad_symbol_{idx:02}");
4715 asm.push_str(&format!(" {symbol}:\n ret\n"));
4716 }
4717 asm.push_str(" _main:\n bl _afs_array_sum\n ret\n");
4718 asm.push_str(" .subsections_via_symbols\n");
4719 if let Err(e) = assemble(&asm, &obj) {
4720 eprintln!("skipping: assemble failed: {e}");
4721 return;
4722 }
4723
4724 let opts = LinkOptions {
4725 inputs: vec![obj.clone(), tbd],
4726 output: Some(our_out.clone()),
4727 kind: OutputKind::Executable,
4728 ..LinkOptions::default()
4729 };
4730 Linker::run(&opts).unwrap();
4731 apple_link(&obj, &apple_out, "_main", &sdk, &sdk_ver).unwrap();
4732
4733 let our_bytes = fs::read(&our_out).unwrap();
4734 let apple_bytes = fs::read(&apple_out).unwrap();
4735 assert_eq!(
4736 canonical_symbol_records(&our_bytes),
4737 canonical_symbol_records(&apple_bytes)
4738 );
4739 let our_strtab = raw_string_table(&our_bytes);
4740 let apple_strtab = raw_string_table(&apple_bytes);
4741 assert_strtab_within_five_percent(&our_strtab, &apple_strtab);
4742 assert!(
4743 our_strtab.len() <= apple_strtab.len(),
4744 "suffix dedup should not grow the output string table: ours={} apple={}",
4745 our_strtab.len(),
4746 apple_strtab.len()
4747 );
4748
4749 let offsets = symbol_name_offsets(&our_bytes);
4750 assert_eq!(offsets["_array_sum"], offsets["_afs_array_sum"] + 4);
4751
4752 let _ = fs::remove_file(obj);
4753 let _ = fs::remove_file(our_out);
4754 let _ = fs::remove_file(apple_out);
4755 }
4756
4757 #[test]
4758 fn linker_run_launches_with_classic_lazy_dylib_import() {
4759 if !have_xcrun() || !have_tool("codesign") {
4760 eprintln!("skipping: xcrun clang or codesign unavailable");
4761 return;
4762 }
4763 let Some(sdk) = sdk_path() else {
4764 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4765 return;
4766 };
4767 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4768 if !tbd.exists() {
4769 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4770 return;
4771 }
4772
4773 let dylib = scratch("lazy-runtime.dylib");
4774 let obj = scratch("lazy-runtime.o");
4775 let out = scratch("lazy-runtime.out");
4776
4777 let dylib_src = r#"
4778 int ext_fn(void) { return 7; }
4779 "#;
4780 if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
4781 eprintln!("skipping: dylib compile failed: {e}");
4782 return;
4783 }
4784
4785 let main_src = r#"
4786 int ext_fn(void);
4787 int main(void) { return ext_fn() == 7 ? 0 : 1; }
4788 "#;
4789 if let Err(e) = compile_c(main_src, &obj) {
4790 eprintln!("skipping: compile failed: {e}");
4791 return;
4792 }
4793
4794 let opts = LinkOptions {
4795 inputs: vec![obj.clone(), tbd, dylib.clone()],
4796 output: Some(out.clone()),
4797 kind: OutputKind::Executable,
4798 ..LinkOptions::default()
4799 };
4800 Linker::run(&opts).unwrap();
4801
4802 let verify = Command::new("codesign")
4803 .arg("-v")
4804 .arg(&out)
4805 .output()
4806 .unwrap();
4807 assert!(
4808 verify.status.success(),
4809 "codesign verify failed: {}",
4810 String::from_utf8_lossy(&verify.stderr)
4811 );
4812 let status = Command::new(&out).status().unwrap();
4813 assert_eq!(
4814 status.code(),
4815 Some(0),
4816 "expected dylib-import executable to exit 0"
4817 );
4818
4819 let _ = fs::remove_file(dylib);
4820 let _ = fs::remove_file(obj);
4821 let _ = fs::remove_file(out);
4822 }
4823
4824 #[test]
4825 fn linker_run_handles_local_tlv_descriptors() {
4826 if !have_xcrun() || !have_tool("codesign") {
4827 eprintln!("skipping: xcrun clang or codesign unavailable");
4828 return;
4829 }
4830 let Some(sdk) = sdk_path() else {
4831 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4832 return;
4833 };
4834 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4835 if !tbd.exists() {
4836 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4837 return;
4838 }
4839
4840 let obj = scratch("tlvp-local.o");
4841 let out = scratch("tlvp-local.out");
4842 let src = r#"
4843 __thread long tls_a = 7;
4844 __thread long tls_b;
4845
4846 static long tls_sum(void) {
4847 return tls_a + tls_b;
4848 }
4849
4850 int main(void) {
4851 return tls_sum() == 7 ? 0 : 1;
4852 }
4853 "#;
4854 if let Err(e) = compile_c(src, &obj) {
4855 eprintln!("skipping: compile failed: {e}");
4856 return;
4857 }
4858
4859 let opts = LinkOptions {
4860 inputs: vec![obj.clone(), tbd],
4861 output: Some(out.clone()),
4862 kind: OutputKind::Executable,
4863 ..LinkOptions::default()
4864 };
4865 Linker::run(&opts).unwrap();
4866
4867 let bytes = fs::read(&out).unwrap();
4868 let (_, thread_vars) = output_section(&bytes, "__DATA", "__thread_vars").unwrap();
4869 let (_, thread_data) = output_section(&bytes, "__DATA", "__thread_data").unwrap();
4870 assert!(output_section(&bytes, "__DATA", "__thread_ptrs").is_none());
4871 assert_eq!(thread_vars.len(), 48);
4872 assert_eq!(thread_data.len(), 8);
4873 assert_eq!(
4874 u64::from_le_bytes(thread_vars[16..24].try_into().unwrap()),
4875 0
4876 );
4877 assert_eq!(
4878 u64::from_le_bytes(thread_vars[40..48].try_into().unwrap()),
4879 8
4880 );
4881
4882 let binds = decode_bind_records(&bytes, false).unwrap();
4883 let mut tlv_binds: Vec<_> = binds
4884 .into_iter()
4885 .filter(|record| record.section == "__thread_vars" && record.symbol == "__tlv_bootstrap")
4886 .collect();
4887 tlv_binds.sort_by_key(|record| record.section_offset);
4888 assert_eq!(tlv_binds.len(), 2);
4889 assert_eq!(tlv_binds[0].section_offset, 0);
4890 assert_eq!(tlv_binds[1].section_offset, 24);
4891
4892 let header = parse_header(&bytes).unwrap();
4893 let commands = parse_commands(&header, &bytes).unwrap();
4894 let symtab = commands
4895 .iter()
4896 .find_map(|cmd| match cmd {
4897 LoadCommand::Symtab(cmd) => Some(*cmd),
4898 _ => None,
4899 })
4900 .unwrap();
4901 let symbols = parse_nlist_table(&bytes, symtab.symoff, symtab.nsyms).unwrap();
4902 let strings = StringTable::from_file(&bytes, symtab.stroff, symtab.strsize).unwrap();
4903 let symbol_names: Vec<&str> = symbols
4904 .iter()
4905 .map(|symbol| strings.get(symbol.strx()).unwrap())
4906 .collect();
4907 assert!(symbol_names.contains(&"__tlv_bootstrap"));
4908
4909 let verify = Command::new("codesign")
4910 .arg("-v")
4911 .arg(&out)
4912 .output()
4913 .unwrap();
4914 assert!(
4915 verify.status.success(),
4916 "codesign verify failed: {}",
4917 String::from_utf8_lossy(&verify.stderr)
4918 );
4919 let status = Command::new(&out).status().unwrap();
4920 assert_eq!(status.code(), Some(0), "expected TLV executable to exit 0");
4921
4922 let _ = fs::remove_file(obj);
4923 let _ = fs::remove_file(out);
4924 }
4925
4926 #[test]
4927 fn linker_run_routes_imported_tlv_through_got() {
4928 if !have_xcrun() || !have_tool("codesign") {
4929 eprintln!("skipping: xcrun clang or codesign unavailable");
4930 return;
4931 }
4932 let Some(sdk) = sdk_path() else {
4933 eprintln!("skipping: xcrun --show-sdk-path unavailable");
4934 return;
4935 };
4936 let Some(sdk_ver) = sdk_version() else {
4937 eprintln!("skipping: xcrun --show-sdk-version unavailable");
4938 return;
4939 };
4940 let tbd = PathBuf::from(format!("{sdk}/usr/lib/libSystem.tbd"));
4941 if !tbd.exists() {
4942 eprintln!("skipping: no libSystem.tbd at {}", tbd.display());
4943 return;
4944 }
4945
4946 let dylib = scratch("libtlvprobe.dylib");
4947 let obj = scratch("imported-tlv.o");
4948 let our_out = scratch("imported-tlv-ours.out");
4949 let apple_out = scratch("imported-tlv-apple.out");
4950
4951 let dylib_src = r#"
4952 __thread long ext_tls = 5;
4953 long read_lib_tls(void) { return ext_tls; }
4954 "#;
4955 if let Err(e) = compile_dylib_c(dylib_src, &dylib) {
4956 eprintln!("skipping: dylib compile failed: {e}");
4957 return;
4958 }
4959
4960 let main_src = r#"
4961 extern __thread long ext_tls;
4962 int main(void) { return ext_tls == 5 ? 0 : 1; }
4963 "#;
4964 if let Err(e) = compile_c(main_src, &obj) {
4965 eprintln!("skipping: compile failed: {e}");
4966 return;
4967 }
4968
4969 let opts = LinkOptions {
4970 inputs: vec![obj.clone(), tbd.clone(), dylib.clone()],
4971 output: Some(our_out.clone()),
4972 kind: OutputKind::Executable,
4973 ..LinkOptions::default()
4974 };
4975 Linker::run(&opts).unwrap();
4976
4977 let apple = Command::new("xcrun")
4978 .args([
4979 "ld",
4980 "-arch",
4981 "arm64",
4982 "-platform_version",
4983 "macos",
4984 &sdk_ver,
4985 &sdk_ver,
4986 "-syslibroot",
4987 &sdk,
4988 "-no_fixup_chains",
4989 "-lSystem",
4990 "-e",
4991 "_main",
4992 "-o",
4993 ])
4994 .arg(&apple_out)
4995 .arg(&obj)
4996 .arg(&dylib)
4997 .output()
4998 .unwrap();
4999 assert!(
5000 apple.status.success(),
5001 "xcrun ld failed: {}",
5002 String::from_utf8_lossy(&apple.stderr)
5003 );
5004
5005 let our_bytes = fs::read(&our_out).unwrap();
5006 let apple_bytes = fs::read(&apple_out).unwrap();
5007 let (our_text_addr, our_text) = output_section(&our_bytes, "__TEXT", "__text").unwrap();
5008 let (apple_text_addr, apple_text) = output_section(&apple_bytes, "__TEXT", "__text").unwrap();
5009 let (our_got_addr, our_got) = output_section(&our_bytes, "__DATA_CONST", "__got").unwrap();
5010 let (apple_got_addr, apple_got) =
5011 output_section(&apple_bytes, "__DATA_CONST", "__got").unwrap();
5012
5013 assert!(output_section(&our_bytes, "__DATA", "__thread_ptrs").is_none());
5014 assert!(output_section(&apple_bytes, "__DATA", "__thread_ptrs").is_none());
5015 assert_eq!(our_got.len(), 8);
5016 assert_eq!(our_got, apple_got);
5017 assert_eq!(
5018 decode_page_reference(&our_text, our_text_addr, 20, &PageRefKind::Load).unwrap(),
5019 our_got_addr
5020 );
5021 assert_eq!(
5022 decode_page_reference(&apple_text, apple_text_addr, 20, &PageRefKind::Load).unwrap(),
5023 apple_got_addr
5024 );
5025 assert_eq!(our_text, apple_text);
5026 assert_eq!(read_insn(&our_text, 24).unwrap(), 0xf9400000);
5027 assert_eq!(read_insn(&our_text, 28).unwrap(), 0xf9400008);
5028 assert_eq!(read_insn(&our_text, 32).unwrap(), 0xd63f0100);
5029 assert_eq!(
5030 decode_bind_records(&our_bytes, false).unwrap(),
5031 decode_bind_records(&apple_bytes, false).unwrap()
5032 );
5033 assert_eq!(
5034 load_dylib_names(&our_bytes).unwrap(),
5035 load_dylib_names(&apple_bytes).unwrap()
5036 );
5037 let verify = Command::new("codesign")
5038 .arg("-v")
5039 .arg(&our_out)
5040 .output()
5041 .unwrap();
5042 assert!(
5043 verify.status.success(),
5044 "codesign verify failed: {}",
5045 String::from_utf8_lossy(&verify.stderr)
5046 );
5047 let status = Command::new(&our_out).status().unwrap();
5048 assert_eq!(
5049 status.code(),
5050 Some(0),
5051 "expected imported TLV executable to exit 0"
5052 );
5053
5054 let _ = fs::remove_file(dylib);
5055 let _ = fs::remove_file(obj);
5056 let _ = fs::remove_file(our_out);
5057 let _ = fs::remove_file(apple_out);
5058 }
5059