mirror of https://github.com/encounter/objdiff.git
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
This commit is contained in:
parent
28348606bf
commit
39a13f4d36
|
@ -2977,6 +2977,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"supports-color",
|
"supports-color",
|
||||||
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|
|
@ -14,16 +14,17 @@ publish = false
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
crossterm = "0.27.0"
|
|
||||||
anyhow = "1.0.80"
|
anyhow = "1.0.80"
|
||||||
argp = "0.3.0"
|
argp = "0.3.0"
|
||||||
|
crossterm = "0.27.0"
|
||||||
enable-ansi-support = "0.2.1"
|
enable-ansi-support = "0.2.1"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
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"
|
supports-color = "3.0.0"
|
||||||
|
time = { version = "0.3.31", features = ["formatting", "local-offset"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-attributes = "0.1.27"
|
tracing-attributes = "0.1.27"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
serde_json = "1.0.111"
|
|
||||||
rayon = "1.8.1"
|
|
||||||
|
|
|
@ -20,10 +20,11 @@ use crossterm::{
|
||||||
};
|
};
|
||||||
use event::KeyModifiers;
|
use event::KeyModifiers;
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
|
config::ProjectConfig,
|
||||||
diff,
|
diff,
|
||||||
diff::display::{display_diff, DiffText},
|
diff::display::{display_diff, DiffText, HighlightKind},
|
||||||
obj,
|
obj,
|
||||||
obj::{ObjInfo, ObjInsArgValue, ObjInsDiffKind, ObjSection, ObjSectionKind, ObjSymbol},
|
obj::{ObjInfo, ObjInsDiffKind, ObjSectionKind, ObjSymbol},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::util::term::crossterm_panic_handler;
|
use crate::util::term::crossterm_panic_handler;
|
||||||
|
@ -32,33 +33,71 @@ use crate::util::term::crossterm_panic_handler;
|
||||||
/// Diff two object files.
|
/// Diff two object files.
|
||||||
#[argp(subcommand, name = "diff")]
|
#[argp(subcommand, name = "diff")]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
#[argp(positional)]
|
#[argp(option, short = '1')]
|
||||||
/// Target object file
|
/// Target object file
|
||||||
target: PathBuf,
|
target: Option<PathBuf>,
|
||||||
#[argp(positional)]
|
#[argp(option, short = '2')]
|
||||||
/// Base object file
|
/// Base object file
|
||||||
base: PathBuf,
|
base: Option<PathBuf>,
|
||||||
|
#[argp(option, short = 'p')]
|
||||||
|
/// Project directory
|
||||||
|
project: Option<PathBuf>,
|
||||||
|
#[argp(option, short = 'u')]
|
||||||
|
/// Unit name within project
|
||||||
|
unit: Option<String>,
|
||||||
#[argp(option, short = 's')]
|
#[argp(option, short = 's')]
|
||||||
/// Function symbol to diff
|
/// Function symbol to diff
|
||||||
symbol: String,
|
symbol: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(args: Args) -> Result<()> {
|
pub fn run(args: Args) -> Result<()> {
|
||||||
let mut target = obj::elf::read(&args.target)
|
let (target_path, base_path, project_config) =
|
||||||
.with_context(|| format!("Loading {}", args.target.display()))?;
|
match (&args.target, &args.base, &args.project, &args.unit) {
|
||||||
let mut base =
|
(Some(t), Some(b), _, _) => (Some(t.clone()), Some(b.clone()), None),
|
||||||
obj::elf::read(&args.base).with_context(|| format!("Loading {}", args.base.display()))?;
|
(_, _, Some(p), Some(u)) => {
|
||||||
let config = diff::DiffObjConfig::default();
|
let Some((project_config, project_config_info)) =
|
||||||
diff::diff_objs(&config, Some(&mut target), Some(&mut base))?;
|
objdiff_core::config::try_project_config(p)
|
||||||
|
else {
|
||||||
let left_sym = find_function(&target, &args.symbol);
|
bail!("Project config not found in {}", p.display())
|
||||||
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 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();
|
crossterm_panic_handler();
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
@ -69,175 +108,26 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
Hide,
|
Hide,
|
||||||
EnableMouseCapture,
|
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 {
|
loop {
|
||||||
let y_offset = 2;
|
let reload = loop {
|
||||||
let per_page = sy as usize - y_offset;
|
if state.redraw {
|
||||||
if redraw {
|
state.draw()?;
|
||||||
let mut w = stdout().lock();
|
if state.redraw {
|
||||||
if clear {
|
continue;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((_, symbol)) = right_sym {
|
match state.handle_event(event::read()?) {
|
||||||
let h = print_sym(
|
FunctionDiffResult::Break => break false,
|
||||||
&mut w,
|
FunctionDiffResult::Continue => {}
|
||||||
symbol,
|
FunctionDiffResult::Reload => break true,
|
||||||
sx / 2,
|
|
||||||
y_offset as u16,
|
|
||||||
sx,
|
|
||||||
sy,
|
|
||||||
skip,
|
|
||||||
&mut highlight,
|
|
||||||
click_xy,
|
|
||||||
)?;
|
|
||||||
if let Some(h) = h {
|
|
||||||
new_highlight = Some(h);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
w.flush()?;
|
if reload {
|
||||||
if let Some(new_highlight) = new_highlight {
|
state.reload()?;
|
||||||
highlight = new_highlight;
|
|
||||||
redraw = true;
|
|
||||||
click_xy = None;
|
|
||||||
clear = false;
|
|
||||||
continue; // Redraw now
|
|
||||||
} else {
|
} else {
|
||||||
redraw = false;
|
break;
|
||||||
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;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,39 +137,256 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_function<'a>(obj: &'a ObjInfo, name: &str) -> Option<(&'a ObjSection, &'a ObjSymbol)> {
|
fn find_function(obj: &ObjInfo, name: &str) -> Option<ObjSymbol> {
|
||||||
for section in &obj.sections {
|
for section in &obj.sections {
|
||||||
if section.kind != ObjSectionKind::Code {
|
if section.kind != ObjSectionKind::Code {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for symbol in §ion.symbols {
|
for symbol in §ion.symbols {
|
||||||
if symbol.name == name {
|
if symbol.name == name {
|
||||||
return Some((section, symbol));
|
return Some(symbol.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(dead_code)]
|
||||||
fn print_sym<W>(
|
struct FunctionDiffUi {
|
||||||
|
clear: bool,
|
||||||
|
redraw: bool,
|
||||||
|
size: (u16, u16),
|
||||||
|
click_xy: Option<(u16, u16)>,
|
||||||
|
left_highlight: HighlightKind,
|
||||||
|
right_highlight: HighlightKind,
|
||||||
|
skip: usize,
|
||||||
|
y_offset: usize,
|
||||||
|
per_page: usize,
|
||||||
|
max_len: usize,
|
||||||
|
symbol_name: String,
|
||||||
|
target_path: Option<PathBuf>,
|
||||||
|
base_path: Option<PathBuf>,
|
||||||
|
project_config: Option<ProjectConfig>,
|
||||||
|
left_sym: Option<ObjSymbol>,
|
||||||
|
right_sym: Option<ObjSymbol>,
|
||||||
|
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 {
|
||||||
|
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<W>(
|
||||||
|
&self,
|
||||||
w: &mut W,
|
w: &mut W,
|
||||||
symbol: &ObjSymbol,
|
symbol: &ObjSymbol,
|
||||||
sx: u16,
|
origin: (u16, u16),
|
||||||
mut sy: u16,
|
max: (u16, u16),
|
||||||
max_sx: u16,
|
highlight: &HighlightKind,
|
||||||
max_sy: u16,
|
) -> Result<Option<HighlightKind>>
|
||||||
skip: usize,
|
where
|
||||||
highlight: &mut HighlightKind,
|
|
||||||
click_xy: Option<(u16, u16)>,
|
|
||||||
) -> Result<Option<HighlightKind>>
|
|
||||||
where
|
|
||||||
W: Write,
|
W: Write,
|
||||||
{
|
{
|
||||||
let base_addr = symbol.address as u32;
|
let base_addr = symbol.address as u32;
|
||||||
let mut new_highlight = None;
|
let mut new_highlight = None;
|
||||||
for ins_diff in symbol.instructions.iter().skip(skip) {
|
let mut sy = origin.1;
|
||||||
let mut sx = sx;
|
for ins_diff in symbol.instructions.iter().skip(self.skip) {
|
||||||
|
let mut sx = origin.0;
|
||||||
if ins_diff.kind != ObjInsDiffKind::None && sx > 2 {
|
if ins_diff.kind != ObjInsDiffKind::None && sx > 2 {
|
||||||
crossterm::queue!(w, MoveTo(sx - 2, sy))?;
|
crossterm::queue!(w, MoveTo(sx - 2, sy))?;
|
||||||
let s = match ins_diff.kind {
|
let s = match ins_diff.kind {
|
||||||
|
@ -294,15 +401,14 @@ where
|
||||||
display_diff(ins_diff, base_addr, |text| -> Result<()> {
|
display_diff(ins_diff, base_addr, |text| -> Result<()> {
|
||||||
let mut label_text;
|
let mut label_text;
|
||||||
let mut base_color = match ins_diff.kind {
|
let mut base_color = match ins_diff.kind {
|
||||||
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
ObjInsDiffKind::None
|
||||||
Color::Grey
|
| ObjInsDiffKind::OpMismatch
|
||||||
}
|
| ObjInsDiffKind::ArgMismatch => Color::Grey,
|
||||||
ObjInsDiffKind::Replace => Color::DarkCyan,
|
ObjInsDiffKind::Replace => Color::DarkCyan,
|
||||||
ObjInsDiffKind::Delete => Color::DarkRed,
|
ObjInsDiffKind::Delete => Color::DarkRed,
|
||||||
ObjInsDiffKind::Insert => Color::DarkGreen,
|
ObjInsDiffKind::Insert => Color::DarkGreen,
|
||||||
};
|
};
|
||||||
let mut pad_to = 0;
|
let mut pad_to = 0;
|
||||||
let mut highlight_kind = HighlightKind::None;
|
|
||||||
match text {
|
match text {
|
||||||
DiffText::Basic(text) => {
|
DiffText::Basic(text) => {
|
||||||
label_text = text.to_string();
|
label_text = text.to_string();
|
||||||
|
@ -319,32 +425,27 @@ where
|
||||||
DiffText::Address(addr) => {
|
DiffText::Address(addr) => {
|
||||||
label_text = format!("{:x}:", addr);
|
label_text = format!("{:x}:", addr);
|
||||||
pad_to = 5;
|
pad_to = 5;
|
||||||
highlight_kind = HighlightKind::Address(addr);
|
|
||||||
}
|
}
|
||||||
DiffText::Opcode(mnemonic, op) => {
|
DiffText::Opcode(mnemonic, _op) => {
|
||||||
label_text = mnemonic.to_string();
|
label_text = mnemonic.to_string();
|
||||||
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
|
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
|
||||||
base_color = Color::Blue;
|
base_color = Color::Blue;
|
||||||
}
|
}
|
||||||
pad_to = 8;
|
pad_to = 8;
|
||||||
highlight_kind = HighlightKind::Opcode(op);
|
|
||||||
}
|
}
|
||||||
DiffText::Argument(arg, diff) => {
|
DiffText::Argument(arg, diff) => {
|
||||||
label_text = arg.to_string();
|
label_text = arg.to_string();
|
||||||
if let Some(diff) = diff {
|
if let Some(diff) = diff {
|
||||||
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
|
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
|
||||||
}
|
}
|
||||||
highlight_kind = HighlightKind::Arg(arg.clone());
|
|
||||||
}
|
}
|
||||||
DiffText::BranchTarget(addr) => {
|
DiffText::BranchTarget(addr) => {
|
||||||
label_text = format!("{addr:x}");
|
label_text = format!("{addr:x}");
|
||||||
highlight_kind = HighlightKind::Address(addr);
|
|
||||||
}
|
}
|
||||||
DiffText::Symbol(sym) => {
|
DiffText::Symbol(sym) => {
|
||||||
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
||||||
label_text = name.clone();
|
label_text = name.clone();
|
||||||
base_color = Color::White;
|
base_color = Color::White;
|
||||||
highlight_kind = HighlightKind::Symbol(name.clone());
|
|
||||||
}
|
}
|
||||||
DiffText::Spacing(n) => {
|
DiffText::Spacing(n) => {
|
||||||
crossterm::queue!(w, MoveRight(n as u16))?;
|
crossterm::queue!(w, MoveRight(n as u16))?;
|
||||||
|
@ -357,20 +458,16 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let len = label_text.len();
|
let len = label_text.len();
|
||||||
if sx >= max_sx {
|
if sx >= max.0 {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let highlighted = highlight == &highlight_kind;
|
let highlighted = *highlight == text;
|
||||||
if let Some((cx, cy)) = click_xy {
|
if let Some((cx, cy)) = self.click_xy {
|
||||||
if cx >= sx && cx < sx + len as u16 && cy == sy {
|
if cx >= sx && cx < sx + len as u16 && cy == sy {
|
||||||
if highlighted {
|
new_highlight = Some(text.into());
|
||||||
new_highlight = Some(HighlightKind::None);
|
|
||||||
} else {
|
|
||||||
new_highlight = Some(highlight_kind);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
label_text.truncate(max.0 as usize - sx as usize);
|
||||||
label_text.truncate(max_sx as usize - sx as usize);
|
|
||||||
let mut content = label_text.with(base_color);
|
let mut content = label_text.with(base_color);
|
||||||
if highlighted {
|
if highlighted {
|
||||||
content = content.on_dark_grey();
|
content = content.on_dark_grey();
|
||||||
|
@ -384,11 +481,40 @@ where
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
if sy >= max_sy {
|
if sy >= max.1 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(new_highlight)
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const COLOR_ROTATION: [Color; 8] = [
|
pub const COLOR_ROTATION: [Color; 8] = [
|
||||||
|
@ -411,26 +537,3 @@ pub fn match_percent_color(match_percent: f32) -> Color {
|
||||||
Color::Red
|
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{BufWriter, Write},
|
io::{BufReader, BufWriter, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
@ -16,9 +16,24 @@ use objdiff_core::{
|
||||||
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
||||||
|
|
||||||
#[derive(FromArgs, PartialEq, Debug)]
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
/// Generate a report from a project.
|
/// Commands for processing NVIDIA Shield TV alf files.
|
||||||
#[argp(subcommand, name = "report")]
|
#[argp(subcommand, name = "report")]
|
||||||
pub struct Args {
|
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')]
|
#[argp(option, short = 'p')]
|
||||||
/// Project directory
|
/// Project directory
|
||||||
project: Option<PathBuf>,
|
project: Option<PathBuf>,
|
||||||
|
@ -30,7 +45,22 @@ pub struct Args {
|
||||||
deduplicate: bool,
|
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<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||||
struct Report {
|
struct Report {
|
||||||
fuzzy_match_percent: f32,
|
fuzzy_match_percent: f32,
|
||||||
total_size: u64,
|
total_size: u64,
|
||||||
|
@ -42,29 +72,46 @@ struct Report {
|
||||||
units: Vec<ReportUnit>,
|
units: Vec<ReportUnit>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||||
struct ReportUnit {
|
struct ReportUnit {
|
||||||
name: String,
|
name: String,
|
||||||
match_percent: f32,
|
fuzzy_match_percent: f32,
|
||||||
total_size: u64,
|
total_size: u64,
|
||||||
matched_size: u64,
|
matched_size: u64,
|
||||||
total_functions: u32,
|
total_functions: u32,
|
||||||
matched_functions: u32,
|
matched_functions: u32,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
complete: Option<bool>,
|
complete: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
module_name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
module_id: Option<u32>,
|
||||||
functions: Vec<ReportFunction>,
|
functions: Vec<ReportFunction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||||
struct ReportFunction {
|
struct ReportFunction {
|
||||||
name: String,
|
name: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
demangled_name: Option<String>,
|
demangled_name: Option<String>,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
serialize_with = "serialize_hex",
|
||||||
|
deserialize_with = "deserialize_hex"
|
||||||
|
)]
|
||||||
|
address: Option<u64>,
|
||||||
size: u64,
|
size: u64,
|
||||||
match_percent: f32,
|
fuzzy_match_percent: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(args: Args) -> Result<()> {
|
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("."));
|
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
|
||||||
log::info!("Loading project {}", project_dir.display());
|
log::info!("Loading project {}", project_dir.display());
|
||||||
|
|
||||||
|
@ -111,7 +158,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
report.units = units.into_iter().flatten().collect();
|
report.units = units.into_iter().flatten().collect();
|
||||||
}
|
}
|
||||||
for unit in &report.units {
|
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.total_size += unit.total_size;
|
||||||
report.matched_size += unit.matched_size;
|
report.matched_size += unit.matched_size;
|
||||||
report.total_functions += unit.total_functions;
|
report.total_functions += unit.total_functions;
|
||||||
|
@ -180,7 +227,16 @@ fn report_object(
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let config = diff::DiffObjConfig { relax_reloc_diffs: true };
|
let config = diff::DiffObjConfig { relax_reloc_diffs: true };
|
||||||
diff::diff_objs(&config, target.as_mut(), base.as_mut())?;
|
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();
|
let obj = target.as_ref().or(base.as_ref()).unwrap();
|
||||||
for section in &obj.sections {
|
for section in &obj.sections {
|
||||||
if section.kind != ObjSectionKind::Code {
|
if section.kind != ObjSectionKind::Code {
|
||||||
|
@ -207,7 +263,7 @@ fn report_object(
|
||||||
0.0
|
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;
|
unit.total_size += symbol.size;
|
||||||
if match_percent == 100.0 {
|
if match_percent == 100.0 {
|
||||||
unit.matched_size += symbol.size;
|
unit.matched_size += symbol.size;
|
||||||
|
@ -216,7 +272,8 @@ fn report_object(
|
||||||
name: symbol.name.clone(),
|
name: symbol.name.clone(),
|
||||||
demangled_name: symbol.demangled_name.clone(),
|
demangled_name: symbol.demangled_name.clone(),
|
||||||
size: symbol.size,
|
size: symbol.size,
|
||||||
match_percent,
|
fuzzy_match_percent: match_percent,
|
||||||
|
address: symbol.virtual_address,
|
||||||
});
|
});
|
||||||
if match_percent == 100.0 {
|
if match_percent == 100.0 {
|
||||||
unit.matched_functions += 1;
|
unit.matched_functions += 1;
|
||||||
|
@ -225,9 +282,212 @@ fn report_object(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if unit.total_size == 0 {
|
if unit.total_size == 0 {
|
||||||
unit.match_percent = 100.0;
|
unit.fuzzy_match_percent = 100.0;
|
||||||
} else {
|
} else {
|
||||||
unit.match_percent /= unit.total_size as f32;
|
unit.fuzzy_match_percent /= unit.total_size as f32;
|
||||||
}
|
}
|
||||||
Ok(Some(unit))
|
Ok(Some(unit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct Changes {
|
||||||
|
from: ChangeInfo,
|
||||||
|
to: ChangeInfo,
|
||||||
|
units: Vec<ChangeUnit>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<ChangeInfo>,
|
||||||
|
to: Option<ChangeInfo>,
|
||||||
|
functions: Vec<ChangeFunction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct ChangeFunction {
|
||||||
|
name: String,
|
||||||
|
from: Option<ChangeFunctionInfo>,
|
||||||
|
to: Option<ChangeFunctionInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Report> {
|
||||||
|
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<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
|
||||||
|
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<Option<u64>, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,13 +3,12 @@ use std::panic;
|
||||||
pub fn crossterm_panic_handler() {
|
pub fn crossterm_panic_handler() {
|
||||||
let original_hook = panic::take_hook();
|
let original_hook = panic::take_hook();
|
||||||
panic::set_hook(Box::new(move |panic_info| {
|
panic::set_hook(Box::new(move |panic_info| {
|
||||||
crossterm::execute!(
|
let _ = crossterm::execute!(
|
||||||
std::io::stderr(),
|
std::io::stderr(),
|
||||||
crossterm::terminal::LeaveAlternateScreen,
|
crossterm::terminal::LeaveAlternateScreen,
|
||||||
crossterm::event::DisableMouseCapture
|
crossterm::event::DisableMouseCapture
|
||||||
)
|
);
|
||||||
.unwrap();
|
let _ = crossterm::terminal::disable_raw_mode();
|
||||||
crossterm::terminal::disable_raw_mode().unwrap();
|
|
||||||
original_hook(panic_info);
|
original_hook(panic_info);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,16 @@ pub enum DiffText<'a> {
|
||||||
Eol,
|
Eol,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, PartialEq, Eq)]
|
||||||
|
pub enum HighlightKind {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Opcode(u8),
|
||||||
|
Arg(ObjInsArgValue),
|
||||||
|
Symbol(String),
|
||||||
|
Address(u32),
|
||||||
|
}
|
||||||
|
|
||||||
pub fn display_diff<E>(
|
pub fn display_diff<E>(
|
||||||
ins_diff: &ObjInsDiff,
|
ins_diff: &ObjInsDiff,
|
||||||
base_addr: u32,
|
base_addr: u32,
|
||||||
|
@ -177,3 +187,31 @@ fn display_reloc<E>(
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq<DiffText<'_>> 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<HighlightKind> for DiffText<'_> {
|
||||||
|
fn eq(&self, other: &HighlightKind) -> bool { other.eq(self) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DiffText<'_>> 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 anyhow::{anyhow, bail, ensure, Context, Result};
|
||||||
use byteorder::{BigEndian, ReadBytesExt};
|
use byteorder::{BigEndian, ReadBytesExt};
|
||||||
use filetime::FileTime;
|
use filetime::FileTime;
|
||||||
use flagset::Flags;
|
use flagset::Flags;
|
||||||
use object::{
|
use object::{
|
||||||
elf, Architecture, Endianness, File, Object, ObjectSection, ObjectSymbol, RelocationKind,
|
elf, Architecture, File, Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget,
|
||||||
RelocationTarget, SectionIndex, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
|
SectionIndex, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::obj::{
|
use crate::obj::{
|
||||||
|
split_meta::{SplitMeta, SPLITMETA_SECTION},
|
||||||
ObjArchitecture, ObjInfo, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol,
|
ObjArchitecture, ObjInfo, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol,
|
||||||
ObjSymbolFlagSet, ObjSymbolFlags,
|
ObjSymbolFlagSet, ObjSymbolFlags,
|
||||||
};
|
};
|
||||||
|
@ -23,7 +24,12 @@ fn to_obj_section_kind(kind: SectionKind) -> Option<ObjSectionKind> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> Result<ObjSymbol> {
|
fn to_obj_symbol(
|
||||||
|
obj_file: &File<'_>,
|
||||||
|
symbol: &Symbol<'_, '_>,
|
||||||
|
addend: i64,
|
||||||
|
split_meta: Option<&SplitMeta>,
|
||||||
|
) -> Result<ObjSymbol> {
|
||||||
let mut name = symbol.name().context("Failed to process symbol name")?;
|
let mut name = symbol.name().context("Failed to process symbol name")?;
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
log::warn!("Found empty sym: {symbol:?}");
|
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 {
|
if obj_file.architecture() == Architecture::PowerPc {
|
||||||
demangled_name = cwdemangle::demangle(name, &Default::default());
|
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 {
|
Ok(ObjSymbol {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
demangled_name,
|
demangled_name,
|
||||||
|
@ -66,13 +76,14 @@ fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> R
|
||||||
size_known: symbol.size() != 0,
|
size_known: symbol.size() != 0,
|
||||||
flags,
|
flags,
|
||||||
addend,
|
addend,
|
||||||
|
virtual_address,
|
||||||
diff_symbol: None,
|
diff_symbol: None,
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: None,
|
match_percent: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filter_sections(obj_file: &File<'_>) -> Result<Vec<ObjSection>> {
|
fn filter_sections(obj_file: &File<'_>, split_meta: Option<&SplitMeta>) -> Result<Vec<ObjSection>> {
|
||||||
let mut result = Vec::<ObjSection>::new();
|
let mut result = Vec::<ObjSection>::new();
|
||||||
for section in obj_file.sections() {
|
for section in obj_file.sections() {
|
||||||
if section.size() == 0 {
|
if section.size() == 0 {
|
||||||
|
@ -83,6 +94,17 @@ fn filter_sections(obj_file: &File<'_>) -> Result<Vec<ObjSection>> {
|
||||||
};
|
};
|
||||||
let name = section.name().context("Failed to process section name")?;
|
let name = section.name().context("Failed to process section name")?;
|
||||||
let data = section.uncompressed_data().context("Failed to read section data")?;
|
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 {
|
result.push(ObjSection {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
kind,
|
kind,
|
||||||
|
@ -92,6 +114,7 @@ fn filter_sections(obj_file: &File<'_>) -> Result<Vec<ObjSection>> {
|
||||||
index: section.index().0,
|
index: section.index().0,
|
||||||
symbols: Vec::new(),
|
symbols: Vec::new(),
|
||||||
relocations: Vec::new(),
|
relocations: Vec::new(),
|
||||||
|
virtual_address,
|
||||||
data_diff: vec![],
|
data_diff: vec![],
|
||||||
match_percent: 0.0,
|
match_percent: 0.0,
|
||||||
});
|
});
|
||||||
|
@ -100,7 +123,11 @@ fn filter_sections(obj_file: &File<'_>) -> Result<Vec<ObjSection>> {
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result<Vec<ObjSymbol>> {
|
fn symbols_by_section(
|
||||||
|
obj_file: &File<'_>,
|
||||||
|
section: &ObjSection,
|
||||||
|
split_meta: Option<&SplitMeta>,
|
||||||
|
) -> Result<Vec<ObjSymbol>> {
|
||||||
let mut result = Vec::<ObjSymbol>::new();
|
let mut result = Vec::<ObjSymbol>::new();
|
||||||
for symbol in obj_file.symbols() {
|
for symbol in obj_file.symbols() {
|
||||||
if symbol.kind() == SymbolKind::Section {
|
if symbol.kind() == SymbolKind::Section {
|
||||||
|
@ -115,7 +142,7 @@ fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result<Vec<O
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.push(to_obj_symbol(obj_file, &symbol, 0)?);
|
result.push(to_obj_symbol(obj_file, &symbol, 0, split_meta)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,11 +160,11 @@ fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result<Vec<O
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn common_symbols(obj_file: &File<'_>) -> Result<Vec<ObjSymbol>> {
|
fn common_symbols(obj_file: &File<'_>, split_meta: Option<&SplitMeta>) -> Result<Vec<ObjSymbol>> {
|
||||||
obj_file
|
obj_file
|
||||||
.symbols()
|
.symbols()
|
||||||
.filter(Symbol::is_common)
|
.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::<Result<Vec<ObjSymbol>>>()
|
.collect::<Result<Vec<ObjSymbol>>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,6 +172,7 @@ fn find_section_symbol(
|
||||||
obj_file: &File<'_>,
|
obj_file: &File<'_>,
|
||||||
target: &Symbol<'_, '_>,
|
target: &Symbol<'_, '_>,
|
||||||
address: u64,
|
address: u64,
|
||||||
|
split_meta: Option<&SplitMeta>,
|
||||||
) -> Result<ObjSymbol> {
|
) -> Result<ObjSymbol> {
|
||||||
let section_index =
|
let section_index =
|
||||||
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
|
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
|
||||||
|
@ -164,7 +192,7 @@ fn find_section_symbol(
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return to_obj_symbol(obj_file, &symbol, 0);
|
return to_obj_symbol(obj_file, &symbol, 0, split_meta);
|
||||||
}
|
}
|
||||||
let (name, offset) = closest_symbol
|
let (name, offset) = closest_symbol
|
||||||
.and_then(|s| s.name().map(|n| (n, s.address())).ok())
|
.and_then(|s| s.name().map(|n| (n, s.address())).ok())
|
||||||
|
@ -180,6 +208,7 @@ fn find_section_symbol(
|
||||||
size_known: false,
|
size_known: false,
|
||||||
flags: Default::default(),
|
flags: Default::default(),
|
||||||
addend: offset_addr as i64,
|
addend: offset_addr as i64,
|
||||||
|
virtual_address: None,
|
||||||
diff_symbol: None,
|
diff_symbol: None,
|
||||||
instructions: vec![],
|
instructions: vec![],
|
||||||
match_percent: None,
|
match_percent: None,
|
||||||
|
@ -190,6 +219,7 @@ fn relocations_by_section(
|
||||||
arch: ObjArchitecture,
|
arch: ObjArchitecture,
|
||||||
obj_file: &File<'_>,
|
obj_file: &File<'_>,
|
||||||
section: &ObjSection,
|
section: &ObjSection,
|
||||||
|
split_meta: Option<&SplitMeta>,
|
||||||
) -> Result<Vec<ObjReloc>> {
|
) -> Result<Vec<ObjReloc>> {
|
||||||
let obj_section = obj_file.section_by_index(SectionIndex(section.index))?;
|
let obj_section = obj_file.section_by_index(SectionIndex(section.index))?;
|
||||||
let mut relocations = Vec::<ObjReloc>::new();
|
let mut relocations = Vec::<ObjReloc>::new();
|
||||||
|
@ -259,11 +289,11 @@ fn relocations_by_section(
|
||||||
// println!("Reloc: {reloc:?}, symbol: {symbol:?}, addend: {addend:#X}");
|
// println!("Reloc: {reloc:?}, symbol: {symbol:?}, addend: {addend:#X}");
|
||||||
let target = match symbol.kind() {
|
let target = match symbol.kind() {
|
||||||
SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => {
|
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 => {
|
SymbolKind::Section => {
|
||||||
ensure!(addend >= 0, "Negative addend in reloc: {addend}");
|
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:?}")),
|
kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")),
|
||||||
}?;
|
}?;
|
||||||
|
@ -298,6 +328,7 @@ fn line_info(obj_file: &File<'_>) -> Result<Option<BTreeMap<u64, u64>>> {
|
||||||
// DWARF 2+
|
// DWARF 2+
|
||||||
#[cfg(feature = "dwarf")]
|
#[cfg(feature = "dwarf")]
|
||||||
{
|
{
|
||||||
|
use std::borrow::Cow;
|
||||||
let dwarf_cow = gimli::Dwarf::load(|id| {
|
let dwarf_cow = gimli::Dwarf::load(|id| {
|
||||||
Ok::<_, gimli::Error>(
|
Ok::<_, gimli::Error>(
|
||||||
obj_file
|
obj_file
|
||||||
|
@ -307,8 +338,8 @@ fn line_info(obj_file: &File<'_>) -> Result<Option<BTreeMap<u64, u64>>> {
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let endian = match obj_file.endianness() {
|
let endian = match obj_file.endianness() {
|
||||||
Endianness::Little => gimli::RunTimeEndian::Little,
|
object::Endianness::Little => gimli::RunTimeEndian::Little,
|
||||||
Endianness::Big => gimli::RunTimeEndian::Big,
|
object::Endianness::Big => gimli::RunTimeEndian::Big,
|
||||||
};
|
};
|
||||||
let dwarf = dwarf_cow.borrow(|section| gimli::EndianSlice::new(section, endian));
|
let dwarf = dwarf_cow.borrow(|section| gimli::EndianSlice::new(section, endian));
|
||||||
let mut iter = dwarf.units();
|
let mut iter = dwarf.units();
|
||||||
|
@ -344,17 +375,35 @@ pub fn read(obj_path: &Path) -> Result<ObjInfo> {
|
||||||
Architecture::Mips => ObjArchitecture::Mips,
|
Architecture::Mips => ObjArchitecture::Mips,
|
||||||
_ => bail!("Unsupported architecture: {:?}", obj_file.architecture()),
|
_ => bail!("Unsupported architecture: {:?}", obj_file.architecture()),
|
||||||
};
|
};
|
||||||
|
let split_meta = split_meta(&obj_file)?;
|
||||||
let mut result = ObjInfo {
|
let mut result = ObjInfo {
|
||||||
architecture,
|
architecture,
|
||||||
path: obj_path.to_owned(),
|
path: obj_path.to_owned(),
|
||||||
timestamp,
|
timestamp,
|
||||||
sections: filter_sections(&obj_file)?,
|
sections: filter_sections(&obj_file, split_meta.as_ref())?,
|
||||||
common: common_symbols(&obj_file)?,
|
common: common_symbols(&obj_file, split_meta.as_ref())?,
|
||||||
line_info: line_info(&obj_file)?,
|
line_info: line_info(&obj_file)?,
|
||||||
|
split_meta: None,
|
||||||
};
|
};
|
||||||
for section in &mut result.sections {
|
for section in &mut result.sections {
|
||||||
section.symbols = symbols_by_section(&obj_file, section)?;
|
section.symbols = symbols_by_section(&obj_file, section, split_meta.as_ref())?;
|
||||||
section.relocations = relocations_by_section(architecture, &obj_file, section)?;
|
section.relocations =
|
||||||
|
relocations_by_section(architecture, &obj_file, section, split_meta.as_ref())?;
|
||||||
}
|
}
|
||||||
|
result.split_meta = split_meta;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn split_meta(obj_file: &File<'_>) -> Result<Option<SplitMeta>> {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -3,11 +3,13 @@ pub mod elf;
|
||||||
pub mod mips;
|
pub mod mips;
|
||||||
#[cfg(feature = "ppc")]
|
#[cfg(feature = "ppc")]
|
||||||
pub mod ppc;
|
pub mod ppc;
|
||||||
|
pub mod split_meta;
|
||||||
|
|
||||||
use std::{collections::BTreeMap, fmt, path::PathBuf};
|
use std::{collections::BTreeMap, fmt, path::PathBuf};
|
||||||
|
|
||||||
use filetime::FileTime;
|
use filetime::FileTime;
|
||||||
use flagset::{flags, FlagSet};
|
use flagset::{flags, FlagSet};
|
||||||
|
use split_meta::SplitMeta;
|
||||||
|
|
||||||
use crate::util::ReallySigned;
|
use crate::util::ReallySigned;
|
||||||
|
|
||||||
|
@ -39,6 +41,7 @@ pub struct ObjSection {
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
pub symbols: Vec<ObjSymbol>,
|
pub symbols: Vec<ObjSymbol>,
|
||||||
pub relocations: Vec<ObjReloc>,
|
pub relocations: Vec<ObjReloc>,
|
||||||
|
pub virtual_address: Option<u64>,
|
||||||
|
|
||||||
// Diff
|
// Diff
|
||||||
pub data_diff: Vec<ObjDataDiff>,
|
pub data_diff: Vec<ObjDataDiff>,
|
||||||
|
@ -139,7 +142,7 @@ pub struct ObjIns {
|
||||||
pub args: Vec<ObjInsArg>,
|
pub args: Vec<ObjInsArg>,
|
||||||
pub reloc: Option<ObjReloc>,
|
pub reloc: Option<ObjReloc>,
|
||||||
pub branch_dest: Option<u32>,
|
pub branch_dest: Option<u32>,
|
||||||
/// Line info
|
/// Line number
|
||||||
pub line: Option<u64>,
|
pub line: Option<u64>,
|
||||||
/// Original (unsimplified) instruction
|
/// Original (unsimplified) instruction
|
||||||
pub orig: Option<String>,
|
pub orig: Option<String>,
|
||||||
|
@ -185,6 +188,8 @@ pub struct ObjSymbol {
|
||||||
pub size_known: bool,
|
pub size_known: bool,
|
||||||
pub flags: ObjSymbolFlagSet,
|
pub flags: ObjSymbolFlagSet,
|
||||||
pub addend: i64,
|
pub addend: i64,
|
||||||
|
/// Original virtual address (from .splitmeta section)
|
||||||
|
pub virtual_address: Option<u64>,
|
||||||
|
|
||||||
// Diff
|
// Diff
|
||||||
pub diff_symbol: Option<String>,
|
pub diff_symbol: Option<String>,
|
||||||
|
@ -206,8 +211,12 @@ pub struct ObjInfo {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub timestamp: FileTime,
|
pub timestamp: FileTime,
|
||||||
pub sections: Vec<ObjSection>,
|
pub sections: Vec<ObjSection>,
|
||||||
|
/// Common BSS symbols
|
||||||
pub common: Vec<ObjSymbol>,
|
pub common: Vec<ObjSymbol>,
|
||||||
|
/// Line number info (.line or .debug_line section)
|
||||||
pub line_info: Option<BTreeMap<u64, u64>>,
|
pub line_info: Option<BTreeMap<u64, u64>>,
|
||||||
|
/// Split object metadata (.splitmeta section)
|
||||||
|
pub split_meta: Option<SplitMeta>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||||
|
|
|
@ -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<String>,
|
||||||
|
/// The name of the source module. (e.g. the DOL or REL name)
|
||||||
|
pub module_name: Option<String>,
|
||||||
|
/// The ID of the source module. (e.g. the DOL or REL ID)
|
||||||
|
pub module_id: Option<u32>,
|
||||||
|
/// Original virtual addresses of each symbol in the object.
|
||||||
|
/// Index 0 is the ELF null symbol.
|
||||||
|
pub virtual_addresses: Option<Vec<u64>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* .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<E, R>(reader: &mut R, e: E, is_64: bool) -> io::Result<Self>
|
||||||
|
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<E, W>(&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
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ use std::default::Default;
|
||||||
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget};
|
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget};
|
||||||
use egui_extras::{Column, TableBuilder, TableRow};
|
use egui_extras::{Column, TableBuilder, TableRow};
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
diff::display::{display_diff, DiffText},
|
diff::display::{display_diff, DiffText, HighlightKind},
|
||||||
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjInsDiff, ObjInsDiffKind, ObjSymbol},
|
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjInsDiff, ObjInsDiffKind, ObjSymbol},
|
||||||
};
|
};
|
||||||
use time::format_description;
|
use time::format_description;
|
||||||
|
@ -13,29 +13,6 @@ use crate::views::{
|
||||||
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolReference, View},
|
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)]
|
#[derive(Default)]
|
||||||
pub struct FunctionViewState {
|
pub struct FunctionViewState {
|
||||||
pub highlight: HighlightKind,
|
pub highlight: HighlightKind,
|
||||||
|
@ -159,7 +136,6 @@ fn diff_text_ui(
|
||||||
ObjInsDiffKind::Insert => appearance.insert_color,
|
ObjInsDiffKind::Insert => appearance.insert_color,
|
||||||
};
|
};
|
||||||
let mut pad_to = 0;
|
let mut pad_to = 0;
|
||||||
let mut highlight_kind = HighlightKind::None;
|
|
||||||
match text {
|
match text {
|
||||||
DiffText::Basic(text) => {
|
DiffText::Basic(text) => {
|
||||||
label_text = text.to_string();
|
label_text = text.to_string();
|
||||||
|
@ -176,32 +152,27 @@ fn diff_text_ui(
|
||||||
DiffText::Address(addr) => {
|
DiffText::Address(addr) => {
|
||||||
label_text = format!("{:x}:", addr);
|
label_text = format!("{:x}:", addr);
|
||||||
pad_to = 5;
|
pad_to = 5;
|
||||||
highlight_kind = HighlightKind::Address(addr);
|
|
||||||
}
|
}
|
||||||
DiffText::Opcode(mnemonic, op) => {
|
DiffText::Opcode(mnemonic, _op) => {
|
||||||
label_text = mnemonic.to_string();
|
label_text = mnemonic.to_string();
|
||||||
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
|
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
|
||||||
base_color = appearance.replace_color;
|
base_color = appearance.replace_color;
|
||||||
}
|
}
|
||||||
pad_to = 8;
|
pad_to = 8;
|
||||||
highlight_kind = HighlightKind::Opcode(op);
|
|
||||||
}
|
}
|
||||||
DiffText::Argument(arg, diff) => {
|
DiffText::Argument(arg, diff) => {
|
||||||
label_text = arg.to_string();
|
label_text = arg.to_string();
|
||||||
if let Some(diff) = diff {
|
if let Some(diff) = diff {
|
||||||
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
|
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
|
||||||
}
|
}
|
||||||
highlight_kind = HighlightKind::Arg(arg.clone());
|
|
||||||
}
|
}
|
||||||
DiffText::BranchTarget(addr) => {
|
DiffText::BranchTarget(addr) => {
|
||||||
label_text = format!("{addr:x}");
|
label_text = format!("{addr:x}");
|
||||||
highlight_kind = HighlightKind::Address(addr);
|
|
||||||
}
|
}
|
||||||
DiffText::Symbol(sym) => {
|
DiffText::Symbol(sym) => {
|
||||||
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
||||||
label_text = name.clone();
|
label_text = name.clone();
|
||||||
base_color = appearance.emphasized_text_color;
|
base_color = appearance.emphasized_text_color;
|
||||||
highlight_kind = HighlightKind::Symbol(name.clone());
|
|
||||||
}
|
}
|
||||||
DiffText::Spacing(n) => {
|
DiffText::Spacing(n) => {
|
||||||
ui.add_space(n as f32 * space_width);
|
ui.add_space(n as f32 * space_width);
|
||||||
|
@ -213,7 +184,7 @@ fn diff_text_ui(
|
||||||
}
|
}
|
||||||
|
|
||||||
let len = label_text.len();
|
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(
|
let response = Label::new(LayoutJob::single_section(
|
||||||
label_text,
|
label_text,
|
||||||
appearance.code_text_format(base_color, highlight),
|
appearance.code_text_format(base_color, highlight),
|
||||||
|
@ -225,7 +196,7 @@ fn diff_text_ui(
|
||||||
if highlight {
|
if highlight {
|
||||||
ins_view_state.highlight = HighlightKind::None;
|
ins_view_state.highlight = HighlightKind::None;
|
||||||
} else {
|
} else {
|
||||||
ins_view_state.highlight = highlight_kind;
|
ins_view_state.highlight = text.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len < pad_to {
|
if len < pad_to {
|
||||||
|
|
|
@ -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.output_mut(|output| output.copied_text = symbol.name.clone());
|
||||||
ui.close_menu();
|
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),
|
format!("Size: {:x} (assumed)", symbol.size),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some(address) = symbol.virtual_address {
|
||||||
|
ui.colored_label(
|
||||||
|
appearance.highlight_color,
|
||||||
|
format!("Virtual address: {:#x}", address),
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue