From 39a13f4d36291237f0c71c16b164c533a1fc4acb Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 28 Feb 2024 21:44:53 -0700 Subject: [PATCH] objdiff-cli diff & report changes, support .splitmeta object section - Add `objdiff-cli report changes` for diffing two reports - Unify some click-to-highlight logic between CLI and GUI - Load .splitmeta section for extra object metadata (original virtual addr, etc) - More work on objdiff-cli diff --- Cargo.lock | 1 + objdiff-cli/Cargo.toml | 9 +- objdiff-cli/src/cmd/diff.rs | 769 ++++++++++++++----------- objdiff-cli/src/cmd/report.rs | 286 ++++++++- objdiff-cli/src/util/term.rs | 7 +- objdiff-core/src/diff/display.rs | 38 ++ objdiff-core/src/obj/elf.rs | 85 ++- objdiff-core/src/obj/mod.rs | 11 +- objdiff-core/src/obj/split_meta.rs | 169 ++++++ objdiff-gui/src/views/function_diff.rs | 37 +- objdiff-gui/src/views/symbol_diff.rs | 12 + 11 files changed, 1018 insertions(+), 406 deletions(-) create mode 100644 objdiff-core/src/obj/split_meta.rs diff --git a/Cargo.lock b/Cargo.lock index 086649b..4686c96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2977,6 +2977,7 @@ dependencies = [ "serde", "serde_json", "supports-color", + "time", "tracing", "tracing-attributes", "tracing-subscriber", diff --git a/objdiff-cli/Cargo.toml b/objdiff-cli/Cargo.toml index 9c1a5b7..08b2fde 100644 --- a/objdiff-cli/Cargo.toml +++ b/objdiff-cli/Cargo.toml @@ -14,16 +14,17 @@ publish = false build = "build.rs" [dependencies] -crossterm = "0.27.0" anyhow = "1.0.80" argp = "0.3.0" +crossterm = "0.27.0" enable-ansi-support = "0.2.1" log = "0.4.20" objdiff-core = { path = "../objdiff-core", features = ["all"] } +rayon = "1.8.1" +serde = { version = "1", features = ["derive"] } +serde_json = "1.0.111" supports-color = "3.0.0" +time = { version = "0.3.31", features = ["formatting", "local-offset"] } tracing = "0.1.40" tracing-attributes = "0.1.27" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1.0.111" -rayon = "1.8.1" diff --git a/objdiff-cli/src/cmd/diff.rs b/objdiff-cli/src/cmd/diff.rs index e9cb27c..fbb507d 100644 --- a/objdiff-cli/src/cmd/diff.rs +++ b/objdiff-cli/src/cmd/diff.rs @@ -20,10 +20,11 @@ use crossterm::{ }; use event::KeyModifiers; use objdiff_core::{ + config::ProjectConfig, diff, - diff::display::{display_diff, DiffText}, + diff::display::{display_diff, DiffText, HighlightKind}, obj, - obj::{ObjInfo, ObjInsArgValue, ObjInsDiffKind, ObjSection, ObjSectionKind, ObjSymbol}, + obj::{ObjInfo, ObjInsDiffKind, ObjSectionKind, ObjSymbol}, }; use crate::util::term::crossterm_panic_handler; @@ -32,33 +33,71 @@ use crate::util::term::crossterm_panic_handler; /// Diff two object files. #[argp(subcommand, name = "diff")] pub struct Args { - #[argp(positional)] + #[argp(option, short = '1')] /// Target object file - target: PathBuf, - #[argp(positional)] + target: Option, + #[argp(option, short = '2')] /// Base object file - base: PathBuf, + base: Option, + #[argp(option, short = 'p')] + /// Project directory + project: Option, + #[argp(option, short = 'u')] + /// Unit name within project + unit: Option, #[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), + let (target_path, base_path, project_config) = + match (&args.target, &args.base, &args.project, &args.unit) { + (Some(t), Some(b), _, _) => (Some(t.clone()), Some(b.clone()), None), + (_, _, Some(p), Some(u)) => { + let Some((project_config, project_config_info)) = + objdiff_core::config::try_project_config(p) + else { + bail!("Project config not found in {}", p.display()) + }; + let mut project_config = project_config.with_context(|| { + format!("Reading project config {}", project_config_info.path.display()) + })?; + let Some(object) = project_config.objects.iter_mut().find(|obj| obj.name() == u) + else { + bail!("Unit not found: {}", u) + }; + object.resolve_paths( + p, + project_config.target_dir.as_deref(), + project_config.base_dir.as_deref(), + ); + let target_path = object.target_path.clone(); + let base_path = object.base_path.clone(); + (target_path, base_path, Some(project_config)) + } + _ => bail!("Either target and base or project and unit must be specified"), + }; + let mut state = FunctionDiffUi { + clear: true, + redraw: true, + size: (0, 0), + click_xy: None, + left_highlight: HighlightKind::None, + right_highlight: HighlightKind::None, + skip: 0, + y_offset: 2, + per_page: 0, + max_len: 0, + symbol_name: args.symbol.clone(), + target_path, + base_path, + project_config, + left_sym: None, + right_sym: None, + reload_time: time::OffsetDateTime::now_local()?, }; + state.reload()?; crossterm_panic_handler(); enable_raw_mode()?; @@ -69,175 +108,26 @@ pub fn run(args: Args) -> Result<()> { Hide, EnableMouseCapture, )?; + state.size = terminal_size()?; - let mut clear = true; - let mut redraw = true; - let mut skip = 0; - let mut click_xy = None; - let mut highlight = HighlightKind::None; - let (mut sx, mut sy) = terminal_size()?; loop { - let y_offset = 2; - let per_page = sy as usize - y_offset; - if redraw { - let mut w = stdout().lock(); - if clear { - crossterm::queue!(w, Clear(ClearType::All))?; - } - crossterm::queue!( - w, - 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; - } - let mut new_highlight = None; - if let Some((_, symbol)) = left_sym { - let h = print_sym( - &mut w, - symbol, - 0, - y_offset as u16, - sx / 2 - 1, - sy, - skip, - &mut highlight, - click_xy, - )?; - if let Some(h) = h { - new_highlight = Some(h); + let reload = loop { + if state.redraw { + state.draw()?; + if state.redraw { + continue; } } - if let Some((_, symbol)) = right_sym { - let h = print_sym( - &mut w, - symbol, - sx / 2, - y_offset as u16, - sx, - sy, - skip, - &mut highlight, - click_xy, - )?; - if let Some(h) = h { - new_highlight = Some(h); - } + match state.handle_event(event::read()?) { + FunctionDiffResult::Break => break false, + FunctionDiffResult::Continue => {} + FunctionDiffResult::Reload => break true, } - w.flush()?; - if let Some(new_highlight) = new_highlight { - highlight = new_highlight; - redraw = true; - click_xy = None; - clear = false; - continue; // Redraw now - } else { - redraw = false; - click_xy = None; - clear = true; - } - } - - 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; - } - KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::CONTROL) => { - skip += per_page; - redraw = true; - } - KeyCode::Char('b') if event.modifiers.contains(KeyModifiers::CONTROL) => { - skip = skip.saturating_sub(per_page); - redraw = true; - } - KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => { - skip += per_page / 2; - redraw = true; - } - KeyCode::Char('u') if event.modifiers.contains(KeyModifiers::CONTROL) => { - skip = skip.saturating_sub(per_page / 2); - 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; - } - MouseEventKind::Down(MouseButton::Left) => { - click_xy = Some((event.column, event.row)); - redraw = true; - } - _ => {} - }, - Event::Resize(x, y) => { - sx = x; - sy = y; - redraw = true; - } - _ => {} + }; + if reload { + state.reload()?; + } else { + break; } } @@ -247,148 +137,384 @@ pub fn run(args: Args) -> Result<()> { Ok(()) } -fn find_function<'a>(obj: &'a ObjInfo, name: &str) -> Option<(&'a ObjSection, &'a ObjSymbol)> { +fn find_function(obj: &ObjInfo, name: &str) -> Option { for section in &obj.sections { if section.kind != ObjSectionKind::Code { continue; } for symbol in §ion.symbols { if symbol.name == name { - return Some((section, symbol)); + return Some(symbol.clone()); } } } None } -#[allow(clippy::too_many_arguments)] -fn print_sym( - w: &mut W, - symbol: &ObjSymbol, - sx: u16, - mut sy: u16, - max_sx: u16, - max_sy: u16, - skip: usize, - highlight: &mut HighlightKind, +#[allow(dead_code)] +struct FunctionDiffUi { + clear: bool, + redraw: bool, + size: (u16, u16), click_xy: Option<(u16, u16)>, -) -> Result> -where - W: Write, -{ - let base_addr = symbol.address as u32; - let mut new_highlight = None; - 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)))?; + left_highlight: HighlightKind, + right_highlight: HighlightKind, + skip: usize, + y_offset: usize, + per_page: usize, + max_len: usize, + symbol_name: String, + target_path: Option, + base_path: Option, + project_config: Option, + left_sym: Option, + right_sym: Option, + reload_time: time::OffsetDateTime, +} + +enum FunctionDiffResult { + Break, + Continue, + Reload, +} + +impl FunctionDiffUi { + fn draw(&mut self) -> Result<()> { + let mut w = stdout().lock(); + if self.clear { + crossterm::queue!(w, Clear(ClearType::All))?; + } + let format = time::format_description::parse("[hour]:[minute]:[second]").unwrap(); + let reload_time = self.reload_time.format(&format).unwrap(); + crossterm::queue!( + w, + MoveTo(0, 0), + PrintStyledContent(self.symbol_name.clone().with(Color::White)), + MoveTo(0, 1), + PrintStyledContent(" ".repeat(self.size.0 as usize).underlined()), + MoveTo(0, 1), + PrintStyledContent("TARGET ".underlined()), + MoveTo(self.size.0 / 2, 0), + PrintStyledContent(format!("Last reload: {}", reload_time).with(Color::White)), + MoveTo(self.size.0 / 2, 1), + PrintStyledContent("BASE ".underlined()), + )?; + if let Some(percent) = self.right_sym.as_ref().and_then(|s| s.match_percent) { + crossterm::queue!( + w, + PrintStyledContent( + format!("{:.2}%", percent).with(match_percent_color(percent)).underlined() + ) + )?; + } + + self.per_page = self.size.1 as usize - self.y_offset; + let max_skip = self.max_len.saturating_sub(self.per_page); + if self.skip > max_skip { + self.skip = max_skip; + } + let mut left_highlight = None; + if let Some(symbol) = &self.left_sym { + let h = self.print_sym( + &mut w, + symbol, + (0, self.y_offset as u16), + (self.size.0 / 2 - 1, self.size.1), + &self.left_highlight, + )?; + if let Some(h) = h { + left_highlight = Some(h); + } + } + let mut right_highlight = None; + if let Some(symbol) = &self.right_sym { + let h = self.print_sym( + &mut w, + symbol, + (self.size.0 / 2, self.y_offset as u16), + self.size, + &self.right_highlight, + )?; + if let Some(h) = h { + right_highlight = Some(h); + } + } + w.flush()?; + if let Some(new_highlight) = left_highlight { + if new_highlight == self.left_highlight { + if self.left_highlight != self.right_highlight { + self.right_highlight = self.left_highlight.clone(); + } else { + self.left_highlight = HighlightKind::None; + self.right_highlight = HighlightKind::None; + } + } else { + self.left_highlight = new_highlight; + } + self.redraw = true; + self.click_xy = None; + self.clear = false; + } else if let Some(new_highlight) = right_highlight { + if new_highlight == self.right_highlight { + if self.left_highlight != self.right_highlight { + self.left_highlight = self.right_highlight.clone(); + } else { + self.left_highlight = HighlightKind::None; + self.right_highlight = HighlightKind::None; + } + } else { + self.right_highlight = new_highlight; + } + self.redraw = true; + self.click_xy = None; + self.clear = false; } else { - crossterm::queue!(w, MoveTo(sx, sy))?; - } - display_diff(ins_diff, base_addr, |text| -> Result<()> { - 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; - let mut highlight_kind = HighlightKind::None; - 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; - highlight_kind = HighlightKind::Address(addr); - } - DiffText::Opcode(mnemonic, op) => { - label_text = mnemonic.to_string(); - if ins_diff.kind == ObjInsDiffKind::OpMismatch { - base_color = Color::Blue; - } - pad_to = 8; - highlight_kind = HighlightKind::Opcode(op); - } - DiffText::Argument(arg, diff) => { - label_text = arg.to_string(); - if let Some(diff) = diff { - base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()] - } - highlight_kind = HighlightKind::Arg(arg.clone()); - } - DiffText::BranchTarget(addr) => { - label_text = format!("{addr:x}"); - highlight_kind = HighlightKind::Address(addr); - } - DiffText::Symbol(sym) => { - let name = sym.demangled_name.as_ref().unwrap_or(&sym.name); - label_text = name.clone(); - base_color = Color::White; - highlight_kind = HighlightKind::Symbol(name.clone()); - } - 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(()); - } - let highlighted = highlight == &highlight_kind; - if let Some((cx, cy)) = click_xy { - if cx >= sx && cx < sx + len as u16 && cy == sy { - if highlighted { - new_highlight = Some(HighlightKind::None); - } else { - new_highlight = Some(highlight_kind); - } - } - } - label_text.truncate(max_sx as usize - sx as usize); - let mut content = label_text.with(base_color); - if highlighted { - content = content.on_dark_grey(); - } - crossterm::queue!(w, PrintStyledContent(content))?; - 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; + self.redraw = false; + self.click_xy = None; + self.clear = true; } + Ok(()) + } + + fn handle_event(&mut self, event: Event) -> FunctionDiffResult { + match event { + Event::Key(event) + if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) => + { + match event.code { + // Quit + KeyCode::Esc | KeyCode::Char('q') => return FunctionDiffResult::Break, + // Page up + KeyCode::PageUp => { + self.skip = self.skip.saturating_sub(self.per_page); + self.redraw = true; + } + // Page up (shift + space) + KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => { + self.skip = self.skip.saturating_sub(self.per_page); + self.redraw = true; + } + // Page down + KeyCode::Char(' ') | KeyCode::PageDown => { + self.skip += self.per_page; + self.redraw = true; + } + // Page down (ctrl + f) + KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::CONTROL) => { + self.skip += self.per_page; + self.redraw = true; + } + // Page up (ctrl + b) + KeyCode::Char('b') if event.modifiers.contains(KeyModifiers::CONTROL) => { + self.skip = self.skip.saturating_sub(self.per_page); + self.redraw = true; + } + // Half page down (ctrl + d) + KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => { + self.skip += self.per_page / 2; + self.redraw = true; + } + // Half page up (ctrl + u) + KeyCode::Char('u') if event.modifiers.contains(KeyModifiers::CONTROL) => { + self.skip = self.skip.saturating_sub(self.per_page / 2); + self.redraw = true; + } + // Scroll down + KeyCode::Down | KeyCode::Char('j') => { + self.skip += 1; + self.redraw = true; + } + // Scroll up + KeyCode::Up | KeyCode::Char('k') => { + self.skip = self.skip.saturating_sub(1); + self.redraw = true; + } + // Scroll to start + KeyCode::Char('g') => { + self.skip = 0; + self.redraw = true; + } + // Scroll to end + KeyCode::Char('G') => { + self.skip = self.max_len; + self.redraw = true; + } + // Reload + KeyCode::Char('r') => { + self.redraw = true; + return FunctionDiffResult::Reload; + } + _ => {} + } + } + Event::Mouse(event) => match event.kind { + MouseEventKind::ScrollDown => { + self.skip += 3; + self.redraw = true; + } + MouseEventKind::ScrollUp => { + self.skip = self.skip.saturating_sub(3); + self.redraw = true; + } + MouseEventKind::Down(MouseButton::Left) => { + self.click_xy = Some((event.column, event.row)); + self.redraw = true; + } + _ => {} + }, + Event::Resize(x, y) => { + self.size = (x, y); + self.redraw = true; + } + _ => {} + } + FunctionDiffResult::Continue + } + + fn print_sym( + &self, + w: &mut W, + symbol: &ObjSymbol, + origin: (u16, u16), + max: (u16, u16), + highlight: &HighlightKind, + ) -> Result> + where + W: Write, + { + let base_addr = symbol.address as u32; + let mut new_highlight = None; + let mut sy = origin.1; + for ins_diff in symbol.instructions.iter().skip(self.skip) { + let mut sx = origin.0; + 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| -> Result<()> { + 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.0 { + return Ok(()); + } + let highlighted = *highlight == text; + if let Some((cx, cy)) = self.click_xy { + if cx >= sx && cx < sx + len as u16 && cy == sy { + new_highlight = Some(text.into()); + } + } + label_text.truncate(max.0 as usize - sx as usize); + let mut content = label_text.with(base_color); + if highlighted { + content = content.on_dark_grey(); + } + crossterm::queue!(w, PrintStyledContent(content))?; + 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.1 { + break; + } + } + Ok(new_highlight) + } + + fn reload(&mut self) -> Result<()> { + let mut target = self + .target_path + .as_deref() + .map(|p| obj::elf::read(p).with_context(|| format!("Loading {}", p.display()))) + .transpose()?; + let mut base = self + .base_path + .as_deref() + .map(|p| obj::elf::read(p).with_context(|| format!("Loading {}", p.display()))) + .transpose()?; + let config = diff::DiffObjConfig::default(); + diff::diff_objs(&config, target.as_mut(), base.as_mut())?; + + let left_sym = target.as_ref().and_then(|o| find_function(o, &self.symbol_name)); + let right_sym = base.as_ref().and_then(|o| find_function(o, &self.symbol_name)); + self.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: {}", self.symbol_name), + }; + self.left_sym = left_sym; + self.right_sym = right_sym; + self.reload_time = time::OffsetDateTime::now_local()?; + Ok(()) } - Ok(new_highlight) } pub const COLOR_ROTATION: [Color; 8] = [ @@ -411,26 +537,3 @@ pub fn match_percent_color(match_percent: f32) -> Color { Color::Red } } - -#[derive(Default)] -pub enum HighlightKind { - #[default] - None, - Opcode(u8), - Arg(ObjInsArgValue), - Symbol(String), - Address(u32), -} - -impl PartialEq for HighlightKind { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (HighlightKind::None, HighlightKind::None) => false, - (HighlightKind::Opcode(a), HighlightKind::Opcode(b)) => a == b, - (HighlightKind::Arg(a), HighlightKind::Arg(b)) => a.loose_eq(b), - (HighlightKind::Symbol(a), HighlightKind::Symbol(b)) => a == b, - (HighlightKind::Address(a), HighlightKind::Address(b)) => a == b, - _ => false, - } - } -} diff --git a/objdiff-cli/src/cmd/report.rs b/objdiff-cli/src/cmd/report.rs index f38c5ae..d806ee8 100644 --- a/objdiff-cli/src/cmd/report.rs +++ b/objdiff-cli/src/cmd/report.rs @@ -1,7 +1,7 @@ use std::{ collections::HashSet, fs::File, - io::{BufWriter, Write}, + io::{BufReader, BufWriter, Write}, path::{Path, PathBuf}, time::Instant, }; @@ -16,9 +16,24 @@ use objdiff_core::{ use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; #[derive(FromArgs, PartialEq, Debug)] -/// Generate a report from a project. +/// Commands for processing NVIDIA Shield TV alf files. #[argp(subcommand, name = "report")] pub struct Args { + #[argp(subcommand)] + command: SubCommand, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argp(subcommand)] +pub enum SubCommand { + Generate(GenerateArgs), + Changes(ChangesArgs), +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Generate a report from a project. +#[argp(subcommand, name = "generate")] +pub struct GenerateArgs { #[argp(option, short = 'p')] /// Project directory project: Option, @@ -30,7 +45,22 @@ pub struct Args { deduplicate: bool, } -#[derive(Debug, Clone, Default, serde::Serialize)] +#[derive(FromArgs, PartialEq, Debug)] +/// List any changes from a previous report. +#[argp(subcommand, name = "changes")] +pub struct ChangesArgs { + #[argp(positional)] + /// Previous report JSON file + previous: PathBuf, + #[argp(positional)] + /// Current report JSON file + current: PathBuf, + #[argp(option, short = 'o')] + /// Output JSON file + output: Option, +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] struct Report { fuzzy_match_percent: f32, total_size: u64, @@ -42,29 +72,46 @@ struct Report { units: Vec, } -#[derive(Debug, Clone, Default, serde::Serialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] struct ReportUnit { name: String, - match_percent: f32, + fuzzy_match_percent: f32, total_size: u64, matched_size: u64, total_functions: u32, matched_functions: u32, #[serde(skip_serializing_if = "Option::is_none")] complete: Option, + #[serde(skip_serializing_if = "Option::is_none")] + module_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + module_id: Option, functions: Vec, } -#[derive(Debug, Clone, Default, serde::Serialize)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] struct ReportFunction { name: String, #[serde(skip_serializing_if = "Option::is_none")] demangled_name: Option, + #[serde( + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_hex", + deserialize_with = "deserialize_hex" + )] + address: Option, size: u64, - match_percent: f32, + fuzzy_match_percent: f32, } pub fn run(args: Args) -> Result<()> { + match args.command { + SubCommand::Generate(args) => generate(args), + SubCommand::Changes(args) => changes(args), + } +} + +fn generate(args: GenerateArgs) -> Result<()> { let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new(".")); log::info!("Loading project {}", project_dir.display()); @@ -111,7 +158,7 @@ pub fn run(args: Args) -> Result<()> { report.units = units.into_iter().flatten().collect(); } for unit in &report.units { - report.fuzzy_match_percent += unit.match_percent * unit.total_size as f32; + report.fuzzy_match_percent += unit.fuzzy_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; @@ -180,7 +227,16 @@ fn report_object( .transpose()?; let config = diff::DiffObjConfig { relax_reloc_diffs: true }; diff::diff_objs(&config, target.as_mut(), base.as_mut())?; - let mut unit = ReportUnit { name: object.name().to_string(), ..Default::default() }; + let mut unit = ReportUnit { + name: object.name().to_string(), + complete: object.complete, + module_name: target + .as_ref() + .and_then(|o| o.split_meta.as_ref()) + .and_then(|m| m.module_name.clone()), + module_id: target.as_ref().and_then(|o| o.split_meta.as_ref()).and_then(|m| m.module_id), + ..Default::default() + }; let obj = target.as_ref().or(base.as_ref()).unwrap(); for section in &obj.sections { if section.kind != ObjSectionKind::Code { @@ -207,7 +263,7 @@ fn report_object( 0.0 } }); - unit.match_percent += match_percent * symbol.size as f32; + unit.fuzzy_match_percent += match_percent * symbol.size as f32; unit.total_size += symbol.size; if match_percent == 100.0 { unit.matched_size += symbol.size; @@ -216,7 +272,8 @@ fn report_object( name: symbol.name.clone(), demangled_name: symbol.demangled_name.clone(), size: symbol.size, - match_percent, + fuzzy_match_percent: match_percent, + address: symbol.virtual_address, }); if match_percent == 100.0 { unit.matched_functions += 1; @@ -225,9 +282,212 @@ fn report_object( } } if unit.total_size == 0 { - unit.match_percent = 100.0; + unit.fuzzy_match_percent = 100.0; } else { - unit.match_percent /= unit.total_size as f32; + unit.fuzzy_match_percent /= unit.total_size as f32; } Ok(Some(unit)) } + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +struct Changes { + from: ChangeInfo, + to: ChangeInfo, + units: Vec, +} + +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] +struct ChangeInfo { + fuzzy_match_percent: f32, + total_size: u64, + matched_size: u64, + matched_size_percent: f32, + total_functions: u32, + matched_functions: u32, + matched_functions_percent: f32, +} + +impl From<&Report> for ChangeInfo { + fn from(report: &Report) -> Self { + Self { + fuzzy_match_percent: report.fuzzy_match_percent, + total_size: report.total_size, + matched_size: report.matched_size, + matched_size_percent: report.matched_size_percent, + total_functions: report.total_functions, + matched_functions: report.matched_functions, + matched_functions_percent: report.matched_functions_percent, + } + } +} + +impl From<&ReportUnit> for ChangeInfo { + fn from(value: &ReportUnit) -> Self { + Self { + fuzzy_match_percent: value.fuzzy_match_percent, + total_size: value.total_size, + matched_size: value.matched_size, + matched_size_percent: if value.total_size == 0 { + 100.0 + } else { + value.matched_size as f32 / value.total_size as f32 * 100.0 + }, + total_functions: value.total_functions, + matched_functions: value.matched_functions, + matched_functions_percent: if value.total_functions == 0 { + 100.0 + } else { + value.matched_functions as f32 / value.total_functions as f32 * 100.0 + }, + } + } +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +struct ChangeUnit { + name: String, + from: Option, + to: Option, + functions: Vec, +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +struct ChangeFunction { + name: String, + from: Option, + to: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] +struct ChangeFunctionInfo { + fuzzy_match_percent: f32, + size: u64, +} + +impl From<&ReportFunction> for ChangeFunctionInfo { + fn from(value: &ReportFunction) -> Self { + Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size } + } +} + +fn changes(args: ChangesArgs) -> Result<()> { + let previous = read_report(&args.previous)?; + let current = read_report(&args.current)?; + let mut changes = Changes { + from: ChangeInfo::from(&previous), + to: ChangeInfo::from(¤t), + units: vec![], + }; + for prev_unit in &previous.units { + let prev_unit_info = ChangeInfo::from(prev_unit); + let curr_unit = current.units.iter().find(|u| u.name == prev_unit.name); + let curr_unit_info = curr_unit.map(ChangeInfo::from); + let mut functions = vec![]; + if let Some(curr_unit) = curr_unit { + for prev_func in &prev_unit.functions { + let prev_func_info = ChangeFunctionInfo::from(prev_func); + let curr_func = curr_unit.functions.iter().find(|f| f.name == prev_func.name); + let curr_func_info = curr_func.map(ChangeFunctionInfo::from); + if let Some(curr_func_info) = curr_func_info { + if prev_func_info != curr_func_info { + functions.push(ChangeFunction { + name: prev_func.name.clone(), + from: Some(prev_func_info), + to: Some(curr_func_info), + }); + } + } else { + functions.push(ChangeFunction { + name: prev_func.name.clone(), + from: Some(prev_func_info), + to: None, + }); + } + } + for curr_func in &curr_unit.functions { + if !prev_unit.functions.iter().any(|f| f.name == curr_func.name) { + functions.push(ChangeFunction { + name: curr_func.name.clone(), + from: None, + to: Some(ChangeFunctionInfo::from(curr_func)), + }); + } + } + } else { + for prev_func in &prev_unit.functions { + functions.push(ChangeFunction { + name: prev_func.name.clone(), + from: Some(ChangeFunctionInfo::from(prev_func)), + to: None, + }); + } + } + if !functions.is_empty() || !matches!(&curr_unit_info, Some(v) if v == &prev_unit_info) { + changes.units.push(ChangeUnit { + name: prev_unit.name.clone(), + from: Some(prev_unit_info), + to: curr_unit_info, + functions, + }); + } + } + for curr_unit in ¤t.units { + if !previous.units.iter().any(|u| u.name == curr_unit.name) { + changes.units.push(ChangeUnit { + name: curr_unit.name.clone(), + from: None, + to: Some(ChangeInfo::from(curr_unit)), + functions: curr_unit + .functions + .iter() + .map(|f| ChangeFunction { + name: f.name.clone(), + from: None, + to: Some(ChangeFunctionInfo::from(f)), + }) + .collect(), + }); + } + } + 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, &changes)?; + output.flush()?; + } else { + serde_json::to_writer_pretty(std::io::stdout(), &changes)?; + } + Ok(()) +} + +fn read_report(path: &Path) -> Result { + serde_json::from_reader(BufReader::new( + File::open(path).with_context(|| format!("Failed to open {}", path.display()))?, + )) + .with_context(|| format!("Failed to read report {}", path.display())) +} + +fn serialize_hex(x: &Option, s: S) -> Result +where S: serde::Serializer { + if let Some(x) = x { + s.serialize_str(&format!("{:#x}", x)) + } else { + s.serialize_none() + } +} + +fn deserialize_hex<'de, D>(d: D) -> Result, D::Error> +where D: serde::Deserializer<'de> { + use serde::Deserialize; + let s = String::deserialize(d)?; + if s.is_empty() { + Ok(None) + } else if !s.starts_with("0x") { + Err(serde::de::Error::custom("expected hex string")) + } else { + u64::from_str_radix(&s[2..], 16).map(Some).map_err(serde::de::Error::custom) + } +} diff --git a/objdiff-cli/src/util/term.rs b/objdiff-cli/src/util/term.rs index 8dc6344..4047952 100644 --- a/objdiff-cli/src/util/term.rs +++ b/objdiff-cli/src/util/term.rs @@ -3,13 +3,12 @@ use std::panic; pub fn crossterm_panic_handler() { let original_hook = panic::take_hook(); panic::set_hook(Box::new(move |panic_info| { - crossterm::execute!( + let _ = crossterm::execute!( std::io::stderr(), crossterm::terminal::LeaveAlternateScreen, crossterm::event::DisableMouseCapture - ) - .unwrap(); - crossterm::terminal::disable_raw_mode().unwrap(); + ); + let _ = crossterm::terminal::disable_raw_mode(); original_hook(panic_info); })); } diff --git a/objdiff-core/src/diff/display.rs b/objdiff-core/src/diff/display.rs index 5c4abd8..2d338ab 100644 --- a/objdiff-core/src/diff/display.rs +++ b/objdiff-core/src/diff/display.rs @@ -28,6 +28,16 @@ pub enum DiffText<'a> { Eol, } +#[derive(Default, Clone, PartialEq, Eq)] +pub enum HighlightKind { + #[default] + None, + Opcode(u8), + Arg(ObjInsArgValue), + Symbol(String), + Address(u32), +} + pub fn display_diff( ins_diff: &ObjInsDiff, base_addr: u32, @@ -177,3 +187,31 @@ fn display_reloc( } Ok(()) } + +impl PartialEq> for HighlightKind { + fn eq(&self, other: &DiffText) -> bool { + match (self, other) { + (HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b, + (HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b), + (HighlightKind::Symbol(a), DiffText::Symbol(b)) => a == &b.name, + (HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchTarget(b)) => a == b, + _ => false, + } + } +} + +impl PartialEq for DiffText<'_> { + fn eq(&self, other: &HighlightKind) -> bool { other.eq(self) } +} + +impl From> for HighlightKind { + fn from(value: DiffText<'_>) -> Self { + match value { + DiffText::Opcode(_, op) => HighlightKind::Opcode(op), + DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()), + DiffText::Symbol(sym) => HighlightKind::Symbol(sym.name.to_string()), + DiffText::Address(addr) | DiffText::BranchTarget(addr) => HighlightKind::Address(addr), + _ => HighlightKind::None, + } + } +} diff --git a/objdiff-core/src/obj/elf.rs b/objdiff-core/src/obj/elf.rs index c075723..57ff70c 100644 --- a/objdiff-core/src/obj/elf.rs +++ b/objdiff-core/src/obj/elf.rs @@ -1,15 +1,16 @@ -use std::{borrow::Cow, collections::BTreeMap, fs, io::Cursor, path::Path}; +use std::{collections::BTreeMap, fs, io::Cursor, path::Path}; use anyhow::{anyhow, bail, ensure, Context, Result}; use byteorder::{BigEndian, ReadBytesExt}; use filetime::FileTime; use flagset::Flags; use object::{ - elf, Architecture, Endianness, File, Object, ObjectSection, ObjectSymbol, RelocationKind, - RelocationTarget, SectionIndex, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection, + elf, Architecture, File, Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget, + SectionIndex, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection, }; use crate::obj::{ + split_meta::{SplitMeta, SPLITMETA_SECTION}, ObjArchitecture, ObjInfo, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, }; @@ -23,7 +24,12 @@ fn to_obj_section_kind(kind: SectionKind) -> Option { } } -fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> Result { +fn to_obj_symbol( + obj_file: &File<'_>, + symbol: &Symbol<'_, '_>, + addend: i64, + split_meta: Option<&SplitMeta>, +) -> Result { let mut name = symbol.name().context("Failed to process symbol name")?; if name.is_empty() { log::warn!("Found empty sym: {symbol:?}"); @@ -57,6 +63,10 @@ fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> R if obj_file.architecture() == Architecture::PowerPc { demangled_name = cwdemangle::demangle(name, &Default::default()); } + // Find the virtual address for the symbol if available + let virtual_address = split_meta + .and_then(|m| m.virtual_addresses.as_ref()) + .and_then(|v| v.get(symbol.index().0).cloned()); Ok(ObjSymbol { name: name.to_string(), demangled_name, @@ -66,13 +76,14 @@ fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> R size_known: symbol.size() != 0, flags, addend, + virtual_address, diff_symbol: None, instructions: vec![], match_percent: None, }) } -fn filter_sections(obj_file: &File<'_>) -> Result> { +fn filter_sections(obj_file: &File<'_>, split_meta: Option<&SplitMeta>) -> Result> { let mut result = Vec::::new(); for section in obj_file.sections() { if section.size() == 0 { @@ -83,6 +94,17 @@ fn filter_sections(obj_file: &File<'_>) -> Result> { }; let name = section.name().context("Failed to process section name")?; let data = section.uncompressed_data().context("Failed to read section data")?; + + // Find the virtual address for the section symbol if available + let section_symbol = obj_file.symbols().find(|s| { + s.kind() == SymbolKind::Section && s.section_index() == Some(section.index()) + }); + let virtual_address = section_symbol.and_then(|s| { + split_meta + .and_then(|m| m.virtual_addresses.as_ref()) + .and_then(|v| v.get(s.index().0).cloned()) + }); + result.push(ObjSection { name: name.to_string(), kind, @@ -92,6 +114,7 @@ fn filter_sections(obj_file: &File<'_>) -> Result> { index: section.index().0, symbols: Vec::new(), relocations: Vec::new(), + virtual_address, data_diff: vec![], match_percent: 0.0, }); @@ -100,7 +123,11 @@ fn filter_sections(obj_file: &File<'_>) -> Result> { Ok(result) } -fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result> { +fn symbols_by_section( + obj_file: &File<'_>, + section: &ObjSection, + split_meta: Option<&SplitMeta>, +) -> Result> { let mut result = Vec::::new(); for symbol in obj_file.symbols() { if symbol.kind() == SymbolKind::Section { @@ -115,7 +142,7 @@ fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result, section: &ObjSection) -> Result) -> Result> { +fn common_symbols(obj_file: &File<'_>, split_meta: Option<&SplitMeta>) -> Result> { obj_file .symbols() .filter(Symbol::is_common) - .map(|symbol| to_obj_symbol(obj_file, &symbol, 0)) + .map(|symbol| to_obj_symbol(obj_file, &symbol, 0, split_meta)) .collect::>>() } @@ -145,6 +172,7 @@ fn find_section_symbol( obj_file: &File<'_>, target: &Symbol<'_, '_>, address: u64, + split_meta: Option<&SplitMeta>, ) -> Result { let section_index = target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?; @@ -164,7 +192,7 @@ fn find_section_symbol( } continue; } - return to_obj_symbol(obj_file, &symbol, 0); + return to_obj_symbol(obj_file, &symbol, 0, split_meta); } let (name, offset) = closest_symbol .and_then(|s| s.name().map(|n| (n, s.address())).ok()) @@ -180,6 +208,7 @@ fn find_section_symbol( size_known: false, flags: Default::default(), addend: offset_addr as i64, + virtual_address: None, diff_symbol: None, instructions: vec![], match_percent: None, @@ -190,6 +219,7 @@ fn relocations_by_section( arch: ObjArchitecture, obj_file: &File<'_>, section: &ObjSection, + split_meta: Option<&SplitMeta>, ) -> Result> { let obj_section = obj_file.section_by_index(SectionIndex(section.index))?; let mut relocations = Vec::::new(); @@ -259,11 +289,11 @@ fn relocations_by_section( // println!("Reloc: {reloc:?}, symbol: {symbol:?}, addend: {addend:#X}"); let target = match symbol.kind() { SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => { - to_obj_symbol(obj_file, &symbol, addend) + to_obj_symbol(obj_file, &symbol, addend, split_meta) } SymbolKind::Section => { ensure!(addend >= 0, "Negative addend in reloc: {addend}"); - find_section_symbol(obj_file, &symbol, addend as u64) + find_section_symbol(obj_file, &symbol, addend as u64, split_meta) } kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")), }?; @@ -298,6 +328,7 @@ fn line_info(obj_file: &File<'_>) -> Result>> { // DWARF 2+ #[cfg(feature = "dwarf")] { + use std::borrow::Cow; let dwarf_cow = gimli::Dwarf::load(|id| { Ok::<_, gimli::Error>( obj_file @@ -307,8 +338,8 @@ fn line_info(obj_file: &File<'_>) -> Result>> { ) })?; let endian = match obj_file.endianness() { - Endianness::Little => gimli::RunTimeEndian::Little, - Endianness::Big => gimli::RunTimeEndian::Big, + object::Endianness::Little => gimli::RunTimeEndian::Little, + object::Endianness::Big => gimli::RunTimeEndian::Big, }; let dwarf = dwarf_cow.borrow(|section| gimli::EndianSlice::new(section, endian)); let mut iter = dwarf.units(); @@ -344,17 +375,35 @@ pub fn read(obj_path: &Path) -> Result { Architecture::Mips => ObjArchitecture::Mips, _ => bail!("Unsupported architecture: {:?}", obj_file.architecture()), }; + let split_meta = split_meta(&obj_file)?; let mut result = ObjInfo { architecture, path: obj_path.to_owned(), timestamp, - sections: filter_sections(&obj_file)?, - common: common_symbols(&obj_file)?, + sections: filter_sections(&obj_file, split_meta.as_ref())?, + common: common_symbols(&obj_file, split_meta.as_ref())?, line_info: line_info(&obj_file)?, + split_meta: None, }; for section in &mut result.sections { - section.symbols = symbols_by_section(&obj_file, section)?; - section.relocations = relocations_by_section(architecture, &obj_file, section)?; + section.symbols = symbols_by_section(&obj_file, section, split_meta.as_ref())?; + section.relocations = + relocations_by_section(architecture, &obj_file, section, split_meta.as_ref())?; } + result.split_meta = split_meta; Ok(result) } + +fn split_meta(obj_file: &File<'_>) -> Result> { + Ok(if let Some(section) = obj_file.section_by_name(SPLITMETA_SECTION) { + if section.size() != 0 { + let data = section.uncompressed_data()?; + let mut reader = data.as_ref(); + Some(SplitMeta::from_reader(&mut reader, obj_file.endianness(), obj_file.is_64())?) + } else { + None + } + } else { + None + }) +} diff --git a/objdiff-core/src/obj/mod.rs b/objdiff-core/src/obj/mod.rs index 020e705..75c50b7 100644 --- a/objdiff-core/src/obj/mod.rs +++ b/objdiff-core/src/obj/mod.rs @@ -3,11 +3,13 @@ pub mod elf; pub mod mips; #[cfg(feature = "ppc")] pub mod ppc; +pub mod split_meta; use std::{collections::BTreeMap, fmt, path::PathBuf}; use filetime::FileTime; use flagset::{flags, FlagSet}; +use split_meta::SplitMeta; use crate::util::ReallySigned; @@ -39,6 +41,7 @@ pub struct ObjSection { pub index: usize, pub symbols: Vec, pub relocations: Vec, + pub virtual_address: Option, // Diff pub data_diff: Vec, @@ -139,7 +142,7 @@ pub struct ObjIns { pub args: Vec, pub reloc: Option, pub branch_dest: Option, - /// Line info + /// Line number pub line: Option, /// Original (unsimplified) instruction pub orig: Option, @@ -185,6 +188,8 @@ pub struct ObjSymbol { pub size_known: bool, pub flags: ObjSymbolFlagSet, pub addend: i64, + /// Original virtual address (from .splitmeta section) + pub virtual_address: Option, // Diff pub diff_symbol: Option, @@ -206,8 +211,12 @@ pub struct ObjInfo { pub path: PathBuf, pub timestamp: FileTime, pub sections: Vec, + /// Common BSS symbols pub common: Vec, + /// Line number info (.line or .debug_line section) pub line_info: Option>, + /// Split object metadata (.splitmeta section) + pub split_meta: Option, } #[derive(Debug, Eq, PartialEq, Copy, Clone)] diff --git a/objdiff-core/src/obj/split_meta.rs b/objdiff-core/src/obj/split_meta.rs new file mode 100644 index 0000000..c5c7b65 --- /dev/null +++ b/objdiff-core/src/obj/split_meta.rs @@ -0,0 +1,169 @@ +use std::{ + io, + io::{Read, Write}, +}; + +use object::{elf::SHT_LOUSER, Endian}; + +pub const SPLITMETA_SECTION: &str = ".splitmeta"; +// Use the same section type as .mwcats.* so the linker ignores it +pub const SHT_SPLITMETA: u32 = SHT_LOUSER + 0x4A2A82C2; + +/// This is used to store metadata about the source of an object file, +/// such as the original virtual addresses and the tool that wrote it. +#[derive(Debug, Default, Clone)] +pub struct SplitMeta { + /// The tool that generated the object. Informational only. + pub generator: Option, + /// The name of the source module. (e.g. the DOL or REL name) + pub module_name: Option, + /// The ID of the source module. (e.g. the DOL or REL ID) + pub module_id: Option, + /// Original virtual addresses of each symbol in the object. + /// Index 0 is the ELF null symbol. + pub virtual_addresses: Option>, +} + +/** + * .splitmeta section format: + * - Magic: "SPMD" + * - Section: Magic: 4 bytes, Data size: 4 bytes, Data: variable + * Section size can be used to skip unknown sections + * - Repeat section until EOF + * Endianness matches the object file + * + * Sections: + * - Generator: Magic: "GENR", Data size: 4 bytes, Data: UTF-8 string (no null terminator) + * - Virtual addresses: Magic: "VIRT", Data size: 4 bytes, Data: array + * Data is u32 array for 32-bit objects, u64 array for 64-bit objects + * Count is size / 4 (32-bit) or size / 8 (64-bit) + */ + +const SPLIT_META_MAGIC: [u8; 4] = *b"SPMD"; +const GENERATOR_MAGIC: [u8; 4] = *b"GENR"; +const MODULE_NAME_MAGIC: [u8; 4] = *b"MODN"; +const MODULE_ID_MAGIC: [u8; 4] = *b"MODI"; +const VIRTUAL_ADDRESS_MAGIC: [u8; 4] = *b"VIRT"; + +impl SplitMeta { + pub fn from_reader(reader: &mut R, e: E, is_64: bool) -> io::Result + where + E: Endian, + R: Read + ?Sized, + { + let mut magic = [0; 4]; + reader.read_exact(&mut magic)?; + if magic != SPLIT_META_MAGIC { + return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid split metadata magic")); + } + let mut result = SplitMeta::default(); + loop { + let mut magic = [0; 4]; + match reader.read_exact(&mut magic) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break, + Err(e) => return Err(e), + }; + let mut size_bytes = [0; 4]; + reader.read_exact(&mut size_bytes)?; + let size = e.read_u32_bytes(size_bytes); + let mut data = vec![0; size as usize]; + reader.read_exact(&mut data)?; + match magic { + GENERATOR_MAGIC => { + let string = String::from_utf8(data) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + result.generator = Some(string); + } + MODULE_NAME_MAGIC => { + let string = String::from_utf8(data) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + result.module_name = Some(string); + } + MODULE_ID_MAGIC => { + let id = e.read_u32_bytes(data.as_slice().try_into().map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "Invalid module ID size") + })?); + result.module_id = Some(id); + } + VIRTUAL_ADDRESS_MAGIC => { + let vec = if is_64 { + let mut vec = vec![0u64; data.len() / 8]; + for i in 0..vec.len() { + vec[i] = e.read_u64_bytes(data[i * 8..(i + 1) * 8].try_into().unwrap()); + } + vec + } else { + let mut vec = vec![0u64; data.len() / 4]; + for i in 0..vec.len() { + vec[i] = e.read_u32_bytes(data[i * 4..(i + 1) * 4].try_into().unwrap()) + as u64; + } + vec + }; + result.virtual_addresses = Some(vec); + } + _ => { + // Ignore unknown sections + } + } + } + Ok(result) + } + + pub fn to_writer(&self, writer: &mut W, e: E, is_64: bool) -> io::Result<()> + where + E: Endian, + W: Write + ?Sized, + { + writer.write_all(&SPLIT_META_MAGIC)?; + if let Some(generator) = &self.generator { + writer.write_all(&GENERATOR_MAGIC)?; + writer.write_all(&e.write_u32_bytes(generator.len() as u32))?; + writer.write_all(generator.as_bytes())?; + } + if let Some(module_name) = &self.module_name { + writer.write_all(&MODULE_NAME_MAGIC)?; + writer.write_all(&e.write_u32_bytes(module_name.len() as u32))?; + writer.write_all(module_name.as_bytes())?; + } + if let Some(module_id) = self.module_id { + writer.write_all(&MODULE_ID_MAGIC)?; + writer.write_all(&e.write_u32_bytes(4))?; + writer.write_all(&e.write_u32_bytes(module_id))?; + } + if let Some(virtual_addresses) = &self.virtual_addresses { + writer.write_all(&VIRTUAL_ADDRESS_MAGIC)?; + let count = virtual_addresses.len() as u32; + if is_64 { + writer.write_all(&e.write_u32_bytes(count * 8))?; + for &addr in virtual_addresses { + writer.write_all(&e.write_u64_bytes(addr))?; + } + } else { + writer.write_all(&e.write_u32_bytes(count * 4))?; + for &addr in virtual_addresses { + writer.write_all(&e.write_u32_bytes(addr as u32))?; + } + } + } + Ok(()) + } + + pub fn write_size(&self, is_64: bool) -> usize { + let mut size = 4; + if let Some(generator) = self.generator.as_deref() { + size += 8 + generator.len(); + } + if let Some(module_name) = self.module_name.as_deref() { + size += 8 + module_name.len(); + } + if self.module_id.is_some() { + size += 12; + } + if let Some(virtual_addresses) = self.virtual_addresses.as_deref() { + size += 8 + if is_64 { 8 } else { 4 } * virtual_addresses.len(); + } + size + } +} diff --git a/objdiff-gui/src/views/function_diff.rs b/objdiff-gui/src/views/function_diff.rs index 7bc7b07..3bf96bd 100644 --- a/objdiff-gui/src/views/function_diff.rs +++ b/objdiff-gui/src/views/function_diff.rs @@ -3,7 +3,7 @@ use std::default::Default; use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget}; use egui_extras::{Column, TableBuilder, TableRow}; use objdiff_core::{ - diff::display::{display_diff, DiffText}, + diff::display::{display_diff, DiffText, HighlightKind}, obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjInsDiff, ObjInsDiffKind, ObjSymbol}, }; use time::format_description; @@ -13,29 +13,6 @@ use crate::views::{ symbol_diff::{match_color_for_symbol, DiffViewState, SymbolReference, View}, }; -#[derive(Default)] -pub enum HighlightKind { - #[default] - None, - Opcode(u8), - Arg(ObjInsArgValue), - Symbol(String), - Address(u32), -} - -impl PartialEq for HighlightKind { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (HighlightKind::None, HighlightKind::None) => false, - (HighlightKind::Opcode(a), HighlightKind::Opcode(b)) => a == b, - (HighlightKind::Arg(a), HighlightKind::Arg(b)) => a.loose_eq(b), - (HighlightKind::Symbol(a), HighlightKind::Symbol(b)) => a == b, - (HighlightKind::Address(a), HighlightKind::Address(b)) => a == b, - _ => false, - } - } -} - #[derive(Default)] pub struct FunctionViewState { pub highlight: HighlightKind, @@ -159,7 +136,6 @@ fn diff_text_ui( ObjInsDiffKind::Insert => appearance.insert_color, }; let mut pad_to = 0; - let mut highlight_kind = HighlightKind::None; match text { DiffText::Basic(text) => { label_text = text.to_string(); @@ -176,32 +152,27 @@ fn diff_text_ui( DiffText::Address(addr) => { label_text = format!("{:x}:", addr); pad_to = 5; - highlight_kind = HighlightKind::Address(addr); } - DiffText::Opcode(mnemonic, op) => { + DiffText::Opcode(mnemonic, _op) => { label_text = mnemonic.to_string(); if ins_diff.kind == ObjInsDiffKind::OpMismatch { base_color = appearance.replace_color; } pad_to = 8; - highlight_kind = HighlightKind::Opcode(op); } DiffText::Argument(arg, diff) => { label_text = arg.to_string(); if let Some(diff) = diff { base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()] } - highlight_kind = HighlightKind::Arg(arg.clone()); } DiffText::BranchTarget(addr) => { label_text = format!("{addr:x}"); - highlight_kind = HighlightKind::Address(addr); } DiffText::Symbol(sym) => { let name = sym.demangled_name.as_ref().unwrap_or(&sym.name); label_text = name.clone(); base_color = appearance.emphasized_text_color; - highlight_kind = HighlightKind::Symbol(name.clone()); } DiffText::Spacing(n) => { ui.add_space(n as f32 * space_width); @@ -213,7 +184,7 @@ fn diff_text_ui( } let len = label_text.len(); - let highlight = ins_view_state.highlight == highlight_kind; + let highlight = ins_view_state.highlight == text; let response = Label::new(LayoutJob::single_section( label_text, appearance.code_text_format(base_color, highlight), @@ -225,7 +196,7 @@ fn diff_text_ui( if highlight { ins_view_state.highlight = HighlightKind::None; } else { - ins_view_state.highlight = highlight_kind; + ins_view_state.highlight = text.into(); } } if len < pad_to { diff --git a/objdiff-gui/src/views/symbol_diff.rs b/objdiff-gui/src/views/symbol_diff.rs index 94a5f44..1306f5b 100644 --- a/objdiff-gui/src/views/symbol_diff.rs +++ b/objdiff-gui/src/views/symbol_diff.rs @@ -143,6 +143,12 @@ fn symbol_context_menu_ui(ui: &mut Ui, symbol: &ObjSymbol) { ui.output_mut(|output| output.copied_text = symbol.name.clone()); ui.close_menu(); } + if let Some(address) = symbol.virtual_address { + if ui.button(format!("Copy \"{:#x}\" (virtual address)", address)).clicked() { + ui.output_mut(|output| output.copied_text = format!("{:#x}", address)); + ui.close_menu(); + } + } }); } @@ -161,6 +167,12 @@ fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol, appearance: &Appearance) { format!("Size: {:x} (assumed)", symbol.size), ); } + if let Some(address) = symbol.virtual_address { + ui.colored_label( + appearance.highlight_color, + format!("Virtual address: {:#x}", address), + ); + } }); }