Rust · 44609 bytes Raw Blame History
1 //! afs-ld — standalone ARM64 Mach-O linker.
2 //!
3 //! Sprint 0 scaffolding: public surface is declared but every link attempt
4 //! returns `LinkError::NotYetImplemented`. Subsequent sprints fill in the
5 //! reader, resolver, layout, reloc, synth, writer, and signing paths.
6
7 pub mod archive;
8 pub mod args;
9 pub mod atom;
10 pub mod diag;
11 pub mod dump;
12 pub mod icf;
13 pub mod input;
14 pub mod layout;
15 pub mod leb;
16 pub mod link_map;
17 pub mod loh;
18 pub mod macho;
19 pub mod reloc;
20 pub mod resolve;
21 pub mod section;
22 pub mod string_table;
23 pub mod symbol;
24 pub mod synth;
25 pub mod why_live;
26
27 use std::os::unix::fs::PermissionsExt;
28 use std::path::PathBuf;
29 use std::sync::{mpsc, Arc, Mutex};
30 use std::thread;
31 use std::time::{Duration, Instant};
32 use std::{collections::VecDeque, fs, io};
33
34 use archive::Archive;
35 use atom::{atomize_object, backpatch_symbol_atoms, AtomTable};
36 use icf::IcfError;
37 use input::ObjectFile;
38 use layout::{ExtraLayoutSections, Layout, LayoutInput};
39 use macho::dylib::{DylibDependency, DylibFile, DylibLoadKind};
40 use macho::reader::ReadError;
41 use macho::tbd::{
42 parse_tbd_for_target, parse_tbd_metadata_for_target, parse_version, Arch, Platform, Target,
43 };
44 use reloc::arm64::RelocError;
45 use resolve::{
46 classify_unresolved, drain_fetches, find_archive_by_path, force_load_all, force_load_archive,
47 format_duplicate_diagnostic, format_undefined_diagnostic, format_undefined_warning_diagnostic,
48 seed_all, DrainReport, DylibLoadMeta, InputAddError, InputId, Inputs, Symbol, SymbolTable,
49 UndefinedTreatment,
50 };
51 use symbol::SymKind;
52
53 const DEFAULT_TBD_VERSION: u32 = 1 << 16;
54 const THUNK_PLAN_MAX_ITERATIONS: usize = 16;
55
56 /// What kind of Mach-O file the linker is producing.
57 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
58 pub enum OutputKind {
59 Executable,
60 Dylib,
61 }
62
63 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
64 pub enum IcfMode {
65 None,
66 Safe,
67 All,
68 }
69
70 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
71 pub enum ThunkMode {
72 None,
73 Safe,
74 All,
75 }
76
77 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
78 pub struct PlatformVersion {
79 pub minos: u32,
80 pub sdk: u32,
81 }
82
83 #[derive(Debug, Clone, PartialEq, Eq)]
84 pub struct FrameworkSpec {
85 pub name: String,
86 pub weak: bool,
87 }
88
89 /// User-facing linker configuration, populated by the CLI parser.
90 #[derive(Debug, Clone)]
91 pub struct LinkOptions {
92 pub inputs: Vec<PathBuf>,
93 pub library_names: Vec<String>,
94 pub frameworks: Vec<FrameworkSpec>,
95 pub search_paths: Vec<PathBuf>,
96 pub syslibroot: Option<PathBuf>,
97 pub platform_version: Option<PlatformVersion>,
98 pub undefined_treatment: UndefinedTreatment,
99 pub rpaths: Vec<String>,
100 pub install_name: Option<String>,
101 pub current_version: Option<u32>,
102 pub compatibility_version: Option<u32>,
103 pub exported_symbols_lists: Vec<PathBuf>,
104 pub unexported_symbols_lists: Vec<PathBuf>,
105 pub exported_symbols: Vec<String>,
106 pub unexported_symbols: Vec<String>,
107 pub map: Option<PathBuf>,
108 pub why_live: Vec<String>,
109 pub trace_inputs: bool,
110 pub show_version: bool,
111 pub show_help: bool,
112 pub output: Option<PathBuf>,
113 pub entry: Option<String>,
114 pub arch: Option<String>,
115 pub relocatable: bool,
116 pub bundle: bool,
117 pub objc_force_load: bool,
118 pub strip_locals: bool,
119 pub strip_debug: bool,
120 pub emit_uuid: bool,
121 pub dead_strip: bool,
122 pub no_loh: bool,
123 pub icf_mode: IcfMode,
124 pub thunks: ThunkMode,
125 pub fixup_chains: bool,
126 pub all_load: bool,
127 pub force_load_archives: Vec<PathBuf>,
128 pub jobs: Option<usize>,
129 pub kind: OutputKind,
130 /// When set, afs-ld operates in dump mode and prints the given file's
131 /// header + load commands instead of linking.
132 pub dump: Option<PathBuf>,
133 /// When set, afs-ld dumps the named static archive's structure.
134 pub dump_archive: Option<PathBuf>,
135 /// When set, afs-ld dumps the named MH_DYLIB's load commands + exports.
136 pub dump_dylib: Option<PathBuf>,
137 /// When set, afs-ld dumps the named TAPI TBD stub (all documents).
138 pub dump_tbd: Option<PathBuf>,
139 }
140
141 impl Default for LinkOptions {
142 fn default() -> Self {
143 Self {
144 inputs: Vec::new(),
145 library_names: Vec::new(),
146 frameworks: Vec::new(),
147 search_paths: Vec::new(),
148 syslibroot: None,
149 platform_version: None,
150 undefined_treatment: UndefinedTreatment::Error,
151 rpaths: Vec::new(),
152 install_name: None,
153 current_version: None,
154 compatibility_version: None,
155 exported_symbols_lists: Vec::new(),
156 unexported_symbols_lists: Vec::new(),
157 exported_symbols: Vec::new(),
158 unexported_symbols: Vec::new(),
159 map: None,
160 why_live: Vec::new(),
161 trace_inputs: false,
162 show_version: false,
163 show_help: false,
164 output: None,
165 entry: None,
166 arch: None,
167 relocatable: false,
168 bundle: false,
169 objc_force_load: false,
170 strip_locals: false,
171 strip_debug: false,
172 emit_uuid: true,
173 dead_strip: false,
174 no_loh: false,
175 icf_mode: IcfMode::None,
176 thunks: ThunkMode::Safe,
177 fixup_chains: false,
178 all_load: false,
179 force_load_archives: Vec::new(),
180 jobs: None,
181 kind: OutputKind::Executable,
182 dump: None,
183 dump_archive: None,
184 dump_dylib: None,
185 dump_tbd: None,
186 }
187 }
188 }
189
190 impl LinkOptions {
191 pub fn parallel_jobs(&self) -> usize {
192 self.jobs
193 .unwrap_or_else(|| {
194 thread::available_parallelism()
195 .map(usize::from)
196 .unwrap_or(1)
197 })
198 .max(1)
199 }
200 }
201
202 #[derive(Debug)]
203 pub enum LinkError {
204 /// No input files were provided on the command line.
205 NoInputs,
206 Io(io::Error),
207 Input(InputAddError),
208 Seed(resolve::SeedError),
209 Fetch(resolve::FetchError),
210 Write(macho::writer::WriteError),
211 Tbd(macho::tbd::TbdError),
212 Reloc(RelocError),
213 Synth(synth::SynthError),
214 Unwind(synth::unwind::UnwindError),
215 Icf(IcfError),
216 Loh(loh::LohError),
217 DuplicateSymbols(String),
218 UndefinedSymbols(String),
219 UnsupportedArch(String),
220 NoTbdDocument(PathBuf),
221 EntrySymbolNotFound(String),
222 ForceLoadNotArchive(PathBuf),
223 LibraryNotFound(String),
224 FrameworkNotFound(String),
225 ThunkPlanningDidNotConverge,
226 WhyLive(String),
227 UnsupportedOption(String),
228 }
229
230 #[derive(Debug, Clone, Default, PartialEq, Eq)]
231 pub struct LinkPhaseTimings {
232 pub input_parsing: Duration,
233 pub input_read: Duration,
234 pub input_object_parse: Duration,
235 pub input_archive_parse: Duration,
236 pub input_dylib_parse: Duration,
237 pub input_tbd_decode: Duration,
238 pub input_tbd_materialize: Duration,
239 pub input_reloc_parse: Duration,
240 pub symbol_resolution: Duration,
241 pub atomization: Duration,
242 pub layout: Duration,
243 pub layout_entry_lookup: Duration,
244 pub layout_dead_strip: Duration,
245 pub layout_icf: Duration,
246 pub layout_synthetic_plan: Duration,
247 pub layout_build: Duration,
248 pub layout_thunk_plan: Duration,
249 pub synth_sections: Duration,
250 pub synth_linkedit_finalize: Duration,
251 pub synth_linkedit_symbol_plan: Duration,
252 pub synth_linkedit_symbol_plan_locals: Duration,
253 pub synth_linkedit_symbol_plan_globals: Duration,
254 pub synth_linkedit_symbol_plan_strtab: Duration,
255 pub synth_linkedit_dyld_info: Duration,
256 pub synth_linkedit_dyld_bind: Duration,
257 pub synth_linkedit_dyld_rebase: Duration,
258 pub synth_linkedit_dyld_export: Duration,
259 pub synth_linkedit_metadata_tables: Duration,
260 pub synth_linkedit_code_signature: Duration,
261 pub synth_unwind: Duration,
262 pub reloc_apply: Duration,
263 pub write_output: Duration,
264 }
265
266 impl LinkPhaseTimings {
267 pub fn accounted_total(&self) -> Duration {
268 self.input_parsing
269 + self.symbol_resolution
270 + self.atomization
271 + self.layout
272 + self.synth_sections
273 + self.reloc_apply
274 + self.write_output
275 }
276
277 fn add_input_load(&mut self, timings: InputLoadTimings) {
278 self.input_read += timings.read;
279 self.input_object_parse += timings.object_parse;
280 self.input_archive_parse += timings.archive_parse;
281 self.input_dylib_parse += timings.dylib_parse;
282 self.input_tbd_decode += timings.tbd_decode;
283 self.input_tbd_materialize += timings.tbd_materialize;
284 }
285 }
286
287 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
288 struct InputLoadTimings {
289 read: Duration,
290 object_parse: Duration,
291 archive_parse: Duration,
292 dylib_parse: Duration,
293 tbd_decode: Duration,
294 tbd_materialize: Duration,
295 }
296
297 #[derive(Debug, Clone, PartialEq, Eq)]
298 pub struct LinkProfile {
299 pub output: PathBuf,
300 pub phases: LinkPhaseTimings,
301 pub total_wall: Duration,
302 }
303
304 impl std::fmt::Display for LinkError {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 match self {
307 LinkError::NoInputs => write!(f, "no input files"),
308 LinkError::Io(e) => write!(f, "{e}"),
309 LinkError::Input(e) => write!(f, "{e}"),
310 LinkError::Seed(e) => write!(f, "{e}"),
311 LinkError::Fetch(e) => write!(f, "{e}"),
312 LinkError::Write(e) => write!(f, "{e}"),
313 LinkError::Tbd(e) => write!(f, "{e}"),
314 LinkError::Reloc(e) => write!(f, "{e}"),
315 LinkError::Synth(e) => write!(f, "{e}"),
316 LinkError::Unwind(e) => write!(f, "{e}"),
317 LinkError::Icf(e) => write!(f, "{e}"),
318 LinkError::Loh(e) => write!(f, "{e}"),
319 LinkError::DuplicateSymbols(msg) | LinkError::UndefinedSymbols(msg) => {
320 write!(f, "{msg}")
321 }
322 LinkError::UnsupportedArch(arch) => {
323 write!(f, "unsupported arch `{arch}` (afs-ld requires arm64)")
324 }
325 LinkError::NoTbdDocument(path) => {
326 write!(f, "{}: no arm64-macos TBD document found", path.display())
327 }
328 LinkError::EntrySymbolNotFound(name) => {
329 write!(f, "entry symbol `{name}` was not found in linked objects")
330 }
331 LinkError::ForceLoadNotArchive(path) => {
332 write!(
333 f,
334 "{}: -force_load requires a path that is also present as an archive input",
335 path.display()
336 )
337 }
338 LinkError::LibraryNotFound(name) => {
339 write!(f, "unable to find library `{name}`")
340 }
341 LinkError::FrameworkNotFound(name) => {
342 write!(f, "unable to find framework `{name}`")
343 }
344 LinkError::ThunkPlanningDidNotConverge => {
345 write!(f, "thunk planning did not converge")
346 }
347 LinkError::WhyLive(msg) => write!(f, "{msg}"),
348 LinkError::UnsupportedOption(msg) => write!(f, "{msg}"),
349 }
350 }
351 }
352
353 impl std::error::Error for LinkError {}
354
355 impl From<io::Error> for LinkError {
356 fn from(value: io::Error) -> Self {
357 LinkError::Io(value)
358 }
359 }
360
361 impl From<InputAddError> for LinkError {
362 fn from(value: InputAddError) -> Self {
363 LinkError::Input(value)
364 }
365 }
366
367 impl From<ReadError> for LinkError {
368 fn from(value: ReadError) -> Self {
369 LinkError::Input(InputAddError::from(value))
370 }
371 }
372
373 impl From<resolve::SeedError> for LinkError {
374 fn from(value: resolve::SeedError) -> Self {
375 LinkError::Seed(value)
376 }
377 }
378
379 impl From<resolve::FetchError> for LinkError {
380 fn from(value: resolve::FetchError) -> Self {
381 LinkError::Fetch(value)
382 }
383 }
384
385 impl From<macho::writer::WriteError> for LinkError {
386 fn from(value: macho::writer::WriteError) -> Self {
387 LinkError::Write(value)
388 }
389 }
390
391 impl From<macho::tbd::TbdError> for LinkError {
392 fn from(value: macho::tbd::TbdError) -> Self {
393 LinkError::Tbd(value)
394 }
395 }
396
397 impl From<RelocError> for LinkError {
398 fn from(value: RelocError) -> Self {
399 LinkError::Reloc(value)
400 }
401 }
402
403 impl From<synth::SynthError> for LinkError {
404 fn from(value: synth::SynthError) -> Self {
405 LinkError::Synth(value)
406 }
407 }
408
409 impl From<synth::unwind::UnwindError> for LinkError {
410 fn from(value: synth::unwind::UnwindError) -> Self {
411 LinkError::Unwind(value)
412 }
413 }
414
415 impl From<IcfError> for LinkError {
416 fn from(value: IcfError) -> Self {
417 LinkError::Icf(value)
418 }
419 }
420
421 impl From<loh::LohError> for LinkError {
422 fn from(value: loh::LohError) -> Self {
423 LinkError::Loh(value)
424 }
425 }
426
427 /// The linker itself. Sprint 0 only validates that inputs exist; later sprints
428 /// grow this into the full pipeline described in `.docs/overview.md`.
429 pub struct Linker;
430
431 impl Linker {
432 pub fn run(opts: &LinkOptions) -> Result<(), LinkError> {
433 Self::run_profiled(opts).map(|_| ())
434 }
435
436 pub fn run_profiled(opts: &LinkOptions) -> Result<LinkProfile, LinkError> {
437 let overall_started = Instant::now();
438 let mut phases = LinkPhaseTimings::default();
439 if opts.relocatable {
440 return Err(LinkError::UnsupportedOption(
441 "`-r` relocatable output is not yet supported".into(),
442 ));
443 }
444 if opts.bundle {
445 return Err(LinkError::UnsupportedOption(
446 "`-bundle` output is not yet supported".into(),
447 ));
448 }
449 if opts.fixup_chains {
450 return Err(LinkError::UnsupportedOption(
451 "`-fixup_chains` is not yet supported".into(),
452 ));
453 }
454 if opts.icf_mode == IcfMode::All {
455 return Err(LinkError::UnsupportedOption(
456 "`-icf=all` is not yet supported; use `-icf=safe` or `-icf=none`".into(),
457 ));
458 }
459 if opts.inputs.is_empty() && opts.library_names.is_empty() && opts.frameworks.is_empty() {
460 return Err(LinkError::NoInputs);
461 }
462 let parallel_jobs = opts.parallel_jobs();
463
464 if let Some(arch) = &opts.arch {
465 if arch != "arm64" {
466 return Err(LinkError::UnsupportedArch(arch.clone()));
467 }
468 }
469
470 if opts.strip_debug {
471 crate::diag::warning(
472 "`-S` requested, but afs-ld does not currently emit debug symbols",
473 );
474 }
475 if opts.objc_force_load {
476 crate::diag::warning(
477 "`-ObjC` requested, but afs-ld does not yet scan Objective-C archive metadata; the flag currently has no effect",
478 );
479 }
480 if opts.no_loh {
481 crate::diag::warning(
482 "`-no_loh` requested, but afs-ld currently matches Apple ld by omitting final-output LOH; the flag has no effect",
483 );
484 }
485
486 let mut load_paths = Vec::new();
487 let mut positional_dylibs = Vec::new();
488 for path in &opts.inputs {
489 match path.extension().and_then(|ext| ext.to_str()) {
490 Some("dylib" | "tbd") => positional_dylibs.push(path.clone()),
491 _ => load_paths.push(path.clone()),
492 }
493 }
494 let mut dylib_load_kinds = std::collections::HashMap::new();
495 for name in &opts.library_names {
496 let path = resolve_library_input(opts, name)?;
497 dylib_load_kinds.insert(path.clone(), DylibLoadKind::Normal);
498 load_paths.push(path);
499 }
500 for framework in &opts.frameworks {
501 let path = resolve_framework_input(opts, &framework.name)?;
502 dylib_load_kinds.insert(
503 path.clone(),
504 if framework.weak {
505 DylibLoadKind::Weak
506 } else {
507 DylibLoadKind::Normal
508 },
509 );
510 load_paths.push(path);
511 }
512 load_paths.extend(positional_dylibs);
513
514 let mut inputs = Inputs::new();
515 let mut deferred_dylibs = Vec::new();
516 let mut initial_loads = Vec::new();
517 let phase_started = Instant::now();
518 for (load_order, path) in load_paths.iter().enumerate() {
519 if matches!(
520 path.extension().and_then(|ext| ext.to_str()),
521 Some("dylib" | "tbd")
522 ) {
523 deferred_dylibs.push((load_order, path.clone()));
524 continue;
525 }
526 if opts.trace_inputs {
527 eprintln!("afs-ld: loading {}", path.display());
528 }
529 initial_loads.push((load_order, path.clone()));
530 }
531 for loaded in load_initial_inputs(initial_loads, parallel_jobs)? {
532 let timings = register_loaded_initial_input(&mut inputs, loaded);
533 phases.add_input_load(timings);
534 }
535 let include_tbd_exports = inputs_may_need_dylib_exports(&inputs)?;
536 for (load_order, path) in &deferred_dylibs {
537 if opts.trace_inputs {
538 eprintln!("afs-ld: loading {}", path.display());
539 }
540 let timings = register_input(&mut inputs, path, *load_order, include_tbd_exports)?;
541 phases.add_input_load(timings);
542 }
543 phases.input_parsing = phase_started.elapsed();
544
545 let mut sym_table = SymbolTable::new();
546 let phase_started = Instant::now();
547 let seed_report = seed_all(&inputs, &mut sym_table)?;
548 if seed_report.has_errors() {
549 let mut msg = String::new();
550 for err in &seed_report.duplicates {
551 msg.push_str(&format_duplicate_diagnostic(&sym_table, &inputs, err));
552 }
553 return Err(LinkError::DuplicateSymbols(msg));
554 }
555
556 let mut force_report = DrainReport::default();
557 if opts.all_load {
558 force_load_all(
559 &mut inputs,
560 &mut sym_table,
561 &mut force_report,
562 parallel_jobs,
563 )?;
564 }
565 for archive_path in &opts.force_load_archives {
566 let Some(archive_id) = find_archive_by_path(&inputs, archive_path) else {
567 return Err(LinkError::ForceLoadNotArchive(archive_path.clone()));
568 };
569 force_load_archive(
570 &mut inputs,
571 &mut sym_table,
572 archive_id,
573 &mut force_report,
574 parallel_jobs,
575 )?;
576 }
577 if opts.trace_inputs {
578 for path in &force_report.loaded_paths {
579 eprintln!("afs-ld: loading {}", path.display());
580 }
581 }
582 if !force_report.duplicates.is_empty() {
583 let mut msg = String::new();
584 for err in &force_report.duplicates {
585 msg.push_str(&format_duplicate_diagnostic(&sym_table, &inputs, err));
586 }
587 return Err(LinkError::DuplicateSymbols(msg));
588 }
589
590 let drain_report = drain_fetches(
591 &mut inputs,
592 &mut sym_table,
593 seed_report.pending_fetches,
594 parallel_jobs,
595 )?;
596 if opts.trace_inputs {
597 for path in &drain_report.loaded_paths {
598 eprintln!("afs-ld: loading {}", path.display());
599 }
600 }
601 if !drain_report.duplicates.is_empty() {
602 let mut msg = String::new();
603 for err in &drain_report.duplicates {
604 msg.push_str(&format_duplicate_diagnostic(&sym_table, &inputs, err));
605 }
606 return Err(LinkError::DuplicateSymbols(msg));
607 }
608 let mut referrers = seed_report.referrers.clone();
609 referrers.extend_from(&force_report.referrers);
610 referrers.extend_from(&drain_report.referrers);
611 let unresolved = classify_unresolved(&mut sym_table, opts.undefined_treatment);
612 if !unresolved.errors.is_empty() {
613 return Err(LinkError::UndefinedSymbols(format_undefined_diagnostic(
614 &sym_table,
615 &inputs,
616 &referrers,
617 &unresolved.errors,
618 )));
619 }
620 if !unresolved.warnings.is_empty() {
621 crate::diag::warning_verbatim(&format_undefined_warning_diagnostic(
622 &sym_table,
623 &inputs,
624 &referrers,
625 &unresolved.warnings,
626 ));
627 }
628 phases.symbol_resolution = phase_started.elapsed();
629
630 let mut atom_table = AtomTable::new();
631 let mut objects = Vec::new();
632 let phase_started = Instant::now();
633 for idx in 0..inputs.objects.len() {
634 let input_id = resolve::InputId(idx as u32);
635 let obj = inputs.object_file(input_id)?;
636 let atomization = atomize_object(input_id, obj, &mut atom_table);
637 backpatch_symbol_atoms(&atomization, input_id, obj, &mut sym_table, &mut atom_table);
638 objects.push((input_id, obj));
639 }
640 phases.atomization = phase_started.elapsed();
641
642 let layout_inputs: Vec<LayoutInput<'_>> = objects
643 .iter()
644 .map(|(id, object)| {
645 let input = inputs.object(*id);
646 LayoutInput {
647 id: *id,
648 object,
649 load_order: input.load_order,
650 archive_member_offset: input.archive_member_offset,
651 }
652 })
653 .collect();
654 let mut dylib_loads = Vec::new();
655 let mut seen_ordinals = std::collections::BTreeSet::new();
656 for dylib in &inputs.dylibs {
657 if !seen_ordinals.insert(dylib.ordinal) {
658 continue;
659 }
660 dylib_loads.push(DylibDependency {
661 kind: dylib_load_kinds
662 .get(&dylib.path)
663 .copied()
664 .unwrap_or(DylibLoadKind::Normal),
665 install_name: dylib.load_install_name.clone(),
666 current_version: dylib.load_current_version,
667 compatibility_version: dylib.load_compatibility_version,
668 ordinal: dylib.ordinal,
669 });
670 }
671 let phase_started = Instant::now();
672 let parsed_relocs = macho::writer::build_parsed_reloc_cache(&layout_inputs)?;
673 let elapsed = phase_started.elapsed();
674 phases.input_reloc_parse += elapsed;
675 phases.input_parsing += elapsed;
676 let layout_started = Instant::now();
677 let phase_started = Instant::now();
678 let entry_symbol = find_entry_symbol_id(opts, &sym_table)?;
679 phases.layout_entry_lookup = phase_started.elapsed();
680 let phase_started = Instant::now();
681 let dead_strip = opts.dead_strip.then(|| {
682 why_live::DeadStripAnalysis::build(
683 opts,
684 &layout_inputs,
685 &atom_table,
686 &sym_table,
687 entry_symbol,
688 )
689 });
690 phases.layout_dead_strip = phase_started.elapsed();
691 let phase_started = Instant::now();
692 let icf = (opts.icf_mode == IcfMode::Safe)
693 .then(|| {
694 icf::fold_safe(
695 &layout_inputs,
696 &mut atom_table,
697 &mut sym_table,
698 dead_strip.as_ref().map(|analysis| analysis.live_atoms()),
699 )
700 })
701 .transpose()?;
702 phases.layout_icf = phase_started.elapsed();
703 let kept_atoms = if let Some(icf) = &icf {
704 Some(icf.kept_atoms())
705 } else {
706 dead_strip.as_ref().map(|analysis| analysis.live_atoms())
707 };
708 let phase_started = Instant::now();
709 let synthetic_plan = synth::SyntheticPlan::build_filtered_with_relocs(
710 &layout_inputs,
711 &atom_table,
712 &mut sym_table,
713 &inputs.dylibs,
714 kept_atoms,
715 &parsed_relocs,
716 )?;
717 phases.layout_synthetic_plan = phase_started.elapsed();
718 let icf_redirects = icf.as_ref().map(|plan| plan.redirects());
719 let phase_started = Instant::now();
720 let mut layout = Layout::build_with_synthetics_filtered(
721 opts.kind,
722 &layout_inputs,
723 &atom_table,
724 0,
725 Some(&synthetic_plan),
726 kept_atoms,
727 );
728 phases.layout_build += phase_started.elapsed();
729 let mut thunk_plan = None;
730 let mut thunk_converged = false;
731 for _ in 0..THUNK_PLAN_MAX_ITERATIONS {
732 let phase_started = Instant::now();
733 let next_plan = reloc::arm64::plan_thunks(
734 opts,
735 reloc::arm64::ThunkPlanningContext {
736 layout: &layout,
737 inputs: &layout_inputs,
738 atoms: &atom_table,
739 sym_table: &sym_table,
740 synthetic_plan: Some(&synthetic_plan),
741 icf_redirects,
742 parsed_relocs: &parsed_relocs,
743 },
744 )?;
745 phases.layout_thunk_plan += phase_started.elapsed();
746 if next_plan == thunk_plan {
747 thunk_converged = true;
748 break;
749 }
750 let extra_sections = next_plan
751 .as_ref()
752 .map_or_else(Vec::new, |plan| plan.output_sections());
753 let split_after_atoms = next_plan
754 .as_ref()
755 .map_or_else(Vec::new, |plan| plan.split_after_atoms());
756 let phase_started = Instant::now();
757 layout = Layout::build_with_synthetics_and_extra_filtered(
758 opts.kind,
759 &layout_inputs,
760 &atom_table,
761 0,
762 Some(&synthetic_plan),
763 kept_atoms,
764 ExtraLayoutSections {
765 extra_sections: &extra_sections,
766 split_after_atoms: &split_after_atoms,
767 },
768 );
769 phases.layout_build += phase_started.elapsed();
770 thunk_plan = next_plan;
771 }
772 if !thunk_converged {
773 return Err(LinkError::ThunkPlanningDidNotConverge);
774 }
775 phases.layout = layout_started.elapsed();
776 let linkedit_context = macho::writer::LinkEditContext {
777 layout_inputs: &layout_inputs,
778 atom_table: &atom_table,
779 sym_table: &sym_table,
780 synthetic_plan: &synthetic_plan,
781 icf_redirects,
782 parsed_relocs: &parsed_relocs,
783 };
784 let phase_started = Instant::now();
785 let mut linkedit = None;
786 let mut synth_linkedit_finalize = Duration::ZERO;
787 let mut synth_linkedit_symbol_plan = Duration::ZERO;
788 let mut synth_linkedit_symbol_plan_locals = Duration::ZERO;
789 let mut synth_linkedit_symbol_plan_globals = Duration::ZERO;
790 let mut synth_linkedit_symbol_plan_strtab = Duration::ZERO;
791 let mut synth_linkedit_dyld_info = Duration::ZERO;
792 let mut synth_linkedit_dyld_bind = Duration::ZERO;
793 let mut synth_linkedit_dyld_rebase = Duration::ZERO;
794 let mut synth_linkedit_dyld_export = Duration::ZERO;
795 let mut synth_linkedit_metadata_tables = Duration::ZERO;
796 let mut synth_linkedit_code_signature = Duration::ZERO;
797 let mut synth_unwind = Duration::ZERO;
798 for _ in 0..4 {
799 let phase_started = Instant::now();
800 let (next_layout, next_linkedit, linkedit_timings) =
801 macho::writer::finalize_layout_with_linkedit(
802 &layout,
803 opts.kind,
804 opts,
805 &dylib_loads,
806 linkedit_context,
807 )?;
808 synth_linkedit_finalize += phase_started.elapsed();
809 synth_linkedit_symbol_plan += linkedit_timings.symbol_plan;
810 synth_linkedit_symbol_plan_locals += linkedit_timings.symbol_plan_locals;
811 synth_linkedit_symbol_plan_globals += linkedit_timings.symbol_plan_globals;
812 synth_linkedit_symbol_plan_strtab += linkedit_timings.symbol_plan_strtab;
813 synth_linkedit_dyld_info += linkedit_timings.dyld_info;
814 synth_linkedit_dyld_bind += linkedit_timings.dyld_bind;
815 synth_linkedit_dyld_rebase += linkedit_timings.dyld_rebase;
816 synth_linkedit_dyld_export += linkedit_timings.dyld_export;
817 synth_linkedit_metadata_tables += linkedit_timings.metadata_tables;
818 synth_linkedit_code_signature += linkedit_timings.code_signature;
819 layout = next_layout;
820 linkedit = Some(next_linkedit);
821 let phase_started = Instant::now();
822 let changed = synth::unwind::synthesize(
823 &mut layout,
824 &layout_inputs,
825 &atom_table,
826 &sym_table,
827 &synthetic_plan,
828 )?;
829 synth_unwind += phase_started.elapsed();
830 if !changed {
831 break;
832 }
833 }
834 let linkedit = linkedit.expect("finalize loop always runs at least once");
835 phases.synth_linkedit_finalize = synth_linkedit_finalize;
836 phases.synth_linkedit_symbol_plan = synth_linkedit_symbol_plan;
837 phases.synth_linkedit_symbol_plan_locals = synth_linkedit_symbol_plan_locals;
838 phases.synth_linkedit_symbol_plan_globals = synth_linkedit_symbol_plan_globals;
839 phases.synth_linkedit_symbol_plan_strtab = synth_linkedit_symbol_plan_strtab;
840 phases.synth_linkedit_dyld_info = synth_linkedit_dyld_info;
841 phases.synth_linkedit_dyld_bind = synth_linkedit_dyld_bind;
842 phases.synth_linkedit_dyld_rebase = synth_linkedit_dyld_rebase;
843 phases.synth_linkedit_dyld_export = synth_linkedit_dyld_export;
844 phases.synth_linkedit_metadata_tables = synth_linkedit_metadata_tables;
845 phases.synth_linkedit_code_signature = synth_linkedit_code_signature;
846 phases.synth_unwind = synth_unwind;
847 phases.synth_sections = phase_started.elapsed();
848 let phase_started = Instant::now();
849 reloc::arm64::apply_layout(
850 &mut layout,
851 &layout_inputs,
852 &atom_table,
853 &sym_table,
854 reloc::arm64::ApplyLayoutPlan {
855 synthetic_plan: Some(&synthetic_plan),
856 thunk_plan: thunk_plan.as_ref(),
857 linkedit: &linkedit,
858 icf_redirects,
859 parsed_relocs: &parsed_relocs,
860 parallel_jobs,
861 },
862 )?;
863 phases.reloc_apply = phase_started.elapsed();
864 let folded_symbols = icf
865 .as_ref()
866 .map(|plan| plan.folded_symbols(&atom_table, &sym_table, &layout_inputs))
867 .unwrap_or_default();
868
869 if let Some(report) = why_live::format_explanations(
870 opts,
871 &layout_inputs,
872 &atom_table,
873 &sym_table,
874 entry_symbol,
875 dead_strip.as_ref(),
876 &folded_symbols,
877 )
878 .map_err(LinkError::WhyLive)?
879 {
880 print!("{report}");
881 }
882
883 let phase_started = Instant::now();
884 let mut image = Vec::new();
885 let entry_point = resolve_entry_point(opts, &sym_table)?;
886 macho::writer::write_finalized_with_linkedit(
887 &layout,
888 opts.kind,
889 opts,
890 entry_point,
891 &dylib_loads,
892 &linkedit,
893 &mut image,
894 )?;
895 let output = default_output_path(opts);
896 fs::write(&output, image)?;
897 if let Some(map_path) = &opts.map {
898 let dead_stripped = dead_strip
899 .as_ref()
900 .map(|analysis| {
901 analysis.dead_stripped_symbols(&atom_table, &sym_table, &layout_inputs)
902 })
903 .unwrap_or_default();
904 link_map::write_link_map(
905 map_path,
906 opts,
907 &layout,
908 &layout_inputs,
909 &linkedit,
910 &folded_symbols,
911 &dead_stripped,
912 )?;
913 }
914 if opts.kind == OutputKind::Executable {
915 let mut perms = fs::metadata(&output)?.permissions();
916 let mode = perms.mode();
917 perms.set_mode(mode | ((mode & 0o444) >> 2));
918 fs::set_permissions(&output, perms)?;
919 }
920 phases.write_output = phase_started.elapsed();
921 Ok(LinkProfile {
922 output,
923 phases,
924 total_wall: overall_started.elapsed(),
925 })
926 }
927 }
928
929 fn resolve_library_input(opts: &LinkOptions, name: &str) -> Result<PathBuf, LinkError> {
930 let mut search_dirs = Vec::new();
931 for dir in &opts.search_paths {
932 search_dirs.push(dir.clone());
933 if let Some(root) = &opts.syslibroot {
934 if let Ok(stripped) = dir.strip_prefix("/") {
935 search_dirs.push(root.join(stripped));
936 }
937 }
938 }
939 if let Some(root) = &opts.syslibroot {
940 search_dirs.push(root.join("usr/lib"));
941 } else {
942 search_dirs.push(PathBuf::from("/usr/lib"));
943 }
944
945 let candidates = [
946 format!("lib{name}.tbd"),
947 format!("lib{name}.dylib"),
948 format!("lib{name}.a"),
949 ];
950 for dir in search_dirs {
951 for candidate in &candidates {
952 let path = dir.join(candidate);
953 if path.is_file() {
954 return Ok(path);
955 }
956 }
957 }
958 Err(LinkError::LibraryNotFound(name.to_string()))
959 }
960
961 fn resolve_framework_input(opts: &LinkOptions, name: &str) -> Result<PathBuf, LinkError> {
962 let mut roots = Vec::new();
963 if let Some(root) = &opts.syslibroot {
964 roots.push(root.join("System/Library/Frameworks"));
965 roots.push(root.join("Library/Frameworks"));
966 } else {
967 roots.push(PathBuf::from("/System/Library/Frameworks"));
968 roots.push(PathBuf::from("/Library/Frameworks"));
969 }
970
971 for root in roots {
972 let framework_dir = root.join(format!("{name}.framework"));
973 for candidate in [
974 framework_dir.join(format!("{name}.tbd")),
975 framework_dir.join(name),
976 ] {
977 if candidate.is_file() {
978 return Ok(candidate);
979 }
980 }
981 }
982
983 Err(LinkError::FrameworkNotFound(name.to_string()))
984 }
985
986 fn default_output_path(opts: &LinkOptions) -> PathBuf {
987 opts.output
988 .clone()
989 .unwrap_or_else(|| PathBuf::from("a.out"))
990 }
991
992 struct LoadedObjectInput {
993 path: PathBuf,
994 load_order: usize,
995 bytes: Vec<u8>,
996 parsed: ObjectFile,
997 timings: InputLoadTimings,
998 }
999
1000 struct LoadedArchiveInput {
1001 path: PathBuf,
1002 load_order: usize,
1003 bytes: Vec<u8>,
1004 timings: InputLoadTimings,
1005 }
1006
1007 enum LoadedInitialInput {
1008 Object(Box<LoadedObjectInput>),
1009 Archive(LoadedArchiveInput),
1010 }
1011
1012 impl LoadedInitialInput {
1013 fn load_order(&self) -> usize {
1014 match self {
1015 LoadedInitialInput::Object(input) => input.load_order,
1016 LoadedInitialInput::Archive(input) => input.load_order,
1017 }
1018 }
1019 }
1020
1021 struct InitialLoadError {
1022 load_order: usize,
1023 error: LinkError,
1024 }
1025
1026 fn load_initial_inputs(
1027 loads: Vec<(usize, PathBuf)>,
1028 parallel_jobs: usize,
1029 ) -> Result<Vec<LoadedInitialInput>, LinkError> {
1030 let mut results = Vec::new();
1031 let mut object_jobs = Vec::new();
1032 for (load_order, path) in loads {
1033 if matches!(path.extension().and_then(|ext| ext.to_str()), Some("a")) {
1034 results.push(load_archive_input(path, load_order));
1035 } else {
1036 object_jobs.push((load_order, path));
1037 }
1038 }
1039 results.extend(load_objects_parallel(object_jobs, parallel_jobs));
1040 results.sort_by_key(|result| match result {
1041 Ok(input) => input.load_order(),
1042 Err(error) => error.load_order,
1043 });
1044
1045 let mut loaded = Vec::with_capacity(results.len());
1046 for result in results {
1047 match result {
1048 Ok(input) => loaded.push(input),
1049 Err(error) => return Err(error.error),
1050 }
1051 }
1052 Ok(loaded)
1053 }
1054
1055 fn load_objects_parallel(
1056 jobs: Vec<(usize, PathBuf)>,
1057 parallel_jobs: usize,
1058 ) -> Vec<Result<LoadedInitialInput, InitialLoadError>> {
1059 if jobs.is_empty() {
1060 return Vec::new();
1061 }
1062 let job_count = parallel_jobs.max(1).min(jobs.len()).max(1);
1063 if job_count == 1 {
1064 return jobs
1065 .into_iter()
1066 .map(|(load_order, path)| load_object_input(path, load_order))
1067 .collect();
1068 }
1069
1070 let queue = Arc::new(Mutex::new(VecDeque::from(jobs)));
1071 let (tx, rx) = mpsc::channel();
1072 thread::scope(|scope| {
1073 for _ in 0..job_count {
1074 let queue = Arc::clone(&queue);
1075 let tx = tx.clone();
1076 scope.spawn(move || loop {
1077 let Some((load_order, path)) = queue
1078 .lock()
1079 .expect("input load queue mutex poisoned")
1080 .pop_front()
1081 else {
1082 break;
1083 };
1084 tx.send(load_object_input(path, load_order))
1085 .expect("input load receiver should stay live");
1086 });
1087 }
1088 drop(tx);
1089 rx.into_iter().collect()
1090 })
1091 }
1092
1093 fn load_object_input(
1094 path: PathBuf,
1095 load_order: usize,
1096 ) -> Result<LoadedInitialInput, InitialLoadError> {
1097 let mut timings = InputLoadTimings::default();
1098 let phase_started = Instant::now();
1099 let bytes = fs::read(&path).map_err(|error| InitialLoadError {
1100 load_order,
1101 error: LinkError::Io(error),
1102 })?;
1103 timings.read = phase_started.elapsed();
1104
1105 let phase_started = Instant::now();
1106 let parsed = ObjectFile::parse(&path, &bytes).map_err(|error| InitialLoadError {
1107 load_order,
1108 error: LinkError::from(error),
1109 })?;
1110 timings.object_parse = phase_started.elapsed();
1111
1112 Ok(LoadedInitialInput::Object(Box::new(LoadedObjectInput {
1113 path,
1114 load_order,
1115 bytes,
1116 parsed,
1117 timings,
1118 })))
1119 }
1120
1121 fn load_archive_input(
1122 path: PathBuf,
1123 load_order: usize,
1124 ) -> Result<LoadedInitialInput, InitialLoadError> {
1125 let mut timings = InputLoadTimings::default();
1126 let phase_started = Instant::now();
1127 let bytes = fs::read(&path).map_err(|error| InitialLoadError {
1128 load_order,
1129 error: LinkError::Io(error),
1130 })?;
1131 timings.read = phase_started.elapsed();
1132
1133 let phase_started = Instant::now();
1134 Archive::open(&path, &bytes).map_err(|error| InitialLoadError {
1135 load_order,
1136 error: LinkError::from(InputAddError::from(error)),
1137 })?;
1138 timings.archive_parse = phase_started.elapsed();
1139
1140 Ok(LoadedInitialInput::Archive(LoadedArchiveInput {
1141 path,
1142 load_order,
1143 bytes,
1144 timings,
1145 }))
1146 }
1147
1148 fn register_loaded_initial_input(
1149 inputs: &mut Inputs,
1150 loaded: LoadedInitialInput,
1151 ) -> InputLoadTimings {
1152 match loaded {
1153 LoadedInitialInput::Object(input) => {
1154 inputs.add_parsed_object(input.path, input.bytes, input.parsed, input.load_order);
1155 input.timings
1156 }
1157 LoadedInitialInput::Archive(input) => {
1158 inputs.add_validated_archive(input.path, input.bytes, input.load_order);
1159 input.timings
1160 }
1161 }
1162 }
1163
1164 fn register_input(
1165 inputs: &mut Inputs,
1166 path: &std::path::Path,
1167 load_order: usize,
1168 include_tbd_exports: bool,
1169 ) -> Result<InputLoadTimings, LinkError> {
1170 let mut timings = InputLoadTimings::default();
1171 let phase_started = Instant::now();
1172 let bytes = fs::read(path)?;
1173 timings.read = phase_started.elapsed();
1174 match path.extension().and_then(|ext| ext.to_str()) {
1175 Some("a") => {
1176 let phase_started = Instant::now();
1177 let _ = inputs.add_archive(path.to_path_buf(), bytes, load_order)?;
1178 timings.archive_parse = phase_started.elapsed();
1179 }
1180 Some("dylib") => {
1181 let phase_started = Instant::now();
1182 let _ = inputs.add_dylib(path.to_path_buf(), bytes)?;
1183 timings.dylib_parse = phase_started.elapsed();
1184 }
1185 Some("tbd") => {
1186 let phase_started = Instant::now();
1187 let text = std::str::from_utf8(&bytes).map_err(|e| {
1188 LinkError::Tbd(macho::tbd::TbdError::Schema {
1189 msg: format!("TBD input is not UTF-8: {e}"),
1190 })
1191 })?;
1192 let target = Target {
1193 arch: Arch::Arm64,
1194 platform: Platform::MacOs,
1195 };
1196 let docs = if include_tbd_exports {
1197 parse_tbd_for_target(text, &target)?
1198 } else {
1199 parse_tbd_metadata_for_target(text, &target)?
1200 };
1201 timings.tbd_decode = phase_started.elapsed();
1202
1203 let phase_started = Instant::now();
1204 if docs.is_empty() {
1205 return Err(LinkError::NoTbdDocument(path.to_path_buf()));
1206 }
1207 let canonical = docs
1208 .iter()
1209 .find(|doc| doc.parent_umbrella.is_empty())
1210 .unwrap_or_else(|| &docs[0]);
1211 let load = DylibLoadMeta {
1212 install_name: canonical.install_name.clone(),
1213 current_version: canonical
1214 .current_version
1215 .as_deref()
1216 .map(parse_version)
1217 .unwrap_or(DEFAULT_TBD_VERSION),
1218 compatibility_version: canonical
1219 .compatibility_version
1220 .as_deref()
1221 .map(parse_version)
1222 .unwrap_or(DEFAULT_TBD_VERSION),
1223 ordinal: inputs.next_dylib_ordinal(),
1224 };
1225 for doc in &docs {
1226 let file = DylibFile::from_tbd(path, doc, &target);
1227 let _ =
1228 inputs.add_dylib_from_file_with_meta(path.to_path_buf(), file, load.clone());
1229 }
1230 timings.tbd_materialize = phase_started.elapsed();
1231 }
1232 _ => {
1233 let phase_started = Instant::now();
1234 let _ = inputs.add_object(path.to_path_buf(), bytes, load_order)?;
1235 timings.object_parse = phase_started.elapsed();
1236 }
1237 }
1238 Ok(timings)
1239 }
1240
1241 fn inputs_may_need_dylib_exports(inputs: &Inputs) -> Result<bool, LinkError> {
1242 if !inputs.archives.is_empty() {
1243 return Ok(true);
1244 }
1245 for i in 0..inputs.objects.len() {
1246 let input_id = InputId(i as u32);
1247 let object = inputs.object_file(input_id)?;
1248 if object.symbols.iter().any(|sym| {
1249 sym.stab_kind().is_none()
1250 && (sym.is_ext() || sym.is_private_ext())
1251 && sym.kind() == SymKind::Undef
1252 && !sym.is_common()
1253 }) {
1254 return Ok(true);
1255 }
1256 }
1257 Ok(false)
1258 }
1259
1260 fn resolve_entry_point(
1261 opts: &LinkOptions,
1262 sym_table: &SymbolTable,
1263 ) -> Result<Option<macho::writer::EntryPoint>, LinkError> {
1264 let Some(symbol_id) = find_entry_symbol_id(opts, sym_table)? else {
1265 return Ok(None);
1266 };
1267 let Symbol::Defined { atom, value, .. } = sym_table.get(symbol_id) else {
1268 let name = sym_table.interner.resolve(sym_table.get(symbol_id).name());
1269 return Err(LinkError::EntrySymbolNotFound(name.to_string()));
1270 };
1271 Ok(Some(macho::writer::EntryPoint {
1272 atom: *atom,
1273 atom_value: *value,
1274 }))
1275 }
1276
1277 fn find_entry_symbol_id(
1278 opts: &LinkOptions,
1279 sym_table: &SymbolTable,
1280 ) -> Result<Option<resolve::SymbolId>, LinkError> {
1281 let name = if let Some(name) = &opts.entry {
1282 name.as_str()
1283 } else if opts.kind == OutputKind::Executable {
1284 if symbol_defined(sym_table, "_main") {
1285 "_main"
1286 } else if symbol_defined(sym_table, "_start") {
1287 "_start"
1288 } else {
1289 return Ok(None);
1290 }
1291 } else {
1292 return Ok(None);
1293 };
1294 let Some((symbol_id, _)) = sym_table
1295 .iter()
1296 .find(|(_, symbol)| sym_table.interner.resolve(symbol.name()) == name)
1297 else {
1298 return Err(LinkError::EntrySymbolNotFound(name.to_string()));
1299 };
1300 Ok(Some(symbol_id))
1301 }
1302
1303 fn symbol_defined(sym_table: &SymbolTable, name: &str) -> bool {
1304 sym_table.iter().any(|(_, symbol)| {
1305 sym_table.interner.resolve(symbol.name()) == name
1306 && matches!(symbol, Symbol::Defined { .. })
1307 })
1308 }
1309