Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions objdiff-cli/src/cmd/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ impl AppState {
JobResult::CheckUpdate(_) => todo!("CheckUpdate"),
JobResult::Update(_) => todo!("Update"),
JobResult::CreateScratch(_) => todo!("CreateScratch"),
JobResult::FindSimilar(_) => unreachable!("Unexpected JobResult::FindSimilar"),
}
}
Ok(redraw)
Expand Down
43 changes: 43 additions & 0 deletions objdiff-core/src/diff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,49 @@ pub fn diff_objs(
})
}

/// Score entry for a candidate symbol when searching for similar functions.
#[derive(Debug, Clone)]
pub struct SimilarSymbol {
pub symbol_idx: usize,
pub match_percent: f32,
}

/// Find all code symbols in `target_obj` that are similar to a given symbol in `source_obj`,
/// sorted descending by similarity score. Symbols that fail to score are silently skipped.
pub fn find_similar_code_symbols(
source_obj: &Object,
source_symbol_idx: usize,
target_obj: &Object,
diff_config: &DiffObjConfig,
) -> Vec<SimilarSymbol> {
let source_symbol = &source_obj.symbols[source_symbol_idx];
let source_section_kind = symbol_section_kind(source_obj, source_symbol);
if source_section_kind != SectionKind::Code {
return vec![];
}

let mut results = Vec::new();
for (target_idx, target_symbol) in target_obj.symbols.iter().enumerate() {
if target_symbol.size == 0 || target_symbol.flags.contains(SymbolFlag::Ignored) {
continue;
}
if symbol_section_kind(target_obj, target_symbol) != SectionKind::Code {
continue;
}
let Ok((left_diff, _)) =
diff_code(source_obj, target_obj, source_symbol_idx, target_idx, diff_config)
else {
continue;
};
let Some(match_percent) = left_diff.match_percent else { continue };
results.push(SimilarSymbol { symbol_idx: target_idx, match_percent });
}
results.sort_by(|a, b| {
b.match_percent.partial_cmp(&a.match_percent).unwrap_or(core::cmp::Ordering::Equal)
});
results
}

#[derive(Clone, Copy)]
enum MappingSymbol<'a> {
Left(&'a str),
Expand Down
181 changes: 181 additions & 0 deletions objdiff-core/src/jobs/find_similar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
use std::{sync::mpsc::Receiver, task::Waker};

use anyhow::Result;
use typed_path::Utf8PlatformPathBuf;

use crate::{
build::{BuildConfig, run_make},
diff::{DiffObjConfig, DiffSide, display::InstructionPart, find_similar_code_symbols},
jobs::{Job, JobContext, JobResult, JobState, start_job, update_status},
obj::{InstructionArg, read},
};

pub struct FindSimilarConfig {
/// Path to the source object file to search for.
pub source_path: Utf8PlatformPathBuf,
/// Mangled name of the source symbol.
pub source_symbol_name: String,
/// 0 = left/target column, 1 = right/base column.
pub source_column: usize,
/// All project objects to scan.
pub objects: Vec<ScanObject>,
pub diff_config: DiffObjConfig,
pub build_config: BuildConfig,
pub build_base: bool,
pub build_target: bool,
}

pub struct ScanObject {
pub name: String,
pub target_path: Option<Utf8PlatformPathBuf>,
pub base_path: Option<Utf8PlatformPathBuf>,
}

#[derive(Debug, Clone)]
pub struct SimilarFunctionMatch {
pub symbol_name: String,
pub demangled_name: Option<String>,
pub match_percent: f32,
/// Human-readable identifier: "ObjectName (target)" or "ObjectName (base)".
pub object_name: String,
}

pub struct FindSimilarResult {
pub source_symbol_name: String,
pub source_column: usize,
pub matches: Vec<SimilarFunctionMatch>,
}

fn run_find_similar(
context: &JobContext,
cancel: Receiver<()>,
config: FindSimilarConfig,
) -> Result<Box<FindSimilarResult>> {
let diff_side = if config.source_column == 0 { DiffSide::Target } else { DiffSide::Base };
let source_obj = read::read(config.source_path.as_ref(), &config.diff_config, diff_side)?;
let source_symbol_idx =
source_obj.symbol_by_name(&config.source_symbol_name).ok_or_else(|| {
anyhow::anyhow!("Source symbol '{}' not found", config.source_symbol_name)
})?;

// Print the instructions of the source symbol to the console.
'print: {
let symbol = &source_obj.symbols[source_symbol_idx];
let Some(section_index) = symbol.section else { break 'print };
let section = &source_obj.sections[section_index];
let Some(data) = section.data_range(symbol.address, symbol.size as usize) else {
break 'print;
};
let Ok(instructions) = source_obj.arch.scan_instructions(
crate::obj::ResolvedSymbol {
obj: &source_obj,
symbol_index: source_symbol_idx,
symbol,
section_index,
section,
data,
},
&config.diff_config,
) else {
break 'print;
};
log::info!(
"find_similar: source symbol '{}' — {} instructions",
config.source_symbol_name,
instructions.len()
);
for ins_ref in &instructions {
let Some(resolved) = source_obj.resolve_instruction_ref(source_symbol_idx, *ins_ref)
else {
continue;
};
let mut text = format!("{:#010x} ", ins_ref.address);
let _ =
source_obj.arch.display_instruction(resolved, &config.diff_config, &mut |part| {
match part {
InstructionPart::Basic(s) | InstructionPart::Opcode(s, _) => {
text.push_str(&s)
}
InstructionPart::Arg(InstructionArg::Value(v)) => {
text.push_str(&v.to_string())
}
InstructionPart::Arg(InstructionArg::BranchDest(addr)) => {
text.push_str(&format!("{addr:#x}"))
}
InstructionPart::Arg(InstructionArg::Reloc) => {
if let Some(reloc) = resolved.relocation {
let sym = &source_obj.symbols[reloc.relocation.target_symbol];
text.push_str(sym.demangled_name.as_deref().unwrap_or(&sym.name));
if reloc.relocation.addend != 0 {
text.push_str(&format!("+{:#x}", reloc.relocation.addend));
}
} else {
text.push_str("<reloc>");
}
}
InstructionPart::Separator => text.push_str(", "),
}
Ok(())
});
log::info!("{text}");
}
}

let total = config.objects.len() as u32;
let mut all_matches = Vec::new();

for (idx, scan_obj) in config.objects.iter().enumerate() {
update_status(context, format!("Scanning {}", scan_obj.name), idx as u32, total, &cancel)?;

let project_dir = config.build_config.project_dir.as_deref();

for side in [DiffSide::Target, DiffSide::Base] {
let (path, should_build) = match side {
DiffSide::Target => (scan_obj.target_path.as_ref(), config.build_target),
DiffSide::Base => (scan_obj.base_path.as_ref(), config.build_base),
};
let Some(path) = path else { continue };

if should_build
&& let Some(project_dir) = project_dir
&& let Ok(rel_path) = path.strip_prefix(project_dir)
{
run_make(&config.build_config, rel_path.with_unix_encoding().as_ref());
}

let Ok(obj) = read::read(path.as_ref(), &config.diff_config, side) else { continue };
let similar = find_similar_code_symbols(
&source_obj,
source_symbol_idx,
&obj,
&config.diff_config,
);
let side_label = if side == DiffSide::Target { "target" } else { "base" };
for sim in similar {
let symbol = &obj.symbols[sim.symbol_idx];
all_matches.push(SimilarFunctionMatch {
symbol_name: symbol.name.clone(),
demangled_name: symbol.demangled_name.clone(),
match_percent: sim.match_percent,
object_name: format!("{} ({})", scan_obj.name, side_label),
});
}
}
}

all_matches.sort_by(|a, b| {
b.match_percent.partial_cmp(&a.match_percent).unwrap_or(std::cmp::Ordering::Equal)
});

Ok(Box::new(FindSimilarResult {
source_symbol_name: config.source_symbol_name,
source_column: config.source_column,
matches: all_matches,
}))
}

pub fn start_find_similar(waker: Waker, config: FindSimilarConfig) -> JobState {
start_job(waker, "Find similar functions", Job::FindSimilar, move |context, cancel| {
run_find_similar(&context, cancel, config).map(|r| JobResult::FindSimilar(Some(r)))
})
}
7 changes: 5 additions & 2 deletions objdiff-core/src/jobs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ use std::{
use anyhow::Result;

use crate::jobs::{
check_update::CheckUpdateResult, create_scratch::CreateScratchResult, objdiff::ObjDiffResult,
update::UpdateResult,
check_update::CheckUpdateResult, create_scratch::CreateScratchResult,
find_similar::FindSimilarResult, objdiff::ObjDiffResult, update::UpdateResult,
};

pub mod check_update;
pub mod create_scratch;
pub mod find_similar;
pub mod objdiff;
pub mod update;

Expand All @@ -26,6 +27,7 @@ pub enum Job {
CheckUpdate,
Update,
CreateScratch,
FindSimilar,
}
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);

Expand Down Expand Up @@ -168,6 +170,7 @@ pub enum JobResult {
CheckUpdate(Option<Box<CheckUpdateResult>>),
Update(Box<UpdateResult>),
CreateScratch(Option<Box<CreateScratchResult>>),
FindSimilar(Option<Box<FindSimilarResult>>),
}

fn start_job(
Expand Down
3 changes: 2 additions & 1 deletion objdiff-gui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,8 @@ impl eframe::App for App {

frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);

let side_panel_available = diff_state.current_view == View::SymbolDiff;
let side_panel_available =
diff_state.current_view == View::SymbolDiff && diff_state.similar_functions.is_none();

egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
// Temporarily use pre-egui 0.32 menu. ComboBox within menu
Expand Down
35 changes: 34 additions & 1 deletion objdiff-gui/src/jobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{
};

use anyhow::{Result, bail};
use jobs::create_scratch;
use jobs::{create_scratch, find_similar};
use objdiff_core::{
build::BuildConfig,
diff::MappingConfig,
Expand Down Expand Up @@ -126,6 +126,39 @@ pub fn start_build(ctx: &egui::Context, jobs: &mut JobQueue, config: objdiff::Ob
jobs.push_once(Job::ObjDiff, || objdiff::start_build(egui_waker(ctx), config));
}

pub fn start_find_similar_job(
ctx: &egui::Context,
jobs: &mut JobQueue,
state: &AppState,
source_symbol_name: String,
column: usize,
) {
let Some(selected_obj) = &state.config.selected_obj else { return };
let source_path =
if column == 0 { selected_obj.target_path.clone() } else { selected_obj.base_path.clone() };
let Some(source_path) = source_path else { return };
let objects = state
.objects
.iter()
.map(|o| find_similar::ScanObject {
name: o.name.clone(),
target_path: o.target_path.clone(),
base_path: o.base_path.clone(),
})
.collect();
let config = find_similar::FindSimilarConfig {
source_path,
source_symbol_name,
source_column: column,
objects,
diff_config: state.effective_diff_config(),
build_config: BuildConfig::from(&state.config),
build_base: state.config.build_base,
build_target: state.config.build_target,
};
jobs.push_once(Job::FindSimilar, || find_similar::start_find_similar(egui_waker(ctx), config));
}

pub fn start_check_update(ctx: &egui::Context, jobs: &mut JobQueue) {
jobs.push_once(Job::Update, || {
jobs::check_update::start_check_update(egui_waker(ctx), CheckUpdateConfig {
Expand Down
Loading
Loading