1#![allow(clippy::too_many_arguments)]
15
16use anyhow::Context as _;
17use anyhow::bail;
18use asm_diff::AddressIndex;
19use clap::Parser;
20use clap::ValueEnum;
21use itertools::Itertools as _;
22#[allow(clippy::wildcard_imports)]
23use linker_utils::elf::secnames::*;
24use object::LittleEndian;
25use object::Object as _;
26use object::ObjectSection;
27use object::ObjectSymbol as _;
28use object::read::elf::ElfSection64;
29use section_map::IndexedLayout;
30use section_map::LayoutAndFiles;
31use std::collections::HashMap;
32use std::fmt::Display;
33use std::path::Path;
34use std::path::PathBuf;
35
36mod aarch64;
37mod arch;
38mod asm_diff;
39mod debug_info_diff;
40mod diagnostics;
41mod eh_frame_diff;
42mod gnu_hash;
43mod header_diff;
44pub(crate) mod section_map;
45mod symtab;
46mod trace;
47mod x86_64;
48
49type Result<T = (), E = anyhow::Error> = core::result::Result<T, E>;
50type ElfFile64<'data> = object::read::elf::ElfFile64<'data, LittleEndian>;
51type ElfSymbol64<'data, 'file> = object::read::elf::ElfSymbol64<'data, 'file, LittleEndian>;
52
53use colored::Colorize;
54pub use diagnostics::enable_diagnostics;
55use section_map::InputSectionId;
56use section_map::OwnedFileIdentifier;
57
58#[non_exhaustive]
59#[derive(Parser, Default, Clone)]
60pub struct Config {
61 #[arg(long, value_delimiter = ',')]
63 pub ignore: Vec<String>,
64
65 #[arg(long, value_delimiter = ',')]
67 pub only: Vec<String>,
68
69 #[arg(long, value_delimiter = ',', value_parser = parse_string_equality)]
71 pub equiv: Vec<(String, String)>,
72
73 #[arg(long)]
76 pub wild_defaults: bool,
77
78 #[arg(long)]
80 pub coverage: bool,
81
82 #[arg(long, value_delimiter = ',', value_name = "NAME,NAME...")]
84 pub display_names: Vec<String>,
85
86 #[arg(long = "ref", value_name = "FILE")]
88 pub references: Vec<PathBuf>,
89
90 #[arg(long, alias = "color", default_value = "auto")]
91 pub colour: Colour,
92
93 pub file: PathBuf,
95}
96
97#[derive(ValueEnum, Copy, Clone, Default)]
98pub enum Colour {
99 #[default]
100 Auto,
101 Never,
102 Always,
103}
104
105pub struct Binary<'data> {
107 name: String,
108 path: PathBuf,
109 elf_file: &'data ElfFile64<'data>,
110 address_index: AddressIndex<'data>,
111 name_index: NameIndex<'data>,
112 indexed_layout: Option<IndexedLayout<'data>>,
113 trace: trace::Trace,
114 sections_by_name: HashMap<&'data [u8], SectionInfo>,
115}
116
117#[derive(Clone, Copy)]
118struct SectionInfo {
119 index: object::SectionIndex,
120 size: u64,
121}
122
123struct NameIndex<'data> {
124 globals_by_name: HashMap<&'data [u8], Vec<object::SymbolIndex>>,
125 locals_by_name: HashMap<&'data [u8], Vec<object::SymbolIndex>>,
126 dynamic_by_name: HashMap<&'data [u8], Vec<object::SymbolIndex>>,
127}
128
129impl Config {
130 #[must_use]
131 pub fn from_env() -> Self {
132 Self::parse()
133 }
134
135 fn apply_wild_defaults(&mut self) {
136 self.ignore.extend(
137 [
138 "section.shdr",
142 "section.phdr",
143 "section.data.rel.ro",
145 "section.debug*",
146 "section.stapsdt.base",
147 "section.note.gnu.build-id",
148 "section.note.gnu.property",
149 "section.note.stapsdt",
150 "section.hash",
151 "section.got.entsize",
153 "section.plt.got.entsize",
154 "section.plt.entsize",
155 "section.plt",
157 "section.plt.alignment",
158 "section.bss.alignment",
159 "section.gnu.build.attributes",
160 "section.annobin.notes.entsize",
161 "section.lrodata",
163 ".dynamic.DT_VERNEEDNUM",
166 ".dynamic.DT_JMPREL",
168 ".dynamic.DT_PLTGOT",
169 ".dynamic.DT_PLTREL",
170 "section.got.plt",
172 GOT_PLT_SECTION_NAME_STR,
173 "section.plt.sec",
175 ".dynamic.DT_HASH",
177 "section.rela.plt",
180 "section.plt.got.alignment",
182 "section.eh_frame.flags",
185 "section.note.package",
187 "rel.match_failed.R_X86_64_GOTPC32_TLSDESC",
189 "rel.missing-opt.R_X86_64_TLSDESC_CALL.SkipTlsDescCall.*",
190 "rel.extra-opt.R_X86_64_GOTPCRELX.CallIndirectToRelative.static-*",
194 "section.gnu.warning",
196 "rel.missing-opt.R_AARCH64_CALL26.ReplaceWithNop.static-non-pie",
198 "rel.missing-opt.R_AARCH64_JUMP26.ReplaceWithNop.static-non-pie",
199 "rel.match_failed.R_AARCH64_TLSDESC_ADR_PAGE21",
200 "rel.match_failed.R_AARCH64_TLSDESC_LD64_LO12",
201 "rel.match_failed.R_AARCH64_TLSGD_ADD_LO12_NC",
202 "rel.missing-opt.R_X86_64_TLSGD.TlsGdToInitialExec.shared-object",
203 "rel.extra-opt.R_AARCH64_TLSIE_ADR_GOTTPREL_PAGE21.MovzXnLsl16.*",
206 "rel.missing-opt.R_AARCH64_ADR_GOT_PAGE.AdrpToAdr.*",
209 "rel.missing-opt.R_AARCH64_ADR_PREL_PG_HI21.AdrpToAdr.*",
210 "section.rodata.entsize",
214 "section.rodata.flags",
215 ]
216 .into_iter()
217 .map(ToOwned::to_owned),
218 );
219
220 #[cfg(target_arch = "aarch64")]
221 {
222 self.ignore.extend(
223 [
224 "section.plt.entsize",
227 ]
228 .into_iter()
229 .map(ToOwned::to_owned),
230 );
231 }
232
233 self.equiv.push((
234 GOT_SECTION_NAME_STR.to_owned(),
235 GOT_PLT_SECTION_NAME_STR.to_owned(),
236 ));
237 self.equiv.push((
239 PLT_SECTION_NAME_STR.to_owned(),
240 PLT_GOT_SECTION_NAME_STR.to_owned(),
241 ));
242 self.equiv.push((
243 PLT_SECTION_NAME_STR.to_owned(),
244 PLT_SEC_SECTION_NAME_STR.to_owned(),
245 ));
246 }
247
248 #[must_use]
249 pub fn to_arg_string(&self) -> String {
250 let mut out = String::new();
251 if self.wild_defaults {
252 out.push_str("--wild-defaults ");
253 }
254 if !self.ignore.is_empty() {
255 out.push_str("--ignore '");
256 out.push_str(&self.ignore.join(","));
257 out.push_str("' ");
258 }
259 if !self.equiv.is_empty() {
260 out.push_str("--equiv '");
261 let parts = self
262 .equiv
263 .iter()
264 .map(|(k, v)| format!("{k}={v}"))
265 .collect_vec();
266 out.push_str(&parts.join(","));
267 out.push_str("' ");
268 }
269 if !self.display_names.is_empty() {
270 out.push_str("--display-names ");
271 out.push_str(&self.display_names.join(","));
272 out.push(' ');
273 }
274 for file in &self.references {
275 out.push_str("--ref ");
276 out.push_str(&file.to_string_lossy());
277 out.push(' ');
278 }
279 out.push_str(&self.file.to_string_lossy());
280 out
281 }
282
283 fn filenames(&self) -> impl Iterator<Item = &PathBuf> {
284 std::iter::once(&self.file).chain(&self.references)
287 }
288}
289
290impl<'data> Binary<'data> {
291 pub(crate) fn new(
292 elf_file: &'data ElfFile64<'data>,
293 name: String,
294 path: PathBuf,
295 layout_and_files: Option<&'data LayoutAndFiles>,
296 ) -> Result<Self> {
297 let address_index = AddressIndex::new(elf_file);
298 let indexed_layout = layout_and_files.map(IndexedLayout::new).transpose()?;
299 let trace = trace::Trace::for_path(&path)?;
300
301 let sections_by_name = elf_file
302 .sections()
303 .map(|section| {
304 Ok((
305 section.name_bytes()?,
306 SectionInfo {
307 index: section.index(),
308 size: section.size(),
309 },
310 ))
311 })
312 .collect::<Result<HashMap<&[u8], SectionInfo>>>()?;
313
314 Ok(Self {
315 name,
316 elf_file,
317 path,
318 address_index,
319 name_index: NameIndex::new(elf_file),
320 indexed_layout,
321 trace,
322 sections_by_name,
323 })
324 }
325
326 pub(crate) fn symbol_by_name(&self, name: &[u8], hint_address: u64) -> NameLookupResult {
329 match self.lookup_symbol(&self.name_index.globals_by_name, name, hint_address) {
330 NameLookupResult::Undefined => {
331 self.lookup_symbol(&self.name_index.locals_by_name, name, hint_address)
332 }
333 other => other,
334 }
335 }
336
337 fn lookup_symbol(
338 &self,
339 symbol_map: &HashMap<&[u8], Vec<object::SymbolIndex>>,
340 name: &[u8],
341 hint_address: u64,
342 ) -> NameLookupResult {
343 let indexes = symbol_map.get(name).map(Vec::as_slice).unwrap_or_default();
344
345 if indexes.len() >= 2 {
346 for sym_index in indexes {
347 if let Ok(sym) = self.elf_file.symbol_by_index(*sym_index) {
348 if sym.address() == hint_address {
349 return NameLookupResult::Defined(sym);
350 }
351 }
352 }
353
354 return NameLookupResult::Duplicate;
356 }
357
358 if let Some(symbol_index) = indexes.first() {
359 if let Ok(sym) = self.elf_file.symbol_by_index(*symbol_index) {
360 NameLookupResult::Defined(sym)
361 } else {
362 NameLookupResult::Undefined
363 }
364 } else {
365 NameLookupResult::Undefined
366 }
367 }
368
369 fn has_symbols(&self) -> bool {
370 !self.name_index.globals_by_name.is_empty()
371 }
372
373 fn section_by_name(&self, name: &str) -> Option<ElfSection64<LittleEndian>> {
374 self.section_by_name_bytes(name.as_bytes())
375 }
376
377 fn section_by_name_bytes(&self, name: &[u8]) -> Option<ElfSection64<LittleEndian>> {
378 let index = self.sections_by_name.get(name)?.index;
379 self.elf_file.section_by_index(index).ok()
380 }
381
382 fn section_containing_address(&self, address: u64) -> Option<&str> {
385 self.elf_file
386 .sections()
387 .find(|sec| (sec.address()..sec.address() + sec.size()).contains(&address))
388 .and_then(|sec| sec.name().ok())
389 }
390}
391
392#[derive(Debug)]
393enum NameLookupResult<'data, 'file> {
394 Undefined,
395 Duplicate,
396 Defined(ElfSymbol64<'data, 'file>),
397}
398
399fn validate_objects(
400 report: &mut Report,
401 objects: &[Binary],
402 validation_name: &str,
403 validation_fn: impl Fn(&Binary) -> Result,
404) {
405 let values = objects
406 .iter()
407 .map(|obj| match validation_fn(obj) {
408 Ok(_) => "OK".to_owned(),
409 Err(e) => e.to_string(),
410 })
411 .collect_vec();
412 if first_equals_any(values.iter()) {
413 return;
414 }
415 report.add_diff(Diff {
416 key: validation_name.to_owned(),
417 values: DiffValues::PerObject(values),
418 });
419}
420
421pub struct Report {
422 names: Vec<String>,
425
426 paths: Vec<PathBuf>,
428
429 diffs: Vec<Diff>,
431
432 config: Config,
434
435 coverage: Option<Coverage>,
436}
437
438#[derive(Default)]
439struct Coverage {
440 sections: HashMap<InputSectionId, SectionCoverage>,
441}
442
443struct SectionCoverage {
444 original_file: OwnedFileIdentifier,
446
447 name: String,
449
450 diffed: bool,
452
453 num_bytes: u64,
455}
456
457impl Report {
458 pub fn from_config(mut config: Config) -> Result<Report> {
459 match config.colour {
464 Colour::Auto => colored::control::unset_override(),
465 Colour::Never => colored::control::set_override(false),
466 Colour::Always => colored::control::set_override(true),
467 }
468
469 if config.wild_defaults {
470 config.apply_wild_defaults();
471 }
472 let display_names = short_file_display_names(&config)?;
473
474 let file_bytes = config
475 .filenames()
476 .map(|filename| -> Result<Vec<u8>> {
477 let bytes = std::fs::read(filename)
478 .with_context(|| format!("Failed to read `{}`", filename.display()))?;
479 Ok(bytes)
480 })
481 .collect::<Result<Vec<Vec<u8>>>>()?;
482
483 let elf_files = file_bytes
484 .iter()
485 .map(|bytes| -> Result<ElfFile64> { Ok(ElfFile64::parse(bytes.as_slice())?) })
486 .collect::<Result<Vec<_>>>()?;
487
488 let layouts = config
489 .filenames()
490 .map(|p| LayoutAndFiles::from_base_path(p))
491 .collect::<Result<Vec<_>>>()?;
492
493 let objects = elf_files
494 .iter()
495 .zip(display_names)
496 .zip(config.filenames())
497 .zip(&layouts)
498 .map(|(((elf_file, name), path), layout)| -> Result<Binary> {
499 Binary::new(elf_file, name, path.clone(), layout.as_ref())
500 })
501 .collect::<Result<Vec<_>>>()?;
502
503 let mut report = Report {
504 names: objects.iter().map(|o| o.name.clone()).collect(),
505 paths: objects.iter().map(|o| o.path.clone()).collect(),
506 diffs: Default::default(),
507 coverage: config.coverage.then(Coverage::default),
508 config,
509 };
510 report.run_on_objects(&objects);
511 Ok(report)
512 }
513
514 fn run_on_objects(&mut self, objects: &[Binary]) {
515 validate_objects(
516 self,
517 objects,
518 GNU_HASH_SECTION_NAME_STR,
519 gnu_hash::check_object,
520 );
521 validate_objects(self, objects, "index", asm_diff::validate_indexes);
522 validate_objects(
523 self,
524 objects,
525 GOT_PLT_SECTION_NAME_STR,
526 asm_diff::validate_got_plt,
527 );
528 validate_objects(
529 self,
530 objects,
531 SYMTAB_SECTION_NAME_STR,
532 symtab::validate_debug,
533 );
534 validate_objects(
535 self,
536 objects,
537 DYNSYM_SECTION_NAME_STR,
538 symtab::validate_dynamic,
539 );
540 header_diff::check_dynamic_headers(self, objects);
541 header_diff::check_file_headers(self, objects);
542 asm_diff::report_section_diffs(self, objects);
543 header_diff::report_section_diffs(self, objects);
544 eh_frame_diff::report_diffs(self, objects);
545 debug_info_diff::check_debug_info(self, objects);
546 }
547
548 fn add_diff(&mut self, diff: Diff) {
549 if self.should_ignore(&diff.key) {
550 return;
551 }
552 self.diffs.push(diff);
553 }
554
555 fn add_diffs(&mut self, new_diffs: Vec<Diff>) {
556 for diff in new_diffs {
557 self.add_diff(diff);
558 }
559 }
560
561 #[must_use]
562 pub fn has_problems(&self) -> bool {
563 !self.diffs.is_empty()
564 }
565
566 fn should_ignore(&self, key: &str) -> bool {
567 if !self.config.only.is_empty() {
568 return !self.config.only.iter().any(|i| {
569 if let Some(prefix) = i.strip_suffix('*') {
570 key.starts_with(prefix)
571 } else {
572 key == *i
573 }
574 });
575 }
576 self.config.ignore.iter().any(|i| {
577 if let Some(prefix) = i.strip_suffix('*') {
578 key.starts_with(prefix)
579 } else {
580 key == *i
581 }
582 })
583 }
584
585 fn add_error(&mut self, error: impl Into<String>) {
586 self.diffs.push(Diff {
587 key: "error".to_owned(),
588 values: DiffValues::PreFormatted(error.into()),
589 });
590 }
591}
592
593struct Diff {
594 key: String,
595 values: DiffValues,
596}
597
598enum DiffValues {
599 PerObject(Vec<String>),
600 PreFormatted(String),
601}
602
603impl Display for Report {
604 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
605 for (name, path) in self.names.iter().zip(&self.paths) {
606 writeln!(f, "{name}: {}", path.display())?;
607 }
608
609 for diff in &self.diffs {
610 writeln!(f, "{}", diff.key)?;
611
612 match &diff.values {
613 DiffValues::PerObject(values) => {
614 for (filename, result) in self.names.iter().zip(values) {
615 writeln!(f, " {filename} {result}")?;
616 }
617 }
618 DiffValues::PreFormatted(values) => {
619 for line in values.lines() {
620 writeln!(f, " {line}")?;
621 }
622 }
623 }
624
625 writeln!(f)?;
626 }
627
628 if let Some(coverage) = self.coverage.as_ref() {
629 Display::fmt(coverage, f)?;
630 }
631
632 Ok(())
633 }
634}
635
636impl Display for Binary<'_> {
637 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
638 self.name.fmt(f)
639 }
640}
641
642impl Display for Coverage {
643 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
644 writeln!(f, "Diffed sections:")?;
645
646 let mut total_bytes = 0;
647 let mut total_diffed = 0;
648
649 for sec in self.sections.values() {
650 writeln!(
651 f,
652 " {} {}: {}",
653 sec.original_file,
654 sec.name,
655 if sec.diffed {
656 "true".green()
657 } else {
658 "false".red()
659 }
660 )?;
661
662 if sec.diffed {
663 total_diffed += sec.num_bytes;
664 }
665
666 total_bytes += sec.num_bytes;
667 }
668
669 writeln!(
670 f,
671 "Diffed {total_diffed} of {total_bytes} section bytes ({}%)",
672 total_diffed * 100 / total_bytes
673 )?;
674
675 Ok(())
676 }
677}
678
679fn short_file_display_names(config: &Config) -> Result<Vec<String>> {
680 let paths: Vec<&PathBuf> = config.filenames().collect();
681 if !config.display_names.is_empty() {
682 if config.display_names.len() != paths.len() {
683 bail!(
684 "--display-names has {} names, but {} filenames were provided",
685 config.display_names.len(),
686 paths.len()
687 );
688 }
689 return Ok(config.display_names.clone());
690 }
691 if paths.is_empty() {
692 return Ok(vec![]);
693 }
694 let mut names = paths
695 .iter()
696 .map(|p| p.to_string_lossy().into_owned())
697 .collect_vec();
698 if names.iter().all(|name| {
699 Path::new(name)
700 .extension()
701 .is_some_and(|ext| ext.eq_ignore_ascii_case("so"))
702 }) {
703 names = names
704 .into_iter()
705 .map(|n| n.strip_suffix(".so").unwrap().to_owned())
706 .collect();
707 }
708
709 if names.len() > 1 {
710 let mut iterators = names.iter().map(|n| n.bytes()).collect_vec();
713 let mut n = 0;
714 while first_equals_all(iterators.iter_mut().map(Iterator::next)) {
715 n += 1;
716 }
717 names = names
718 .iter()
719 .map(|name| String::from_utf8_lossy(&name.bytes().skip(n).collect_vec()).into_owned())
720 .collect_vec();
721 }
722 Ok(names)
723}
724
725fn first_equals_all<T: PartialEq>(mut inputs: impl Iterator<Item = T>) -> bool {
726 let Some(first) = inputs.next() else {
727 return true;
728 };
729 for next in inputs {
730 if next != first {
731 return false;
732 }
733 }
734 true
735}
736
737fn first_equals_any<T: PartialEq>(mut inputs: impl Iterator<Item = T>) -> bool {
739 let Some(first) = inputs.next() else {
740 return true;
741 };
742 for next in inputs {
743 if next == first {
744 return true;
745 }
746 }
747 false
748}
749
750impl<'data> NameIndex<'data> {
751 fn new(elf_file: &ElfFile64<'data>) -> NameIndex<'data> {
752 let mut globals_by_name: HashMap<&[u8], Vec<object::SymbolIndex>> = HashMap::new();
753 let mut locals_by_name: HashMap<&[u8], Vec<object::SymbolIndex>> = HashMap::new();
754 let mut dynamic_by_name: HashMap<&[u8], Vec<object::SymbolIndex>> = HashMap::new();
755
756 for sym in elf_file.symbols() {
757 if sym.section_index().is_none() {
760 continue;
761 }
762
763 if let Ok(mut name) = sym.name_bytes() {
764 if name.starts_with(b".L") {
770 continue;
771 }
772
773 if let Some(at_pos) = name.iter().position(|b| *b == b'@') {
778 name = &name[..at_pos];
779 }
780
781 if sym.is_global() {
782 globals_by_name.entry(name).or_default().push(sym.index());
783 } else {
784 locals_by_name.entry(name).or_default().push(sym.index());
785 }
786 }
787 }
788
789 for sym in elf_file.dynamic_symbols() {
790 if let Ok(name) = sym.name_bytes() {
791 dynamic_by_name.entry(name).or_default().push(sym.index());
792 }
793 }
794
795 NameIndex {
796 globals_by_name,
797 locals_by_name,
798 dynamic_by_name,
799 }
800 }
801}
802
803fn slice_from_all_bytes<T: object::Pod>(data: &[u8]) -> &[T] {
804 object::slice_from_bytes(data, data.len() / size_of::<T>())
805 .unwrap()
806 .0
807}
808
809fn parse_string_equality(
810 s: &str,
811) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync + 'static>> {
812 let (a, b) = s
813 .split_once('=')
814 .ok_or_else(|| format!("invalid key-value pair. No '=' found in `{s}`"))?;
815 Ok((a.to_owned(), b.to_owned()))
816}