Experimental objdiff-cli (WIP)

This commit is contained in:
2024-02-27 18:47:51 -07:00
parent 4eba5f71b0
commit 9a7d2bcebf
23 changed files with 1541 additions and 501 deletions

322
objdiff-cli/src/cmd/diff.rs Normal file
View File

@@ -0,0 +1,322 @@
use std::{
io::{stdout, Write},
path::PathBuf,
};
use anyhow::{bail, Context, Result};
use argp::FromArgs;
use crossterm::{
cursor::{Hide, MoveRight, MoveTo, Show},
event,
event::{
DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseEventKind,
},
style::{Color, PrintStyledContent, Stylize},
terminal::{
disable_raw_mode, enable_raw_mode, size as terminal_size, Clear, ClearType,
EnterAlternateScreen, LeaveAlternateScreen, SetTitle,
},
};
use event::KeyModifiers;
use objdiff_core::{
diff,
diff::display::{display_diff, DiffText},
obj,
obj::{ObjInfo, ObjInsDiffKind, ObjSection, ObjSectionKind, ObjSymbol},
};
use crate::util::term::crossterm_panic_handler;
#[derive(FromArgs, PartialEq, Debug)]
/// Diff two object files.
#[argp(subcommand, name = "diff")]
pub struct Args {
#[argp(positional)]
/// Target object file
target: PathBuf,
#[argp(positional)]
/// Base object file
base: PathBuf,
#[argp(option, short = 's')]
/// Function symbol to diff
symbol: String,
}
pub fn run(args: Args) -> Result<()> {
let mut target = obj::elf::read(&args.target)
.with_context(|| format!("Loading {}", args.target.display()))?;
let mut base =
obj::elf::read(&args.base).with_context(|| format!("Loading {}", args.base.display()))?;
let config = diff::DiffObjConfig::default();
diff::diff_objs(&config, Some(&mut target), Some(&mut base))?;
let left_sym = find_function(&target, &args.symbol);
let right_sym = find_function(&base, &args.symbol);
let max_len = match (left_sym, right_sym) {
(Some((_, l)), Some((_, r))) => l.instructions.len().max(r.instructions.len()),
(Some((_, l)), None) => l.instructions.len(),
(None, Some((_, r))) => r.instructions.len(),
(None, None) => bail!("Symbol not found: {}", args.symbol),
};
crossterm_panic_handler();
enable_raw_mode()?;
crossterm::queue!(
stdout(),
EnterAlternateScreen,
SetTitle(format!("{} - objdiff", args.symbol)),
Hide,
EnableMouseCapture,
)?;
let mut redraw = true;
let mut skip = 0;
loop {
let y_offset = 2;
let (sx, sy) = terminal_size()?;
let per_page = sy as usize - y_offset;
if redraw {
let mut w = stdout().lock();
crossterm::queue!(
w,
Clear(ClearType::All),
MoveTo(0, 0),
PrintStyledContent(args.symbol.clone().with(Color::White)),
MoveTo(0, 1),
PrintStyledContent(" ".repeat(sx as usize).underlined()),
MoveTo(0, 1),
PrintStyledContent("TARGET ".underlined()),
MoveTo(sx / 2, 0),
PrintStyledContent("Last built: 18:24:20".with(Color::White)),
MoveTo(sx / 2, 1),
PrintStyledContent("BASE ".underlined()),
)?;
if let Some(percent) = right_sym.and_then(|(_, s)| s.match_percent) {
crossterm::queue!(
w,
PrintStyledContent(
format!("{:.2}%", percent).with(match_percent_color(percent)).underlined()
)
)?;
}
if skip > max_len - per_page {
skip = max_len - per_page;
}
if let Some((_, symbol)) = left_sym {
print_sym(&mut w, symbol, 0, y_offset as u16, sx / 2 - 1, sy, skip)?;
}
if let Some((_, symbol)) = right_sym {
print_sym(&mut w, symbol, sx / 2, y_offset as u16, sx, sy, skip)?;
}
w.flush()?;
redraw = false;
}
match event::read()? {
Event::Key(event)
if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) =>
{
match event.code {
// Quit
KeyCode::Esc | KeyCode::Char('q') => break,
// Page up
KeyCode::PageUp => {
skip = skip.saturating_sub(per_page);
redraw = true;
}
// Page up (shift + space)
KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => {
skip = skip.saturating_sub(per_page);
redraw = true;
}
// Page down
KeyCode::Char(' ') | KeyCode::PageDown => {
skip += per_page;
redraw = true;
}
// Scroll down
KeyCode::Down | KeyCode::Char('j') => {
skip += 1;
redraw = true;
}
// Scroll up
KeyCode::Up | KeyCode::Char('k') => {
skip = skip.saturating_sub(1);
redraw = true;
}
// Scroll to start
KeyCode::Char('g') => {
skip = 0;
redraw = true;
}
// Scroll to end
KeyCode::Char('G') => {
skip = max_len;
redraw = true;
}
_ => {}
}
}
Event::Mouse(event) => match event.kind {
MouseEventKind::ScrollDown => {
skip += 3;
redraw = true;
}
MouseEventKind::ScrollUp => {
skip = skip.saturating_sub(3);
redraw = true;
}
_ => {}
},
Event::Resize(_, _) => redraw = true,
_ => {}
}
}
// Reset terminal
crossterm::execute!(stdout(), LeaveAlternateScreen, Show, DisableMouseCapture)?;
disable_raw_mode()?;
Ok(())
}
fn find_function<'a>(obj: &'a ObjInfo, name: &str) -> Option<(&'a ObjSection, &'a ObjSymbol)> {
for section in &obj.sections {
if section.kind != ObjSectionKind::Code {
continue;
}
for symbol in &section.symbols {
if symbol.name == name {
return Some((section, symbol));
}
}
}
None
}
fn print_sym<W>(
w: &mut W,
symbol: &ObjSymbol,
sx: u16,
mut sy: u16,
max_sx: u16,
max_sy: u16,
skip: usize,
) -> Result<()>
where
W: Write,
{
let base_addr = symbol.address as u32;
for ins_diff in symbol.instructions.iter().skip(skip) {
let mut sx = sx;
if ins_diff.kind != ObjInsDiffKind::None && sx > 2 {
crossterm::queue!(w, MoveTo(sx - 2, sy))?;
let s = match ins_diff.kind {
ObjInsDiffKind::Delete => "< ",
ObjInsDiffKind::Insert => "> ",
_ => "| ",
};
crossterm::queue!(w, PrintStyledContent(s.with(Color::DarkGrey)))?;
} else {
crossterm::queue!(w, MoveTo(sx, sy))?;
}
display_diff(ins_diff, base_addr, |text| {
let mut label_text;
let mut base_color = match ins_diff.kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
Color::Grey
}
ObjInsDiffKind::Replace => Color::DarkCyan,
ObjInsDiffKind::Delete => Color::DarkRed,
ObjInsDiffKind::Insert => Color::DarkGreen,
};
let mut pad_to = 0;
match text {
DiffText::Basic(text) => {
label_text = text.to_string();
}
DiffText::BasicColor(s, idx) => {
label_text = s.to_string();
base_color = COLOR_ROTATION[idx % COLOR_ROTATION.len()];
}
DiffText::Line(num) => {
label_text = format!("{num} ");
base_color = Color::DarkGrey;
pad_to = 5;
}
DiffText::Address(addr) => {
label_text = format!("{:x}:", addr);
pad_to = 5;
}
DiffText::Opcode(mnemonic, _op) => {
label_text = mnemonic.to_string();
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
base_color = Color::Blue;
}
pad_to = 8;
}
DiffText::Argument(arg, diff) => {
label_text = arg.to_string();
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::BranchTarget(addr) => {
label_text = format!("{addr:x}");
}
DiffText::Symbol(sym) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone();
base_color = Color::White;
}
DiffText::Spacing(n) => {
crossterm::queue!(w, MoveRight(n as u16))?;
sx += n as u16;
return Ok(());
}
DiffText::Eol => {
sy += 1;
return Ok(());
}
}
let len = label_text.len();
if sx >= max_sx {
return Ok(());
}
label_text.truncate(max_sx as usize - sx as usize);
crossterm::queue!(w, PrintStyledContent(label_text.with(base_color)))?;
sx += len as u16;
if pad_to > len {
let pad = (pad_to - len) as u16;
crossterm::queue!(w, MoveRight(pad))?;
sx += pad;
}
Ok(())
})?;
if sy >= max_sy {
break;
}
}
Ok(())
}
pub const COLOR_ROTATION: [Color; 8] = [
Color::Magenta,
Color::Cyan,
Color::Green,
Color::Red,
Color::Yellow,
Color::DarkMagenta,
Color::Blue,
Color::Green,
];
pub fn match_percent_color(match_percent: f32) -> Color {
if match_percent == 100.0 {
Color::Green
} else if match_percent >= 50.0 {
Color::Blue
} else {
Color::Red
}
}

View File

@@ -0,0 +1,2 @@
pub mod diff;
pub mod report;

View File

@@ -0,0 +1,229 @@
use std::{
collections::HashSet,
fs::File,
io::{BufWriter, Write},
path::{Path, PathBuf},
time::Instant,
};
use anyhow::{bail, Context, Result};
use argp::FromArgs;
use objdiff_core::{
config::ProjectObject,
diff, obj,
obj::{ObjSectionKind, ObjSymbolFlags},
};
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
#[derive(FromArgs, PartialEq, Debug)]
/// Generate a report from a project.
#[argp(subcommand, name = "report")]
pub struct Args {
#[argp(option, short = 'p')]
/// Project directory
project: Option<PathBuf>,
#[argp(option, short = 'o')]
/// Output JSON file
output: Option<PathBuf>,
#[argp(switch, short = 'd')]
/// Deduplicate global and weak symbols
deduplicate: bool,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
struct Report {
fuzzy_match_percent: f32,
total_size: u64,
matched_size: u64,
matched_size_percent: f32,
total_functions: u32,
matched_functions: u32,
matched_functions_percent: f32,
units: Vec<ReportUnit>,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
struct ReportUnit {
name: String,
match_percent: f32,
total_size: u64,
matched_size: u64,
total_functions: u32,
matched_functions: u32,
#[serde(skip_serializing_if = "Option::is_none")]
complete: Option<bool>,
functions: Vec<ReportFunction>,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
struct ReportFunction {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
demangled_name: Option<String>,
size: u64,
match_percent: f32,
}
pub fn run(args: Args) -> Result<()> {
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
log::info!("Loading project {}", project_dir.display());
let config = objdiff_core::config::try_project_config(project_dir);
let Some((Ok(mut project), _)) = config else {
bail!("No project configuration found");
};
log::info!(
"Generating report for {} units (using {} threads)",
project.objects.len(),
if args.deduplicate { 1 } else { rayon::current_num_threads() }
);
let start = Instant::now();
let mut report = Report::default();
let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate {
// If deduplicating, we need to run single-threaded
for object in &mut project.objects {
if let Some(unit) = report_object(
object,
project_dir,
project.target_dir.as_deref(),
project.base_dir.as_deref(),
Some(&mut existing_functions),
)? {
report.units.push(unit);
}
}
} else {
let units = project
.objects
.par_iter_mut()
.map(|object| {
report_object(
object,
project_dir,
project.target_dir.as_deref(),
project.base_dir.as_deref(),
None,
)
})
.collect::<Result<Vec<Option<ReportUnit>>>>()?;
report.units = units.into_iter().flatten().collect::<Vec<ReportUnit>>();
}
for unit in &report.units {
report.fuzzy_match_percent += unit.match_percent * unit.total_size as f32;
report.total_size += unit.total_size;
report.matched_size += unit.matched_size;
report.total_functions += unit.total_functions;
report.matched_functions += unit.matched_functions;
}
if report.total_size == 0 {
report.fuzzy_match_percent = 100.0;
} else {
report.fuzzy_match_percent /= report.total_size as f32;
}
report.matched_size_percent = if report.total_size == 0 {
100.0
} else {
report.matched_size as f32 / report.total_size as f32 * 100.0
};
report.matched_functions_percent = if report.total_functions == 0 {
100.0
} else {
report.matched_functions as f32 / report.total_functions as f32 * 100.0
};
let duration = start.elapsed();
log::info!("Report generated in {}.{:03}s", duration.as_secs(), duration.subsec_millis());
if let Some(output) = &args.output {
log::info!("Writing to {}", output.display());
let mut output = BufWriter::new(
File::create(output)
.with_context(|| format!("Failed to create file {}", output.display()))?,
);
serde_json::to_writer_pretty(&mut output, &report)?;
output.flush()?;
} else {
serde_json::to_writer_pretty(std::io::stdout(), &report)?;
}
Ok(())
}
fn report_object(
object: &mut ProjectObject,
project_dir: &Path,
target_dir: Option<&Path>,
base_dir: Option<&Path>,
mut existing_functions: Option<&mut HashSet<String>>,
) -> Result<Option<ReportUnit>> {
object.resolve_paths(project_dir, target_dir, base_dir);
match (&object.target_path, &object.base_path) {
(None, Some(_)) if object.complete != Some(true) => {
log::warn!("Skipping object without target: {}", object.name());
return Ok(None);
}
(None, None) => {
log::warn!("Skipping object without target or base: {}", object.name());
return Ok(None);
}
_ => {}
}
// println!("Checking {}", object.name());
let mut target = object
.target_path
.as_ref()
.map(|p| obj::elf::read(p).with_context(|| format!("Failed to open {}", p.display())))
.transpose()?;
let mut base = object
.base_path
.as_ref()
.map(|p| obj::elf::read(p).with_context(|| format!("Failed to open {}", p.display())))
.transpose()?;
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
diff::diff_objs(&config, target.as_mut(), base.as_mut())?;
let mut unit = ReportUnit { name: object.name().to_string(), ..Default::default() };
let obj = target.as_ref().or(base.as_ref()).unwrap();
for section in &obj.sections {
if section.kind != ObjSectionKind::Code {
continue;
}
for symbol in &section.symbols {
if symbol.size == 0 {
continue;
}
if let Some(existing_functions) = &mut existing_functions {
if (symbol.flags.0.contains(ObjSymbolFlags::Global)
|| symbol.flags.0.contains(ObjSymbolFlags::Weak))
&& !existing_functions.insert(symbol.name.clone())
{
continue;
}
}
let match_percent = symbol.match_percent.unwrap_or(if object.complete == Some(true) {
100.0
} else {
0.0
});
unit.match_percent += match_percent * symbol.size as f32;
unit.total_size += symbol.size;
if match_percent == 100.0 {
unit.matched_size += symbol.size;
}
unit.functions.push(ReportFunction {
name: symbol.name.clone(),
demangled_name: symbol.demangled_name.clone(),
size: symbol.size,
match_percent,
});
if match_percent == 100.0 {
unit.matched_functions += 1;
}
unit.total_functions += 1;
}
}
if unit.total_size == 0 {
unit.match_percent = 100.0;
} else {
unit.match_percent /= unit.total_size as f32;
}
Ok(Some(unit))
}