Skip to content

Commit 79542a4

Browse files
Significantly improve diffing performance and fix minor bug with bss section match percents (#316)
* Move symbol name processing from diff step to read step * Use min_by_key instead of sorted_unstable_by_key * Sort all symbols by address when reading the object This fixes an underreport bug in diff_bss_section. * More symbol sorting logic on read * Move is_name_compiler_generated into SymbolFlag & filter out File symbols --------- Co-authored-by: Luke Street <luke@street.dev>
1 parent 0ebd2ae commit 79542a4

31 files changed

Lines changed: 3375 additions & 2714 deletions

objdiff-core/src/diff/data.rs

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,31 +35,13 @@ pub fn diff_bss_symbol(
3535
))
3636
}
3737

38-
pub fn symbol_name_matches(left_name: &str, right_name: &str) -> bool {
39-
if let Some((left_prefix, left_suffix)) = left_name.split_once("@class$")
40-
&& let Some((right_prefix, right_suffix)) = right_name.split_once("@class$")
38+
pub fn symbol_name_matches(left: &Symbol, right: &Symbol) -> bool {
39+
if let Some(left_name) = &left.normalized_name
40+
&& let Some(right_name) = &right.normalized_name
4141
{
42-
// Match Metrowerks anonymous class symbol names, ignoring the unique ID.
43-
// e.g. __dt__Q29dCamera_c23@class$3665d_camera_cppFv
44-
if left_prefix == right_prefix
45-
&& let Some(left_idx) = left_suffix.chars().position(|c| !c.is_numeric())
46-
&& let Some(right_idx) = right_suffix.chars().position(|c| !c.is_numeric())
47-
{
48-
// e.g. d_camera_cppFv (after the unique ID)
49-
left_suffix[left_idx..] == right_suffix[right_idx..]
50-
} else {
51-
false
52-
}
53-
} else if let Some((prefix, suffix)) = left_name.split_once(['$', '.'])
54-
&& suffix.chars().all(char::is_numeric)
55-
{
56-
// Match Metrowerks symbol$1234 against symbol$2345
57-
// and GCC symbol.1234 against symbol.2345
58-
right_name
59-
.split_once(['$', '.'])
60-
.is_some_and(|(p, s)| p == prefix && s.chars().all(char::is_numeric))
61-
} else {
6242
left_name == right_name
43+
} else {
44+
left.name == right.name
6345
}
6446
}
6547

@@ -73,7 +55,7 @@ fn reloc_eq(
7355
return false;
7456
}
7557

76-
let symbol_name_addend_matches = symbol_name_matches(&left.symbol.name, &right.symbol.name)
58+
let symbol_name_addend_matches = symbol_name_matches(left.symbol, right.symbol)
7759
&& left.relocation.addend == right.relocation.addend;
7860
match (left.symbol.section, right.symbol.section) {
7961
(Some(sl), Some(sr)) => {

objdiff-core/src/diff/display.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -812,19 +812,15 @@ pub fn display_sections(
812812
}
813813
let section_diff = &diff.sections[section_idx];
814814
let reverse_fn_order = section.kind == SectionKind::Code && reverse_fn_order;
815-
symbols.sort_by(|a, b| {
816-
let a = &obj.symbols[a.symbol];
817-
let b = &obj.symbols[b.symbol];
818-
section_symbol_sort(a, b)
819-
.then_with(|| {
820-
if reverse_fn_order {
821-
b.address.cmp(&a.address)
822-
} else {
823-
a.address.cmp(&b.address)
824-
}
825-
})
826-
.then_with(|| a.size.cmp(&b.size))
827-
});
815+
if reverse_fn_order {
816+
symbols.sort_by(|a, b| {
817+
let a = &obj.symbols[a.symbol];
818+
let b = &obj.symbols[b.symbol];
819+
section_symbol_sort(a, b)
820+
.then_with(|| b.address.cmp(&a.address))
821+
.then_with(|| a.size.cmp(&b.size))
822+
});
823+
}
828824
sections.push(SectionDisplay {
829825
id: section.id.clone(),
830826
name: if section.flags.contains(SectionFlag::Combined) {

objdiff-core/src/diff/mod.rs

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use alloc::{
77
use core::{num::NonZeroU32, ops::Range};
88

99
use anyhow::Result;
10-
use itertools::Itertools;
1110

1211
use crate::{
1312
diff::{
@@ -687,18 +686,6 @@ fn symbol_section_kind(obj: &Object, symbol: &Symbol) -> SectionKind {
687686
}
688687
}
689688

690-
/// Check if a symbol is a compiler-generated like @1234 or _$E1234.
691-
fn is_symbol_compiler_generated(symbol: &Symbol) -> bool {
692-
if symbol.name.starts_with('@') && symbol.name[1..].chars().all(char::is_numeric) {
693-
// Exclude @stringBase0, @GUARD@, etc.
694-
return true;
695-
}
696-
if symbol.name.starts_with("_$E") && symbol.name[3..].chars().all(char::is_numeric) {
697-
return true;
698-
}
699-
false
700-
}
701-
702689
fn find_symbol(
703690
obj: Option<&Object>,
704691
in_obj: &Object,
@@ -712,7 +699,7 @@ fn find_symbol(
712699

713700
// Match compiler-generated symbols against each other (e.g. @251 -> @60)
714701
// If they are in the same section and have the same value
715-
if is_symbol_compiler_generated(in_symbol)
702+
if in_symbol.flags.contains(SymbolFlag::CompilerGenerated)
716703
&& matches!(section_kind, SectionKind::Code | SectionKind::Data | SectionKind::Bss)
717704
{
718705
let mut closest_match_symbol_idx = None;
@@ -724,7 +711,7 @@ fn find_symbol(
724711
if obj.sections[section_index].name != section_name {
725712
continue;
726713
}
727-
if !is_symbol_compiler_generated(symbol) {
714+
if !symbol.flags.contains(SymbolFlag::CompilerGenerated) {
728715
continue;
729716
}
730717
match section_kind {
@@ -761,15 +748,11 @@ fn find_symbol(
761748
}
762749

763750
// Try to find a symbol with a matching name
764-
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used)
765-
.filter(|&(_, symbol)| {
766-
symbol_name_matches(&in_symbol.name, &symbol.name)
767-
&& symbol_section_kind(obj, symbol) == section_kind
768-
&& symbol_section(obj, symbol).is_some_and(|(name, _)| name == section_name)
769-
})
770-
.sorted_unstable_by_key(|&(_, symbol)| (symbol.section, symbol.address))
771-
.next()
772-
{
751+
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|&(_, symbol)| {
752+
symbol_name_matches(in_symbol, symbol)
753+
&& symbol_section_kind(obj, symbol) == section_kind
754+
&& symbol_section(obj, symbol).is_some_and(|(name, _)| name == section_name)
755+
}) {
773756
return Some(symbol_idx);
774757
}
775758

objdiff-core/src/obj/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub enum SectionKind {
3737

3838
flags! {
3939
#[derive(Hash)]
40-
pub enum SymbolFlag: u8 {
40+
pub enum SymbolFlag: u16 {
4141
Global,
4242
Local,
4343
Weak,
@@ -50,6 +50,8 @@ flags! {
5050
SizeInferred,
5151
/// Symbol should be ignored by any diffing
5252
Ignored,
53+
/// Symbol name is compiler-generated; compare by value instead of name
54+
CompilerGenerated,
5355
}
5456
}
5557

@@ -264,6 +266,7 @@ pub trait FlowAnalysisResult: core::fmt::Debug + Send {
264266
pub struct Symbol {
265267
pub name: String,
266268
pub demangled_name: Option<String>,
269+
pub normalized_name: Option<String>,
267270
pub address: u64,
268271
pub size: u64,
269272
pub kind: SymbolKind,
@@ -403,6 +406,7 @@ pub struct ResolvedInstructionRef<'obj> {
403406
static DUMMY_SYMBOL: Symbol = Symbol {
404407
name: String::new(),
405408
demangled_name: None,
409+
normalized_name: None,
406410
address: 0,
407411
size: 0,
408412
kind: SymbolKind::Unknown,

objdiff-core/src/obj/read.rs

Lines changed: 92 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use alloc::{
33
collections::BTreeMap,
44
format,
55
string::{String, ToString},
6+
vec,
67
vec::Vec,
78
};
89
use core::{cmp::Ordering, num::NonZeroU64};
@@ -35,6 +36,46 @@ fn map_section_kind(section: &object::Section) -> SectionKind {
3536
}
3637
}
3738

39+
/// Check if a symbol's name is partially compiler-generated, and if so normalize it for pairing.
40+
/// e.g. symbol$1234 and symbol$2345 will both be replaced with symbol$0000 internally.
41+
fn get_normalized_symbol_name(name: &str) -> Option<String> {
42+
const DUMMY_UNIQUE_ID: &str = "0000";
43+
if let Some((prefix, suffix)) = name.split_once("@class$")
44+
&& let Some(idx) = suffix.chars().position(|c| !c.is_numeric())
45+
&& idx > 0
46+
{
47+
// Match Metrowerks anonymous class symbol names, ignoring the unique ID.
48+
// e.g. __dt__Q29dCamera_c23@class$3665d_camera_cppFv
49+
// and: __dt__Q29dCamera_c23@class$1727d_camera_cppFv
50+
let suffix = &suffix[idx..];
51+
Some(format!("{prefix}@class${DUMMY_UNIQUE_ID}{suffix}"))
52+
} else if let Some((prefix, suffix)) = name.split_once('$')
53+
&& suffix.chars().all(char::is_numeric)
54+
{
55+
// Match Metrowerks symbol$1234 against symbol$2345
56+
Some(format!("{prefix}${DUMMY_UNIQUE_ID}"))
57+
} else if let Some((prefix, suffix)) = name.split_once('.')
58+
&& suffix.chars().all(char::is_numeric)
59+
{
60+
// Match GCC symbol.1234 against symbol.2345
61+
Some(format!("{prefix}.{DUMMY_UNIQUE_ID}"))
62+
} else {
63+
None
64+
}
65+
}
66+
67+
/// Check if a symbol's name is entirely compiler-generated, such as @1234 or _$E1234.
68+
/// This enables pairing these symbols up by their value instead of their name.
69+
fn is_symbol_name_compiler_generated(name: &str) -> bool {
70+
if name.starts_with('@') && name[1..].chars().all(char::is_numeric) {
71+
// Exclude @stringBase0, @GUARD@, etc.
72+
return true;
73+
} else if name.starts_with("_$E") && name[3..].chars().all(char::is_numeric) {
74+
return true;
75+
}
76+
false
77+
}
78+
3879
fn map_symbol(
3980
arch: &dyn Arch,
4081
file: &object::File,
@@ -97,10 +138,15 @@ fn map_symbol(
97138
.and_then(|m| m.virtual_addresses.as_ref())
98139
.and_then(|v| v.get(symbol.index().0).cloned());
99140
let section = symbol.section_index().and_then(|i| section_indices.get(i.0).copied());
141+
let normalized_name = get_normalized_symbol_name(&name);
142+
if is_symbol_name_compiler_generated(&name) {
143+
flags |= SymbolFlag::CompilerGenerated;
144+
}
100145

101146
Ok(Symbol {
102147
name,
103148
demangled_name,
149+
normalized_name,
104150
address,
105151
size,
106152
kind,
@@ -119,13 +165,38 @@ fn map_symbols(
119165
split_meta: Option<&SplitMeta>,
120166
config: &DiffObjConfig,
121167
) -> Result<(Vec<Symbol>, Vec<usize>)> {
122-
let symbol_count = obj_file.symbols().count();
123-
let mut symbols = Vec::<Symbol>::with_capacity(symbol_count + obj_file.sections().count());
124-
let mut symbol_indices = Vec::<usize>::with_capacity(symbol_count + 1);
125-
for obj_symbol in obj_file.symbols() {
126-
if symbol_indices.len() <= obj_symbol.index().0 {
127-
symbol_indices.resize(obj_symbol.index().0 + 1, usize::MAX);
128-
}
168+
// symbols() is not guaranteed to be sorted by address.
169+
// We sort it here to fix pairing bugs with diff algorithms that assume the symbols are ordered.
170+
// Sorting everything here once is less expensive than sorting subsets later in expensive loops.
171+
let mut max_index = 0;
172+
let mut obj_symbols = obj_file
173+
.symbols()
174+
.filter(|s| s.kind() != object::SymbolKind::File)
175+
.inspect(|sym| max_index = max_index.max(sym.index().0))
176+
.collect::<Vec<_>>();
177+
obj_symbols.sort_by(|a, b| {
178+
// Sort symbols by section index, placing absolute symbols last
179+
a.section_index()
180+
.map_or(usize::MAX, |s| s.0)
181+
.cmp(&b.section_index().map_or(usize::MAX, |s| s.0))
182+
.then_with(|| {
183+
// Sort section symbols first in a section
184+
if a.kind() == object::SymbolKind::Section {
185+
Ordering::Less
186+
} else if b.kind() == object::SymbolKind::Section {
187+
Ordering::Greater
188+
} else {
189+
Ordering::Equal
190+
}
191+
})
192+
// Sort by address within section
193+
.then_with(|| a.address().cmp(&b.address()))
194+
// If there are multiple symbols with the same address, smaller symbol first
195+
.then_with(|| a.size().cmp(&b.size()))
196+
});
197+
let mut symbols = Vec::<Symbol>::with_capacity(obj_symbols.len() + obj_file.sections().count());
198+
let mut symbol_indices = vec![usize::MAX; max_index + 1];
199+
for obj_symbol in obj_symbols {
129200
let symbol = map_symbol(arch, obj_file, &obj_symbol, section_indices, split_meta, config)?;
130201
symbol_indices[obj_symbol.index().0] = symbols.len();
131202
symbols.push(symbol);
@@ -172,6 +243,7 @@ fn add_section_symbols(sections: &[Section], symbols: &mut Vec<Symbol>) {
172243
symbols.push(Symbol {
173244
name,
174245
demangled_name: None,
246+
normalized_name: None,
175247
address: 0,
176248
size,
177249
kind: SymbolKind::Section,
@@ -193,40 +265,18 @@ fn is_local_label(symbol: &Symbol) -> bool {
193265
}
194266

195267
fn infer_symbol_sizes(arch: &dyn Arch, symbols: &mut [Symbol], sections: &[Section]) -> Result<()> {
196-
// Create a sorted list of symbol indices by section
197-
let mut symbols_with_section = Vec::<usize>::with_capacity(symbols.len());
198-
for (i, symbol) in symbols.iter().enumerate() {
199-
if symbol.section.is_some() {
200-
symbols_with_section.push(i);
201-
}
202-
}
203-
symbols_with_section.sort_by(|a, b| {
204-
let a = &symbols[*a];
205-
let b = &symbols[*b];
206-
a.section
207-
.unwrap_or(usize::MAX)
208-
.cmp(&b.section.unwrap_or(usize::MAX))
209-
.then_with(|| {
210-
// Sort section symbols first
211-
if a.kind == SymbolKind::Section {
212-
Ordering::Less
213-
} else if b.kind == SymbolKind::Section {
214-
Ordering::Greater
215-
} else {
216-
Ordering::Equal
217-
}
218-
})
219-
.then_with(|| a.address.cmp(&b.address))
220-
.then_with(|| a.size.cmp(&b.size))
221-
});
268+
// Above, we've sorted the symbols by section and then by address.
222269

223270
// Set symbol sizes based on the next symbol's address
224271
let mut iter_idx = 0;
225272
let mut last_end = (0, 0);
226-
while iter_idx < symbols_with_section.len() {
227-
let symbol_idx = symbols_with_section[iter_idx];
273+
while iter_idx < symbols.len() {
274+
let symbol_idx = iter_idx;
228275
let symbol = &symbols[symbol_idx];
229-
let section_idx = symbol.section.unwrap();
276+
let Some(section_idx) = symbol.section else {
277+
// Start of absolute symbols
278+
break;
279+
};
230280
iter_idx += 1;
231281
if symbol.size != 0 {
232282
if symbol.kind != SymbolKind::Section {
@@ -239,10 +289,9 @@ fn infer_symbol_sizes(arch: &dyn Arch, symbols: &mut [Symbol], sections: &[Secti
239289
continue;
240290
}
241291
let next_symbol = loop {
242-
if iter_idx >= symbols_with_section.len() {
292+
let Some(next_symbol) = symbols.get(iter_idx) else {
243293
break None;
244-
}
245-
let next_symbol = &symbols[symbols_with_section[iter_idx]];
294+
};
246295
if next_symbol.section != Some(section_idx) {
247296
break None;
248297
}
@@ -298,9 +347,11 @@ fn map_sections(
298347
split_meta: Option<&SplitMeta>,
299348
) -> Result<(Vec<Section>, Vec<usize>)> {
300349
let mut section_names = BTreeMap::<String, usize>::new();
301-
let section_count = obj_file.sections().count();
350+
let mut max_index = 0;
351+
let section_count =
352+
obj_file.sections().inspect(|s| max_index = max_index.max(s.index().0)).count();
302353
let mut result = Vec::<Section>::with_capacity(section_count);
303-
let mut section_indices = Vec::<usize>::with_capacity(section_count + 1);
354+
let mut section_indices = vec![usize::MAX; max_index + 1];
304355
for section in obj_file.sections() {
305356
let name = section.name().context("Failed to process section name")?;
306357
let kind = map_section_kind(&section);
@@ -325,9 +376,6 @@ fn map_sections(
325376
let id = format!("{name}-{unique_id}");
326377
*unique_id += 1;
327378

328-
if section_indices.len() <= section.index().0 {
329-
section_indices.resize(section.index().0 + 1, usize::MAX);
330-
}
331379
section_indices[section.index().0] = result.len();
332380
result.push(Section {
333381
id,

0 commit comments

Comments
 (0)