Compare commits

...

29 Commits

Author SHA1 Message Date
Luke Street 48305e0380 Add hover/context APIs to wasm component 2025-03-02 21:51:47 -07:00
Luke Street 2eafbb218b Update all dependencies 2025-03-02 16:13:12 -07:00
Luke Street a1f2a535e5 Unify context menu / hover tooltip code + UI improvements 2025-03-02 16:13:12 -07:00
Luke Street 8461b35cd7 Fix WASM CI build 2025-03-02 16:13:12 -07:00
Luke Street 95868f1d19 Reimplement x86 arch, MSVC section group combining
Plus display_row/DiffText refactoring
2025-03-02 16:13:12 -07:00
Luke Street 506c251d68 Various fixes 2025-02-26 22:47:42 -07:00
Luke Street f3c157ff06 WIP objdiff 3.0 refactor 2025-02-26 22:09:39 -07:00
angie 6d3c63ccd8 Use rabbitizer 2
Co-Authored-By: Luke Street <luke@street.dev>
2025-02-09 22:28:04 -07:00
Luke Street bbd8d9714f Adjust CI build features 2025-02-09 22:28:04 -07:00
Luke Street 3c66ac3d54 Fix run_make on Windows 2025-02-09 22:28:04 -07:00
Luke Street 561a9107e2 clippy & deny fixes 2025-02-09 22:28:04 -07:00
Luke Street e8de35b78e Make objdiff-core no_std + huge WASM rework 2025-02-09 22:28:02 -07:00
Luke Street d938988d43 Diff view refactor 2025-02-09 22:27:42 -07:00
LagoLunatic 3e6efb7736 Check relocation addends when diffing functions (#158)
* Check relocation addends when diffing functions

* Also highlight addend when reloc differs
2025-02-09 22:26:49 -07:00
Steven Casper 674c942d7d Implement context menu copy functionality for data values (#163)
* Implement context menu copy functionality for data values

* Clippy fixes
2025-02-09 22:24:52 -07:00
LagoLunatic 6b7dcabbed Fix added/removed bytes being visually misaligned in data diff view (#159) 2025-01-24 17:12:44 -07:00
Luke Street e202c3ef95 Version v2.7.1 2025-01-21 22:59:20 -07:00
LagoLunatic b7730b3d00 Refactor data relocation diffing to improve accuracy and fix bugs (#157)
* Data reloc hover tooltip: Show relocation source address

* Refactor data relocation diffing to improve accuracy and fix bugs
2025-01-21 22:54:31 -07:00
LagoLunatic a4fdb61f04 Implement diffing relocations within data sections (#154)
* Data view: Show data bytes with differing relocations as a diff

* Data view: Show differing relocations on hover

* Symbol list view: Adjust symbol/section match %s when relocations differ

* Improve data reloc diffing logic

* Don't make reloc diffs cause bytes to show as red or green

* Properly detect byte size of each relocation

* Data view: Add context menu for copying relocation target symbols

* Also show already-matching relocations on hover/right click

* Change font color for nonmatching relocs on hover
2025-01-18 16:20:07 -07:00
LagoLunatic 2876be37a3 Show relocation diffs in function view when the data's content differs (#153)
* Show reloc diff in func view when data content differs

* Add "Relax shifted data diffs" option

* Display fake pool relocations at end of line

* Diff reloc data by display string instead of raw bytes

This is to handle data symbols that contain multiple values in them at once, such as stringBase. If you compare the target symbol's bytes directly, then any part of the symbol having different bytes will cause *all* relocations to that symbol to show as a diff, even if the specific string being accessed is the same.

* Fix weak stripped symbols showing as a false diff

Fixed this by showing extern symbols correctly instead of skipping them.

* Add "Relax shifted data diffs" option to objdiff-cli

Includes both a command line argument and a keyboard shortcut (S).

* Remove addi string data hack and ... pool name hack

* Clippy fix

* PPC: Clear relocs from GPRs when overwritten

* PPC: Follow branches to improve pool detection accuracy

* PPC: Handle following bctr jump table control flow

* Clippy fixes

* PPC: Fix extern relocations not having their addend copied

* Add option to disable func data value diffing

* PPC: Handle lmw when clearing GPRs

* PPC: Handle moving reloc address with `add` inst

* Combine "relax reloc diffs" with other reloc diff options

* Add v3 config and migrate from v2

---------

Co-authored-by: Luke Street <luke@street.dev>
2025-01-18 16:18:05 -07:00
Luke Street 11171763eb Use cargo-deny-action@v2 2025-01-18 16:16:12 -07:00
Luke Street 6037a79ba2 Update all dependencies 2025-01-18 15:58:38 -07:00
Luke Street f7efe5fdff cargo update 2025-01-04 21:29:29 -07:00
Luke Street 0692deac59 Use ObjInsArgValue::loose_eq in arg_eq 2025-01-04 21:02:54 -07:00
Luke Street c3e3d175c5 Create schema for diff config properties 2025-01-04 21:02:54 -07:00
Luke Street c45f4bbc99 Diff schema updates & WASM updates 2025-01-04 21:02:54 -07:00
Luke Street b0c5431ac5 Add version to notify deps 2025-01-04 21:02:54 -07:00
LagoLunatic 9ab246367b Add buttons to collapse or expand all sections in the symbol list view for an object simultaneously (#149)
* Add buttons to expand/collapse all sections to symbol list view

* Add buttons to expand/collapse all sections to the split view
2025-01-01 20:48:25 -07:00
NWPlayer123 dcafe51eda Update Dependencies (#150)
* Update Dependencies

* Fix non-WGPU builds

---------

Co-authored-by: NWPlayer123 <NWPlayer123@users.noreply.github.com>
2025-01-01 20:45:48 -07:00
102 changed files with 21885 additions and 9842 deletions
+25 -10
View File
@@ -10,7 +10,6 @@ on:
env:
BUILD_PROFILE: release-lto
CARGO_TARGET_DIR: target
CARGO_INCREMENTAL: 0
jobs:
@@ -33,9 +32,9 @@ jobs:
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo check
run: cargo check --all-features --all-targets
run: cargo check --features all
- name: Cargo clippy
run: cargo clippy --all-features --all-targets
run: cargo clippy --features all
fmt:
name: Format
@@ -65,13 +64,12 @@ jobs:
continue-on-error: ${{ matrix.checks == 'advisories' }}
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check ${{ matrix.checks }}
test:
name: Test
if: 'false' # No tests yet
strategy:
matrix:
platform: [ ubuntu-latest, windows-latest, macos-latest ]
@@ -90,7 +88,7 @@ jobs:
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo test
run: cargo test --release --all-features
run: cargo test --release --features all
build-cli:
name: Build objdiff-cli
@@ -168,8 +166,8 @@ jobs:
with:
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
path: |
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
if-no-files-found: error
build-gui:
@@ -223,10 +221,27 @@ jobs:
with:
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
path: |
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
if-no-files-found: error
build-wasm:
name: Build objdiff-wasm
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Install dependencies
run: npm -C objdiff-wasm install
- name: Build
run: npm -C objdiff-wasm run build
release:
name: Release
if: startsWith(github.ref, 'refs/tags/')
Generated
+1501 -854
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -3,6 +3,7 @@ members = [
"objdiff-cli",
"objdiff-core",
"objdiff-gui",
"objdiff-wasm",
]
resolver = "2"
@@ -13,9 +14,9 @@ strip = "debuginfo"
codegen-units = 1
[workspace.package]
version = "2.5.0"
version = "3.0.0-alpha.1"
authors = ["Luke Street <luke@street.dev>"]
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
rust-version = "1.74"
rust-version = "1.82"
+7 -4
View File
@@ -73,7 +73,6 @@ ignore = [
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained indirect dependency" },
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
@@ -102,8 +101,7 @@ allow = [
"Zlib",
"0BSD",
"OFL-1.1",
"LicenseRef-UFL-1.0",
"OpenSSL",
"Ubuntu-font-1.0",
]
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
@@ -240,7 +238,12 @@ allow-git = []
[sources.allow-org]
# github.com organizations to allow git sources for
github = ["notify-rs"]
github = [
"CelestialAmber", # cwextab, rlwinmdec
"Decompollaborate", # rabbitizer
"enarx", # flagset
"encounter",
]
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for
+1
View File
@@ -28,6 +28,7 @@ supports-color = "3.0"
time = { version = "0.3", features = ["formatting", "local-offset"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
typed-path = "0.10"
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1"
+197 -133
View File
@@ -1,8 +1,6 @@
use std::{
fs,
io::stdout,
mem,
path::{Path, PathBuf},
str::FromStr,
sync::{
atomic::{AtomicBool, Ordering},
@@ -12,7 +10,7 @@ use std::{
time::Duration,
};
use anyhow::{bail, Context, Result};
use anyhow::{anyhow, bail, Context, Result};
use argp::FromArgs;
use crossterm::{
event,
@@ -27,17 +25,23 @@ use objdiff_core::{
watcher::{create_watcher, Watcher},
BuildConfig,
},
config::{build_globset, default_watch_patterns, ProjectConfig, ProjectObject},
diff,
diff::ObjDiff,
config::{
build_globset,
path::{check_path_buf, platform_path, platform_path_serde_option},
ProjectConfig, ProjectObject, ProjectObjectMetadata,
},
diff::{
self, ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig, MappingConfig,
ObjectDiff,
},
jobs::{
objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult,
},
obj,
obj::ObjInfo,
obj::{self, Object},
};
use ratatui::prelude::*;
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
use crate::{
util::{
@@ -51,111 +55,97 @@ use crate::{
/// Diff two object files. (Interactive or one-shot mode)
#[argp(subcommand, name = "diff")]
pub struct Args {
#[argp(option, short = '1')]
#[argp(option, short = '1', from_str_fn(platform_path))]
/// Target object file
target: Option<PathBuf>,
#[argp(option, short = '2')]
target: Option<Utf8PlatformPathBuf>,
#[argp(option, short = '2', from_str_fn(platform_path))]
/// Base object file
base: Option<PathBuf>,
#[argp(option, short = 'p')]
base: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'p', from_str_fn(platform_path))]
/// Project directory
project: Option<PathBuf>,
project: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'u')]
/// Unit name within project
unit: Option<String>,
#[argp(switch, short = 'x')]
/// Relax relocation diffs
relax_reloc_diffs: bool,
#[argp(option, short = 'o')]
#[argp(option, short = 'o', from_str_fn(platform_path))]
/// Output file (one-shot mode) ("-" for stdout)
output: Option<PathBuf>,
output: Option<Utf8PlatformPathBuf>,
#[argp(option)]
/// Output format (json, json-pretty, proto) (default: json)
format: Option<String>,
#[argp(positional)]
/// Function symbol to diff
symbol: Option<String>,
#[argp(option, short = 'c')]
/// Configuration property (key=value)
config: Vec<String>,
#[argp(option, short = 'm')]
/// Symbol mapping (target=base)
mapping: Vec<String>,
#[argp(option)]
/// Left symbol name for selection
selecting_left: Option<String>,
#[argp(option)]
/// Right symbol name for selection
selecting_right: Option<String>,
}
pub fn run(args: Args) -> Result<()> {
let (target_path, base_path, project_config) = match (
&args.target,
&args.base,
&args.project,
&args.unit,
) {
(Some(t), Some(b), None, None) => (Some(t.clone()), Some(b.clone()), None),
(None, None, p, u) => {
let project = match p {
Some(project) => project.clone(),
_ => std::env::current_dir().context("Failed to get the current directory")?,
};
let Some((project_config, project_config_info)) =
objdiff_core::config::try_project_config(&project)
else {
bail!("Project config not found in {}", &project.display())
};
let mut project_config = project_config.with_context(|| {
format!("Reading project config {}", project_config_info.path.display())
})?;
let object = {
let resolve_paths = |o: &mut ProjectObject| {
o.resolve_paths(
&project,
project_config.target_dir.as_deref(),
project_config.base_dir.as_deref(),
let (target_path, base_path, project_config) =
match (&args.target, &args.base, &args.project, &args.unit) {
(Some(_), Some(_), None, None)
| (Some(_), None, None, None)
| (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None),
(None, None, p, u) => {
let project = match p {
Some(project) => project.clone(),
_ => check_path_buf(
std::env::current_dir().context("Failed to get the current directory")?,
)
.context("Current directory is not valid UTF-8")?,
};
if let Some(u) = u {
let unit_path =
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
let Some(object) = project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.find_map(|obj| {
if obj.name.as_deref() == Some(u) {
resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?;
resolve_paths(obj);
if [&obj.base_path, &obj.target_path]
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
None
})
else {
bail!("Unit not found: {}", u)
};
object
let Some((project_config, project_config_info)) =
objdiff_core::config::try_project_config(project.as_ref())
else {
bail!("Project config not found in {}", &project)
};
let project_config = project_config.with_context(|| {
format!("Reading project config {}", project_config_info.path.display())
})?;
let target_obj_dir = project_config
.target_dir
.as_ref()
.map(|p| project.join(p.with_platform_encoding()));
let base_obj_dir = project_config
.base_dir
.as_ref()
.map(|p| project.join(p.with_platform_encoding()));
let objects = project_config
.units
.iter()
.flatten()
.map(|o| {
ObjectConfig::new(
o,
&project,
target_obj_dir.as_deref(),
base_obj_dir.as_deref(),
)
})
.collect::<Vec<_>>();
let object = if let Some(u) = u {
objects
.iter()
.find(|obj| obj.name == *u)
.ok_or_else(|| anyhow!("Unit not found: {}", u))?
} else if let Some(symbol_name) = &args.symbol {
let mut idx = None;
let mut count = 0usize;
for (i, obj) in project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.enumerate()
{
resolve_paths(obj);
for (i, obj) in objects.iter().enumerate() {
if obj
.target_path
.as_deref()
.map(|o| obj::read::has_function(o, symbol_name))
.map(|o| obj::read::has_function(o.as_ref(), symbol_name))
.transpose()?
.unwrap_or(false)
{
@@ -168,7 +158,7 @@ pub fn run(args: Args) -> Result<()> {
}
match (count, idx) {
(0, None) => bail!("Symbol not found: {}", symbol_name),
(1, Some(i)) => &mut project_config.units_mut()[i],
(1, Some(i)) => &objects[i],
(2.., Some(_)) => bail!(
"Multiple instances of {} were found, try specifying a unit",
symbol_name
@@ -177,14 +167,13 @@ pub fn run(args: Args) -> Result<()> {
}
} else {
bail!("Must specify one of: symbol, project and unit, target and base objects")
}
};
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 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"),
};
if let Some(output) = &args.output {
run_oneshot(&args, output, target_path.as_deref(), base_path.as_deref())
@@ -193,24 +182,63 @@ pub fn run(args: Args) -> Result<()> {
}
}
fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> {
let mut diff_config = DiffObjConfig::default();
for config in &args.config {
let (key, value) = config.split_once('=').context("--config expects \"key=value\"")?;
let property_id = ConfigPropertyId::from_str(key)
.map_err(|()| anyhow!("Invalid configuration property: {}", key))?;
diff_config.set_property_value_str(property_id, value).map_err(|()| {
let mut options = String::new();
match property_id.kind() {
ConfigPropertyKind::Boolean => {
options = "true, false".to_string();
}
ConfigPropertyKind::Choice(variants) => {
for (i, variant) in variants.iter().enumerate() {
if i > 0 {
options.push_str(", ");
}
options.push_str(variant.value);
}
}
}
anyhow!("Invalid value for {}. Expected one of: {}", property_id.name(), options)
})?;
}
let mut mapping_config = MappingConfig {
mappings: Default::default(),
selecting_left: args.selecting_left.clone(),
selecting_right: args.selecting_right.clone(),
};
for mapping in &args.mapping {
let (target, base) =
mapping.split_once('=').context("--mapping expects \"target=base\"")?;
mapping_config.mappings.insert(target.to_string(), base.to_string());
}
Ok((diff_config, mapping_config))
}
fn run_oneshot(
args: &Args,
output: &Path,
target_path: Option<&Path>,
base_path: Option<&Path>,
output: &Utf8PlatformPath,
target_path: Option<&Utf8PlatformPath>,
base_path: Option<&Utf8PlatformPath>,
) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?;
let config = diff::DiffObjConfig {
relax_reloc_diffs: args.relax_reloc_diffs,
..Default::default() // TODO
};
let (diff_config, mapping_config) = build_config_from_args(args)?;
let target = target_path
.map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display())))
.map(|p| {
obj::read::read(p.as_ref(), &diff_config).with_context(|| format!("Loading {}", p))
})
.transpose()?;
let base = base_path
.map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display())))
.map(|p| {
obj::read::read(p.as_ref(), &diff_config).with_context(|| format!("Loading {}", p))
})
.transpose()?;
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
let result =
diff::diff_objs(target.as_ref(), base.as_ref(), None, &diff_config, &mapping_config)?;
let left = target.as_ref().and_then(|o| result.left.as_ref().map(|d| (o, d)));
let right = base.as_ref().and_then(|o| result.right.as_ref().map(|d| (o, d)));
write_output(&DiffResult::new(left, right), Some(output), output_format)?;
@@ -220,18 +248,19 @@ fn run_oneshot(
pub struct AppState {
pub jobs: JobQueue,
pub waker: Arc<TermWaker>,
pub project_dir: Option<PathBuf>,
pub project_dir: Option<Utf8PlatformPathBuf>,
pub project_config: Option<ProjectConfig>,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub left_obj: Option<(ObjInfo, ObjDiff)>,
pub right_obj: Option<(ObjInfo, ObjDiff)>,
pub prev_obj: Option<(ObjInfo, ObjDiff)>,
pub target_path: Option<Utf8PlatformPathBuf>,
pub base_path: Option<Utf8PlatformPathBuf>,
pub left_obj: Option<(Object, ObjectDiff)>,
pub right_obj: Option<(Object, ObjectDiff)>,
pub prev_obj: Option<(Object, ObjectDiff)>,
pub reload_time: Option<time::OffsetDateTime>,
pub time_format: Vec<time::format_description::FormatItem<'static>>,
pub relax_reloc_diffs: bool,
pub watcher: Option<Watcher>,
pub modified: Arc<AtomicBool>,
pub diff_obj_config: DiffObjConfig,
pub mapping_config: MappingConfig,
}
fn create_objdiff_config(state: &AppState) -> ObjDiffConfig {
@@ -257,13 +286,51 @@ fn create_objdiff_config(state: &AppState) -> ObjDiffConfig {
.is_some_and(|p| p.build_target.unwrap_or(false)),
target_path: state.target_path.clone(),
base_path: state.base_path.clone(),
diff_obj_config: diff::DiffObjConfig {
relax_reloc_diffs: state.relax_reloc_diffs,
..Default::default() // TODO
},
symbol_mappings: Default::default(),
selecting_left: None,
selecting_right: None,
diff_obj_config: state.diff_obj_config.clone(),
mapping_config: state.mapping_config.clone(),
}
}
/// The configuration for a single object file.
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
pub struct ObjectConfig {
pub name: String,
#[serde(default, with = "platform_path_serde_option")]
pub target_path: Option<Utf8PlatformPathBuf>,
#[serde(default, with = "platform_path_serde_option")]
pub base_path: Option<Utf8PlatformPathBuf>,
pub metadata: ProjectObjectMetadata,
pub complete: Option<bool>,
}
impl ObjectConfig {
pub fn new(
object: &ProjectObject,
project_dir: &Utf8PlatformPath,
target_obj_dir: Option<&Utf8PlatformPath>,
base_obj_dir: Option<&Utf8PlatformPath>,
) -> Self {
let target_path = if let (Some(target_obj_dir), Some(path), None) =
(target_obj_dir, &object.path, &object.target_path)
{
Some(target_obj_dir.join(path.with_platform_encoding()))
} else {
object.target_path.as_ref().map(|path| project_dir.join(path.with_platform_encoding()))
};
let base_path = if let (Some(base_obj_dir), Some(path), None) =
(base_obj_dir, &object.path, &object.base_path)
{
Some(base_obj_dir.join(path.with_platform_encoding()))
} else {
object.base_path.as_ref().map(|path| project_dir.join(path.with_platform_encoding()))
};
Self {
name: object.name().to_string(),
target_path,
base_path,
metadata: object.metadata.clone().unwrap_or_default(),
complete: object.complete(),
}
}
}
@@ -307,13 +374,14 @@ impl Wake for TermWaker {
fn run_interactive(
args: Args,
target_path: Option<PathBuf>,
base_path: Option<PathBuf>,
target_path: Option<Utf8PlatformPathBuf>,
base_path: Option<Utf8PlatformPathBuf>,
project_config: Option<ProjectConfig>,
) -> Result<()> {
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") };
let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]")
.context("Failed to parse time format")?;
let (diff_obj_config, mapping_config) = build_config_from_args(&args)?;
let mut state = AppState {
jobs: Default::default(),
waker: Default::default(),
@@ -326,20 +394,16 @@ fn run_interactive(
prev_obj: None,
reload_time: None,
time_format,
relax_reloc_diffs: args.relax_reloc_diffs,
watcher: None,
modified: Default::default(),
diff_obj_config,
mapping_config,
};
if let Some(project_dir) = &state.project_dir {
let watch_patterns = state
.project_config
.as_ref()
.and_then(|c| c.watch_patterns.as_ref())
.cloned()
.unwrap_or_else(default_watch_patterns);
if let (Some(project_dir), Some(project_config)) = (&state.project_dir, &state.project_config) {
let watch_patterns = project_config.build_watch_patterns()?;
state.watcher = Some(create_watcher(
state.modified.clone(),
project_dir,
project_dir.as_ref(),
build_globset(&watch_patterns)?,
Waker::from(state.waker.clone()),
)?);
+81 -84
View File
@@ -1,10 +1,4 @@
use std::{
collections::HashSet,
fs::File,
io::Read,
path::{Path, PathBuf},
time::Instant,
};
use std::{collections::HashSet, fs::File, io::Read, time::Instant};
use anyhow::{bail, Context, Result};
use argp::FromArgs;
@@ -14,15 +8,19 @@ use objdiff_core::{
ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
REPORT_VERSION,
},
config::ProjectObject,
config::path::platform_path,
diff, obj,
obj::{ObjSectionKind, ObjSymbolFlags},
obj::{SectionKind, SymbolFlag},
};
use prost::Message;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use tracing::{info, warn};
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
use crate::util::output::{write_output, OutputFormat};
use crate::{
cmd::diff::ObjectConfig,
util::output::{write_output, OutputFormat},
};
#[derive(FromArgs, PartialEq, Debug)]
/// Generate a progress report for a project.
@@ -43,12 +41,12 @@ pub enum SubCommand {
/// Generate a progress report for a project.
#[argp(subcommand, name = "generate")]
pub struct GenerateArgs {
#[argp(option, short = 'p')]
#[argp(option, short = 'p', from_str_fn(platform_path))]
/// Project directory
project: Option<PathBuf>,
#[argp(option, short = 'o')]
project: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'o', from_str_fn(platform_path))]
/// Output file
output: Option<PathBuf>,
output: Option<Utf8PlatformPathBuf>,
#[argp(switch, short = 'd')]
/// Deduplicate global and weak symbols (runs single-threaded)
deduplicate: bool,
@@ -61,15 +59,15 @@ pub struct GenerateArgs {
/// List any changes from a previous report.
#[argp(subcommand, name = "changes")]
pub struct ChangesArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(platform_path))]
/// Previous report file
previous: PathBuf,
#[argp(positional)]
previous: Utf8PlatformPathBuf,
#[argp(positional, from_str_fn(platform_path))]
/// Current report file
current: PathBuf,
#[argp(option, short = 'o')]
current: Utf8PlatformPathBuf,
#[argp(option, short = 'o', from_str_fn(platform_path))]
/// Output file
output: Option<PathBuf>,
output: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'f')]
/// Output format (json, json-pretty, proto) (default: json)
format: Option<String>,
@@ -84,10 +82,10 @@ pub fn run(args: Args) -> Result<()> {
fn generate(args: GenerateArgs) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?;
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
info!("Loading project {}", project_dir.display());
let project_dir = args.project.as_deref().unwrap_or_else(|| Utf8PlatformPath::new("."));
info!("Loading project {}", project_dir);
let mut project = match objdiff_core::config::try_project_config(project_dir) {
let project = match objdiff_core::config::try_project_config(project_dir.as_ref()) {
Some((Ok(config), _)) => config,
Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
None => bail!("No project configuration found"),
@@ -98,37 +96,33 @@ fn generate(args: GenerateArgs) -> Result<()> {
if args.deduplicate { 1 } else { rayon::current_num_threads() }
);
let target_obj_dir =
project.target_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
let base_obj_dir =
project.base_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
let objects = project
.units
.iter()
.flatten()
.map(|o| {
ObjectConfig::new(o, project_dir, target_obj_dir.as_deref(), base_obj_dir.as_deref())
})
.collect::<Vec<_>>();
let start = Instant::now();
let mut units = vec![];
let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate {
// If deduplicating, we need to run single-threaded
for object in project.units.as_deref_mut().unwrap_or_default() {
if let Some(unit) = report_object(
object,
project_dir,
project.target_dir.as_deref(),
project.base_dir.as_deref(),
Some(&mut existing_functions),
)? {
for object in &objects {
if let Some(unit) = report_object(object, Some(&mut existing_functions))? {
units.push(unit);
}
}
} else {
let vec = project
.units
.as_deref_mut()
.unwrap_or_default()
.par_iter_mut()
.map(|object| {
report_object(
object,
project_dir,
project.target_dir.as_deref(),
project.base_dir.as_deref(),
None,
)
})
let vec = objects
.par_iter()
.map(|object| report_object(object, None))
.collect::<Result<Vec<Option<ReportUnit>>>>()?;
units = vec.into_iter().flatten().collect();
}
@@ -151,55 +145,54 @@ fn generate(args: GenerateArgs) -> Result<()> {
}
fn report_object(
object: &mut ProjectObject,
project_dir: &Path,
target_dir: Option<&Path>,
base_dir: Option<&Path>,
object: &ObjectConfig,
mut existing_functions: Option<&mut HashSet<String>>,
) -> Result<Option<ReportUnit>> {
object.resolve_paths(project_dir, target_dir, base_dir);
match (&object.target_path, &object.base_path) {
(None, Some(_)) if !object.complete().unwrap_or(false) => {
warn!("Skipping object without target: {}", object.name());
(None, Some(_)) if !object.complete.unwrap_or(false) => {
warn!("Skipping object without target: {}", object.name);
return Ok(None);
}
(None, None) => {
warn!("Skipping object without target or base: {}", object.name());
warn!("Skipping object without target or base: {}", object.name);
return Ok(None);
}
_ => {}
}
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
let diff_config = diff::DiffObjConfig {
function_reloc_diffs: diff::FunctionRelocDiffs::None,
..Default::default()
};
let mapping_config = diff::MappingConfig::default();
let target = object
.target_path
.as_ref()
.map(|p| {
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
obj::read::read(p.as_ref(), &diff_config)
.with_context(|| format!("Failed to open {}", p))
})
.transpose()?;
let base = object
.base_path
.as_ref()
.map(|p| {
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
obj::read::read(p.as_ref(), &diff_config)
.with_context(|| format!("Failed to open {}", p))
})
.transpose()?;
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
let result =
diff::diff_objs(target.as_ref(), base.as_ref(), None, &diff_config, &mapping_config)?;
let metadata = ReportUnitMetadata {
complete: object.complete(),
complete: object.metadata.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),
source_path: object.metadata.as_ref().and_then(|m| m.source_path.clone()),
progress_categories: object
.metadata
.as_ref()
.and_then(|m| m.progress_categories.clone())
.unwrap_or_default(),
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
source_path: object.metadata.source_path.as_ref().map(|p| p.to_string()),
progress_categories: object.metadata.progress_categories.clone().unwrap_or_default(),
auto_generated: object.metadata.auto_generated,
};
let mut measures = Measures { total_units: 1, ..Default::default() };
let mut sections = vec![];
@@ -207,11 +200,13 @@ fn report_object(
let obj = target.as_ref().or(base.as_ref()).unwrap();
let obj_diff = result.left.as_ref().or(result.right.as_ref()).unwrap();
for (section, section_diff) in obj.sections.iter().zip(&obj_diff.sections) {
for ((section_idx, section), section_diff) in
obj.sections.iter().enumerate().zip(&obj_diff.sections)
{
let section_match_percent = section_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object,
// assume complete means 100% match
if object.complete().unwrap_or(false) {
if object.complete.unwrap_or(false) {
100.0
} else {
0.0
@@ -228,23 +223,26 @@ fn report_object(
});
match section.kind {
ObjSectionKind::Data | ObjSectionKind::Bss => {
SectionKind::Data | SectionKind::Bss => {
measures.total_data += section.size;
if section_match_percent == 100.0 {
measures.matched_data += section.size;
}
continue;
}
ObjSectionKind::Code => (),
_ => {}
}
for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.symbols) {
if symbol.size == 0 || symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
for (symbol, symbol_diff) in obj.symbols.iter().zip(&obj_diff.symbols) {
if symbol.section != Some(section_idx)
|| symbol.size == 0
|| symbol.flags.contains(SymbolFlag::Hidden)
{
continue;
}
if let Some(existing_functions) = &mut existing_functions {
if (symbol.flags.0.contains(ObjSymbolFlags::Global)
|| symbol.flags.0.contains(ObjSymbolFlags::Weak))
if (symbol.flags.contains(SymbolFlag::Global)
|| symbol.flags.contains(SymbolFlag::Weak))
&& !existing_functions.insert(symbol.name.clone())
{
continue;
@@ -253,7 +251,7 @@ fn report_object(
let match_percent = symbol_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object,
// assume complete means 100% match
if object.complete().unwrap_or(false) {
if object.complete.unwrap_or(false) {
100.0
} else {
0.0
@@ -287,7 +285,7 @@ fn report_object(
measures.calc_fuzzy_match_percent();
measures.calc_matched_percent();
Ok(Some(ReportUnit {
name: object.name().to_string(),
name: object.name.clone(),
measures: Some(measures),
sections,
functions,
@@ -297,7 +295,7 @@ fn report_object(
fn changes(args: ChangesArgs) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?;
let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") {
let (previous, current) = if args.previous == "-" && args.current == "-" {
// Special case for comparing two reports from stdin
let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?;
@@ -412,15 +410,14 @@ fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
.collect()
}
fn read_report(path: &Path) -> Result<Report> {
if path == Path::new("-") {
fn read_report(path: &Utf8PlatformPath) -> Result<Report> {
if path == Utf8PlatformPath::new("-") {
let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?;
return Report::parse(&data).with_context(|| "Failed to load report from stdin");
}
let file = File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
let mmap = unsafe { memmap2::Mmap::map(&file) }
.with_context(|| format!("Failed to map {}", path.display()))?;
Report::parse(mmap.as_ref())
.with_context(|| format!("Failed to load report {}", path.display()))
let file = File::open(path).with_context(|| format!("Failed to open {}", path))?;
let mmap =
unsafe { memmap2::Mmap::map(&file) }.with_context(|| format!("Failed to map {}", path))?;
Report::parse(mmap.as_ref()).with_context(|| format!("Failed to load report {}", path))
}
+2
View File
@@ -1,3 +1,5 @@
#![allow(clippy::too_many_arguments)]
mod argp_version;
mod cmd;
mod util;
+6 -3
View File
@@ -34,9 +34,12 @@ impl OutputFormat {
}
}
pub fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
where T: serde::Serialize + prost::Message {
match output {
pub fn write_output<T, P>(input: &T, output: Option<P>, format: OutputFormat) -> Result<()>
where
T: serde::Serialize + prost::Message,
P: AsRef<Path>,
{
match output.as_ref().map(|p| p.as_ref()) {
Some(output) if output != Path::new("-") => {
info!("Writing to {}", output.display());
let file = File::options()
+96 -110
View File
@@ -1,11 +1,13 @@
use core::cmp::Ordering;
use anyhow::{bail, Result};
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};
use objdiff_core::{
diff::{
display::{display_diff, DiffText, HighlightKind},
ObjDiff, ObjInsDiffKind, ObjSymbolDiff,
display::{display_row, DiffText, DiffTextColor, HighlightKind},
DiffObjConfig, FunctionRelocDiffs, InstructionDiffKind, ObjectDiff, SymbolDiff,
},
obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef},
obj::Object,
};
use ratatui::{
prelude::*,
@@ -28,9 +30,9 @@ pub struct FunctionDiffUi {
pub scroll_state_y: ScrollbarState,
pub per_page: usize,
pub num_rows: usize,
pub left_sym: Option<SymbolRef>,
pub right_sym: Option<SymbolRef>,
pub prev_sym: Option<SymbolRef>,
pub left_sym: Option<usize>,
pub right_sym: Option<usize>,
pub prev_sym: Option<usize>,
pub open_options: bool,
pub three_way: bool,
}
@@ -80,8 +82,8 @@ impl UiView for FunctionDiffUi {
f.render_widget(line_l, header_chunks[0]);
let mut line_r = Line::default();
if let Some(percent) =
get_symbol(state.right_obj.as_ref(), self.right_sym).and_then(|(_, d)| d.match_percent)
if let Some(percent) = get_symbol(state.right_obj.as_ref(), self.right_sym)
.and_then(|(_, _, d)| d.match_percent)
{
line_r.spans.push(Span::styled(
format!("{:.2}% ", percent),
@@ -106,13 +108,17 @@ impl UiView for FunctionDiffUi {
let mut left_text = None;
let mut left_highlight = None;
let mut max_width = 0;
if let Some((symbol, symbol_diff)) = get_symbol(state.left_obj.as_ref(), self.left_sym) {
if let Some((obj, symbol_idx, symbol_diff)) =
get_symbol(state.left_obj.as_ref(), self.left_sym)
{
let mut text = Text::default();
let rect = content_chunks[0].inner(Margin::new(0, 1));
left_highlight = self.print_sym(
&mut text,
symbol,
obj,
symbol_idx,
symbol_diff,
&state.diff_obj_config,
rect,
&self.left_highlight,
result,
@@ -125,13 +131,17 @@ impl UiView for FunctionDiffUi {
let mut right_text = None;
let mut right_highlight = None;
let mut margin_text = None;
if let Some((symbol, symbol_diff)) = get_symbol(state.right_obj.as_ref(), self.right_sym) {
if let Some((obj, symbol_idx, symbol_diff)) =
get_symbol(state.right_obj.as_ref(), self.right_sym)
{
let mut text = Text::default();
let rect = content_chunks[2].inner(Margin::new(0, 1));
right_highlight = self.print_sym(
&mut text,
symbol,
obj,
symbol_idx,
symbol_diff,
&state.diff_obj_config,
rect,
&self.right_highlight,
result,
@@ -150,14 +160,17 @@ impl UiView for FunctionDiffUi {
let mut prev_text = None;
let mut prev_margin_text = None;
if self.three_way {
if let Some((symbol, symbol_diff)) = get_symbol(state.prev_obj.as_ref(), self.prev_sym)
if let Some((obj, symbol_idx, symbol_diff)) =
get_symbol(state.prev_obj.as_ref(), self.prev_sym)
{
let mut text = Text::default();
let rect = content_chunks[4].inner(Margin::new(0, 1));
self.print_sym(
&mut text,
symbol,
obj,
symbol_idx,
symbol_diff,
&state.diff_obj_config,
rect,
&self.right_highlight,
result,
@@ -368,9 +381,15 @@ impl UiView for FunctionDiffUi {
self.scroll_x = self.scroll_x.saturating_sub(1);
result.redraw = true;
}
// Toggle relax relocation diffs
// Cycle through function relocation diff mode
KeyCode::Char('x') => {
state.relax_reloc_diffs = !state.relax_reloc_diffs;
state.diff_obj_config.function_reloc_diffs =
match state.diff_obj_config.function_reloc_diffs {
FunctionRelocDiffs::None => FunctionRelocDiffs::NameAddress,
FunctionRelocDiffs::NameAddress => FunctionRelocDiffs::DataValue,
FunctionRelocDiffs::DataValue => FunctionRelocDiffs::All,
FunctionRelocDiffs::All => FunctionRelocDiffs::None,
};
result.redraw = true;
return EventControlFlow::Reload;
}
@@ -429,9 +448,11 @@ impl UiView for FunctionDiffUi {
get_symbol(state.left_obj.as_ref(), left_sym),
get_symbol(state.right_obj.as_ref(), right_sym),
) {
(Some((_l, ld)), Some((_r, rd))) => ld.instructions.len().max(rd.instructions.len()),
(Some((_l, ld)), None) => ld.instructions.len(),
(None, Some((_r, rd))) => rd.instructions.len(),
(Some((_l, _ls, ld)), Some((_r, _rs, rd))) => {
ld.instruction_rows.len().max(rd.instruction_rows.len())
}
(Some((_l, _ls, ld)), None) => ld.instruction_rows.len(),
(None, Some((_r, _rs, rd))) => rd.instruction_rows.len(),
(None, None) => bail!("Symbol not found: {}", self.symbol_name),
};
self.left_sym = left_sym;
@@ -474,113 +495,82 @@ impl FunctionDiffUi {
self.scroll_y += self.per_page / if half { 2 } else { 1 };
}
#[allow(clippy::too_many_arguments)]
fn print_sym(
&self,
out: &mut Text<'static>,
symbol: &ObjSymbol,
symbol_diff: &ObjSymbolDiff,
obj: &Object,
symbol_index: usize,
symbol_diff: &SymbolDiff,
diff_config: &DiffObjConfig,
rect: Rect,
highlight: &HighlightKind,
result: &EventResult,
only_changed: bool,
) -> Option<HighlightKind> {
let base_addr = symbol.address;
let mut new_highlight = None;
for (y, ins_diff) in symbol_diff
.instructions
for (y, ins_row) in symbol_diff
.instruction_rows
.iter()
.skip(self.scroll_y)
.take(rect.height as usize)
.enumerate()
{
if only_changed && ins_diff.kind == ObjInsDiffKind::None {
if only_changed && ins_row.kind == InstructionDiffKind::None {
out.lines.push(Line::default());
continue;
}
let mut sx = rect.x;
let sy = rect.y + y as u16;
let mut line = Line::default();
display_diff(ins_diff, base_addr, |text| -> Result<()> {
let label_text;
let mut base_color = match ins_diff.kind {
ObjInsDiffKind::None
| ObjInsDiffKind::OpMismatch
| ObjInsDiffKind::ArgMismatch => Color::Gray,
ObjInsDiffKind::Replace => Color::Cyan,
ObjInsDiffKind::Delete => Color::Red,
ObjInsDiffKind::Insert => Color::Green,
};
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::DarkGray;
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::BranchDest(addr, diff) => {
label_text = format!("{addr:x}");
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::Symbol(sym, diff) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone();
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
} else {
base_color = Color::White;
}
display_row(obj, symbol_index, ins_row, diff_config, |segment| {
let highlight_kind = HighlightKind::from(&segment.text);
let label_text = match segment.text {
DiffText::Basic(text) => text.to_string(),
DiffText::Line(num) => format!("{num} "),
DiffText::Address(addr) => format!("{:x}:", addr),
DiffText::Opcode(mnemonic, _op) => format!("{mnemonic} "),
DiffText::Argument(arg) => arg.to_string(),
DiffText::BranchDest(addr) => format!("{addr:x}"),
DiffText::Symbol(sym) => {
sym.demangled_name.as_ref().unwrap_or(&sym.name).clone()
}
DiffText::Addend(addend) => match addend.cmp(&0i64) {
Ordering::Greater => format!("+{:#x}", addend),
Ordering::Less => format!("-{:#x}", -addend),
_ => String::new(),
},
DiffText::Spacing(n) => {
line.spans.push(Span::raw(" ".repeat(n)));
line.spans.push(Span::raw(" ".repeat(n as usize)));
sx += n as u16;
return Ok(());
}
DiffText::Eol => {
return Ok(());
}
}
DiffText::Eol => return Ok(()),
};
let len = label_text.len();
let highlighted = *highlight == text;
let highlighted =
highlight_kind != HighlightKind::None && *highlight == highlight_kind;
if let Some((cx, cy)) = result.click_xy {
if cx >= sx && cx < sx + len as u16 && cy == sy {
new_highlight = Some(text.into());
new_highlight = Some(highlight_kind);
}
}
let mut style = Style::new().fg(base_color);
let mut style = Style::new().fg(match segment.color {
DiffTextColor::Normal => Color::Gray,
DiffTextColor::Dim => Color::DarkGray,
DiffTextColor::Bright => Color::White,
DiffTextColor::Replace => Color::Cyan,
DiffTextColor::Delete => Color::Red,
DiffTextColor::Insert => Color::Green,
DiffTextColor::Rotating(i) => COLOR_ROTATION[i as usize % COLOR_ROTATION.len()],
});
if highlighted {
style = style.bg(Color::DarkGray);
}
line.spans.push(Span::styled(label_text, style));
sx += len as u16;
if pad_to > len {
let pad = (pad_to - len) as u16;
if segment.pad_to as usize > len {
let pad = (segment.pad_to as usize - len) as u16;
line.spans.push(Span::raw(" ".repeat(pad as usize)));
sx += pad;
}
@@ -592,12 +582,13 @@ impl FunctionDiffUi {
new_highlight
}
fn print_margin(&self, out: &mut Text, symbol: &ObjSymbolDiff, rect: Rect) {
for ins_diff in symbol.instructions.iter().skip(self.scroll_y).take(rect.height as usize) {
if ins_diff.kind != ObjInsDiffKind::None {
out.lines.push(Line::raw(match ins_diff.kind {
ObjInsDiffKind::Delete => "<",
ObjInsDiffKind::Insert => ">",
fn print_margin(&self, out: &mut Text, symbol: &SymbolDiff, rect: Rect) {
for ins_row in symbol.instruction_rows.iter().skip(self.scroll_y).take(rect.height as usize)
{
if ins_row.kind != InstructionDiffKind::None {
out.lines.push(Line::raw(match ins_row.kind {
InstructionDiffKind::Delete => "<",
InstructionDiffKind::Insert => ">",
_ => "|",
}));
} else {
@@ -629,23 +620,18 @@ pub fn match_percent_color(match_percent: f32) -> Color {
#[inline]
fn get_symbol(
obj: Option<&(ObjInfo, ObjDiff)>,
sym: Option<SymbolRef>,
) -> Option<(&ObjSymbol, &ObjSymbolDiff)> {
obj: Option<&(Object, ObjectDiff)>,
sym: Option<usize>,
) -> Option<(&Object, usize, &SymbolDiff)> {
let (obj, diff) = obj?;
let sym = sym?;
Some((obj.section_symbol(sym).1, diff.symbol_diff(sym)))
Some((obj, sym, &diff.symbols[sym]))
}
fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
if section.kind != ObjSectionKind::Code {
continue;
}
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == name {
return Some(SymbolRef { section_idx, symbol_idx });
}
fn find_function(obj: &Object, name: &str) -> Option<usize> {
for (symbol_idx, symbol) in obj.symbols.iter().enumerate() {
if symbol.name == name {
return Some(symbol_idx);
}
}
None
+138 -51
View File
@@ -12,97 +12,184 @@ A local diffing tool for decompilation projects.
"""
documentation = "https://docs.rs/objdiff-core"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "arm64", "bindings", "build"]
any-arch = ["config", "dep:bimap", "dep:strum", "dep:similar", "dep:flagset", "dep:log", "dep:memmap2", "dep:byteorder", "dep:num-traits"] # Implicit, used to check if any arch is enabled
bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"]
build = ["dep:shell-escape", "dep:path-slash", "dep:winapi", "dep:notify", "dep:notify-debouncer-full", "dep:reqwest", "dep:self_update", "dep:tempfile", "dep:time"]
config = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"]
default = ["std"]
all = [
# Features
"bindings",
"build",
"config",
"dwarf",
"serde",
# Architectures
"arm",
"arm64",
"mips",
"ppc",
"x86",
]
# Implicit, used to check if any arch is enabled
any-arch = [
"dep:flagset",
"dep:heck",
"dep:log",
"dep:num-traits",
"dep:prettyplease",
"dep:proc-macro2",
"dep:quote",
"dep:regex",
"dep:similar",
"dep:syn",
]
bindings = [
"dep:prost",
"dep:prost-build",
]
build = [
"dep:notify",
"dep:notify-debouncer-full",
"dep:reqwest",
"dep:self_update",
"dep:shell-escape",
"dep:tempfile",
"dep:time",
"dep:winapi",
]
config = [
"dep:globset",
"dep:semver",
"dep:typed-path",
]
dwarf = ["dep:gimli"]
mips = ["any-arch", "dep:rabbitizer"]
ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"]
x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"]
arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"]
arm64 = ["any-arch", "dep:cpp_demangle", "dep:yaxpeax-arch", "dep:yaxpeax-arm"]
wasm = ["bindings", "any-arch", "dep:console_error_panic_hook", "dep:console_log", "dep:wasm-bindgen", "dep:tsify-next", "dep:log"]
serde = [
"dep:pbjson",
"dep:pbjson-build",
"dep:serde",
"dep:serde_json",
]
std = [
"anyhow/std",
"flagset?/std",
"log?/std",
"num-traits?/std",
"object/std",
"prost?/std",
"serde?/std",
"typed-path?/std",
"dep:filetime",
"dep:memmap2",
]
mips = [
"any-arch",
"dep:rabbitizer",
]
ppc = [
"any-arch",
"dep:cwdemangle",
"dep:cwextab",
"dep:ppc750cl",
"dep:rlwinmdec",
]
x86 = [
"any-arch",
"dep:cpp_demangle",
"dep:iced-x86",
"dep:msvc-demangler",
]
arm = [
"any-arch",
"dep:arm-attr",
"dep:cpp_demangle",
"dep:unarm",
]
arm64 = [
"any-arch",
"dep:cpp_demangle",
"dep:yaxpeax-arch",
"dep:yaxpeax-arm",
]
[package.metadata.docs.rs]
features = ["all"]
[dependencies]
anyhow = "1.0"
bimap = { version = "0.6", features = ["serde"], optional = true }
byteorder = { version = "1.5", optional = true }
anyhow = { version = "1.0", default-features = false }
filetime = { version = "0.2", optional = true }
flagset = { version = "0.4", optional = true }
log = { version = "0.4", optional = true }
flagset = { version = "0.4", default-features = false, optional = true, git = "https://github.com/enarx/flagset.git", rev = "a1fe9369b3741e43fec45da1998e83b9d78966a2" }
itertools = { version = "0.14", default-features = false, features = ["use_alloc"] }
log = { version = "0.4", default-features = false, optional = true }
memmap2 = { version = "0.9", optional = true }
num-traits = { version = "0.2", optional = true }
object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false }
pbjson = { version = "0.7", optional = true }
prost = { version = "0.13", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
similar = { version = "2.6", default-features = false, optional = true }
strum = { version = "0.26", features = ["derive"], optional = true }
wasm-bindgen = { version = "0.2", optional = true }
tsify-next = { version = "0.5", default-features = false, features = ["js"], optional = true }
console_log = { version = "1.0", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
num-traits = { version = "0.2", default-features = false, optional = true }
object = { version = "0.36", default-features = false, features = ["read_core", "elf", "pe"] }
pbjson = { version = "0.7", default-features = false, optional = true }
prost = { version = "0.13", default-features = false, features = ["prost-derive"], optional = true }
regex = { version = "1.11", default-features = false, features = [], optional = true }
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
similar = { version = "2.7", default-features = false, optional = true, git = "https://github.com/encounter/similar.git", branch = "no_std" }
typed-path = { version = "0.10", default-features = false, optional = true }
# config
globset = { version = "0.4", features = ["serde1"], optional = true }
semver = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
globset = { version = "0.4", default-features = false, optional = true }
semver = { version = "1.0", default-features = false, optional = true }
serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true }
# dwarf
gimli = { version = "0.31", default-features = false, features = ["read-all"], optional = true }
gimli = { version = "0.31", default-features = false, features = ["read"], optional = true }
# ppc
cwdemangle = { version = "1.0", optional = true }
cwextab = { version = "1.0.2", optional = true }
cwextab = { version = "1.0", optional = true, git = "https://github.com/CelestialAmber/cwextab.git" }
ppc750cl = { version = "0.3", optional = true }
rlwinmdec = { version = "1.1", optional = true, git = "https://github.com/CelestialAmber/rlwinmdec.git" }
# mips
rabbitizer = { version = "1.12", optional = true }
rabbitizer = { git = "https://github.com/Decompollaborate/rabbitizer.git", branch = "🦀", default-features = false, features = ["all_extensions"], optional = true }
# x86
cpp_demangle = { version = "0.4", optional = true }
iced-x86 = { version = "1.21", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
msvc-demangler = { version = "0.10", optional = true }
cpp_demangle = { version = "0.4", default-features = false, features = ["alloc"], optional = true }
iced-x86 = { version = "1.21", default-features = false, features = ["decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums", "no_std"], optional = true }
msvc-demangler = { version = "0.11", optional = true }
# arm
unarm = { version = "1.6", optional = true }
arm-attr = { version = "0.1", optional = true }
unarm = { version = "1.7", optional = true }
arm-attr = { version = "0.2", optional = true }
# arm64
yaxpeax-arch = { version = "0.3", default-features = false, features = ["std"], optional = true }
yaxpeax-arm = { version = "0.3", default-features = false, features = ["std"], optional = true }
yaxpeax-arch = { version = "0.3", default-features = false, optional = true }
yaxpeax-arm = { version = "0.3", default-features = false, optional = true }
# build
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2", optional = true }
notify-debouncer-full = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2", optional = true }
notify = { version = "8.0.0", optional = true }
notify-debouncer-full = { version = "0.5.0", optional = true }
shell-escape = { version = "0.1", optional = true }
tempfile = { version = "3.14", optional = true }
tempfile = { version = "3.17", optional = true }
time = { version = "0.3", optional = true }
[target.'cfg(windows)'.dependencies]
path-slash = { version = "0.2", optional = true }
winapi = { version = "0.3", optional = true }
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"], optional = true }
self_update = { version = "0.41", default-features = false, features = ["rustls"], optional = true }
self_update = { version = "0.42", default-features = false, features = ["rustls"], optional = true }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"], optional = true }
self_update = { version = "0.41", optional = true }
self_update = { version = "0.42", optional = true }
[build-dependencies]
prost-build = { version = "0.13", optional = true }
heck = { version = "0.5", optional = true }
pbjson-build = { version = "0.7", optional = true }
prettyplease = { version = "0.2", optional = true }
proc-macro2 = { version = "1.0", optional = true }
prost-build = { version = "0.13", optional = true }
quote = { version = "1.0", optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
syn = { version = "2.0", optional = true }
[dev-dependencies]
# Enable all features for tests
objdiff-core = { path = ".", features = ["all"] }
insta = "1.42"
+18 -9
View File
@@ -1,6 +1,12 @@
fn main() {
#[cfg(feature = "any-arch")]
mod config_gen;
fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "bindings")]
compile_protos();
#[cfg(feature = "any-arch")]
config_gen::generate_diff_config();
Ok(())
}
#[cfg(feature = "bindings")]
@@ -13,7 +19,7 @@ fn compile_protos() {
.map(|m| m.modified().unwrap())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
let mut run_protoc = false;
let proto_files = vec![root.join("diff.proto"), root.join("report.proto")];
let proto_files = vec![root.join("report.proto")];
for proto_file in &proto_files {
println!("cargo:rerun-if-changed={}", proto_file.display());
let mtime = match std::fs::metadata(proto_file) {
@@ -49,11 +55,14 @@ fn compile_protos() {
}
}
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
pbjson_build::Builder::new()
.register_descriptors(&descriptor_set)
.expect("Failed to register descriptors")
.preserve_proto_field_names()
.build(&[".objdiff"])
.expect("Failed to build pbjson");
#[cfg(feature = "serde")]
{
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
pbjson_build::Builder::new()
.register_descriptors(&descriptor_set)
.expect("Failed to register descriptors")
.preserve_proto_field_names()
.build(&[".objdiff"])
.expect("Failed to build pbjson");
}
}
+256
View File
@@ -0,0 +1,256 @@
{
"properties": [
{
"id": "functionRelocDiffs",
"type": "choice",
"default": "name_address",
"name": "Function relocation diffs",
"description": "How relocation targets will be diffed in the function view.",
"items": [
{
"value": "none",
"name": "None"
},
{
"value": "name_address",
"name": "Name or address"
},
{
"value": "data_value",
"name": "Data value"
},
{
"value": "all",
"name": "Name or address, data value"
}
]
},
{
"id": "spaceBetweenArgs",
"type": "boolean",
"default": true,
"name": "Space between args",
"description": "Adds a space between arguments in the diff output."
},
{
"id": "combineDataSections",
"type": "boolean",
"default": false,
"name": "Combine data sections",
"description": "Combines data sections with equal names."
},
{
"id": "combineTextSections",
"type": "boolean",
"default": false,
"name": "Combine text sections",
"description": "Combines all text sections into one."
},
{
"id": "arm.archVersion",
"type": "choice",
"default": "auto",
"name": "Architecture version",
"description": "ARM architecture version to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "v4t",
"name": "ARMv4T (GBA)"
},
{
"value": "v5te",
"name": "ARMv5TE (DS)"
},
{
"value": "v6k",
"name": "ARMv6K (3DS)"
}
]
},
{
"id": "arm.unifiedSyntax",
"type": "boolean",
"default": false,
"name": "Unified syntax",
"description": "Disassemble as unified assembly language (UAL)."
},
{
"id": "arm.avRegisters",
"type": "boolean",
"default": false,
"name": "Use A/V registers",
"description": "Display R0-R3 as A1-A4 and R4-R11 as V1-V8."
},
{
"id": "arm.r9Usage",
"type": "choice",
"default": "generalPurpose",
"name": "Display R9 as",
"items": [
{
"value": "generalPurpose",
"name": "R9 or V6",
"description": "Use R9 as a general-purpose register."
},
{
"value": "sb",
"name": "SB (static base)",
"description": "Used for position-independent data (PID)."
},
{
"value": "tr",
"name": "TR (TLS register)",
"description": "Used for thread-local storage."
}
]
},
{
"id": "arm.slUsage",
"type": "boolean",
"default": false,
"name": "Display R10 as SL",
"description": "Used for explicit stack limits."
},
{
"id": "arm.fpUsage",
"type": "boolean",
"default": false,
"name": "Display R11 as FP",
"description": "Used for frame pointers."
},
{
"id": "arm.ipUsage",
"type": "boolean",
"default": false,
"name": "Display R12 as IP",
"description": "Used for interworking and long branches."
},
{
"id": "mips.abi",
"type": "choice",
"default": "auto",
"name": "ABI",
"description": "MIPS ABI to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "o32",
"name": "O32"
},
{
"value": "n32",
"name": "N32"
},
{
"value": "n64",
"name": "N64"
}
]
},
{
"id": "mips.instrCategory",
"type": "choice",
"default": "auto",
"name": "Instruction category",
"description": "MIPS instruction category to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "cpu",
"name": "CPU"
},
{
"value": "rsp",
"name": "RSP (N64)"
},
{
"value": "r3000gte",
"name": "R3000 GTE (PS1)"
},
{
"value": "r4000allegrex",
"name": "R4000 ALLEGREX (PSP)"
},
{
"value": "r5900",
"name": "R5900 EE (PS2)"
}
]
},
{
"id": "x86.formatter",
"type": "choice",
"default": "intel",
"name": "Format",
"description": "x86 disassembly syntax.",
"items": [
{
"value": "intel",
"name": "Intel"
},
{
"value": "gas",
"name": "AT&T"
},
{
"value": "nasm",
"name": "NASM"
},
{
"value": "masm",
"name": "MASM"
}
]
}
],
"groups": [
{
"id": "general",
"name": "General",
"properties": [
"functionRelocDiffs",
"spaceBetweenArgs",
"combineDataSections",
"combineTextSections"
]
},
{
"id": "arm",
"name": "ARM",
"properties": [
"arm.archVersion",
"arm.unifiedSyntax",
"arm.avRegisters",
"arm.r9Usage",
"arm.slUsage",
"arm.fpUsage",
"arm.ipUsage"
]
},
{
"id": "mips",
"name": "MIPS",
"properties": [
"mips.abi",
"mips.instrCategory"
]
},
{
"id": "x86",
"name": "x86",
"properties": [
"x86.formatter"
]
}
]
}
File diff suppressed because it is too large Load Diff
+11 -9
View File
@@ -21,9 +21,9 @@ enum SymbolFlag {
SYMBOL_NONE = 0;
SYMBOL_GLOBAL = 1;
SYMBOL_LOCAL = 2;
SYMBOL_WEAK = 3;
SYMBOL_COMMON = 4;
SYMBOL_HIDDEN = 5;
SYMBOL_WEAK = 4;
SYMBOL_COMMON = 8;
SYMBOL_HIDDEN = 16;
}
// A single parsed instruction
@@ -87,11 +87,11 @@ message Relocation {
}
message RelocationTarget {
Symbol symbol = 1;
uint32 symbol_index = 1;
int64 addend = 2;
}
message InstructionDiff {
message InstructionDiffRow {
DiffKind diff_kind = 1;
optional Instruction instruction = 2;
optional InstructionBranchFrom branch_from = 3;
@@ -122,10 +122,12 @@ message InstructionBranchTo {
uint32 branch_index = 2;
}
message FunctionDiff {
message SymbolDiff {
Symbol symbol = 1;
repeated InstructionDiff instructions = 2;
repeated InstructionDiffRow instruction_rows = 2;
optional float match_percent = 3;
// The symbol index in the _other_ object that this symbol was diffed against
optional uint32 target_symbol = 5;
}
message DataDiff {
@@ -140,7 +142,7 @@ message SectionDiff {
SectionKind kind = 2;
uint64 size = 3;
uint64 address = 4;
repeated FunctionDiff functions = 5;
reserved 5;
repeated DataDiff data = 6;
optional float match_percent = 7;
}
@@ -150,11 +152,11 @@ enum SectionKind {
SECTION_TEXT = 1;
SECTION_DATA = 2;
SECTION_BSS = 3;
SECTION_COMMON = 4;
}
message ObjectDiff {
repeated SectionDiff sections = 1;
repeated SymbolDiff symbols = 2;
}
message DiffResult {
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+201 -67
View File
@@ -1,19 +1,25 @@
use std::{borrow::Cow, collections::BTreeMap, ffi::CStr};
use alloc::{borrow::Cow, boxed::Box, format, string::String, vec::Vec};
use core::{ffi::CStr, fmt, fmt::Debug};
use anyhow::{bail, Result};
use byteorder::ByteOrder;
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
use object::Endian as _;
use crate::{
diff::DiffObjConfig,
obj::{ObjIns, ObjReloc, ObjSection},
diff::{
display::{ContextItem, HoverItem, InstructionPart},
DiffObjConfig,
},
obj::{
InstructionArg, Object, ParsedInstruction, RelocationFlags, ResolvedInstructionRef,
ScannedInstruction, SymbolFlagSet, SymbolKind,
},
util::ReallySigned,
};
#[cfg(feature = "arm")]
mod arm;
pub mod arm;
#[cfg(feature = "arm64")]
mod arm64;
pub mod arm64;
#[cfg(feature = "mips")]
pub mod mips;
#[cfg(feature = "ppc")]
@@ -27,18 +33,41 @@ pub enum DataType {
Int16,
Int32,
Int64,
Int128,
Float,
Double,
Bytes,
String,
}
impl fmt::Display for DataType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DataType::Int8 => write!(f, "Int8"),
DataType::Int16 => write!(f, "Int16"),
DataType::Int32 => write!(f, "Int32"),
DataType::Int64 => write!(f, "Int64"),
DataType::Float => write!(f, "Float"),
DataType::Double => write!(f, "Double"),
DataType::Bytes => write!(f, "Bytes"),
DataType::String => write!(f, "String"),
}
}
}
impl DataType {
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
pub fn display_labels(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<String> {
let mut strs = Vec::new();
for literal in self.display_literals(endian, bytes) {
strs.push(format!("{}: {}", self, literal))
}
strs
}
pub fn display_literals(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<String> {
let mut strs = Vec::new();
if self.required_len().is_some_and(|l| bytes.len() < l) {
log::warn!("Failed to display a symbol value for a symbol whose size is too small for instruction referencing it.");
return None;
return strs;
}
let mut bytes = bytes;
if self.required_len().is_some_and(|l| bytes.len() > l) {
@@ -56,58 +85,61 @@ impl DataType {
match self {
DataType::Int8 => {
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
strs.push(format!("{:#x}", i));
if i < 0 {
format!("Int8: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int8: {:#x}", i)
strs.push(format!("{:#x}", ReallySigned(i)));
}
}
DataType::Int16 => {
let i = Endian::read_i16(bytes);
let i = endian.read_i16_bytes(bytes.try_into().unwrap());
strs.push(format!("{:#x}", i));
if i < 0 {
format!("Int16: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int16: {:#x}", i)
strs.push(format!("{:#x}", ReallySigned(i)));
}
}
DataType::Int32 => {
let i = Endian::read_i32(bytes);
let i = endian.read_i32_bytes(bytes.try_into().unwrap());
strs.push(format!("{:#x}", i));
if i < 0 {
format!("Int32: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int32: {:#x}", i)
strs.push(format!("{:#x}", ReallySigned(i)));
}
}
DataType::Int64 => {
let i = Endian::read_i64(bytes);
let i = endian.read_i64_bytes(bytes.try_into().unwrap());
strs.push(format!("{:#x}", i));
if i < 0 {
format!("Int64: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int64: {:#x}", i)
}
}
DataType::Int128 => {
let i = Endian::read_i128(bytes);
if i < 0 {
format!("Int128: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int128: {:#x}", i)
strs.push(format!("{:#x}", ReallySigned(i)));
}
}
DataType::Float => {
format!("Float: {:?}f", Endian::read_f32(bytes))
let bytes: [u8; 4] = bytes.try_into().unwrap();
strs.push(format!("{:?}f", match endian {
object::Endianness::Little => f32::from_le_bytes(bytes),
object::Endianness::Big => f32::from_be_bytes(bytes),
}));
}
DataType::Double => {
format!("Double: {:?}", Endian::read_f64(bytes))
let bytes: [u8; 8] = bytes.try_into().unwrap();
strs.push(format!("{:?}f", match endian {
object::Endianness::Little => f64::from_le_bytes(bytes),
object::Endianness::Big => f64::from_be_bytes(bytes),
}));
}
DataType::Bytes => {
format!("Bytes: {:#?}", bytes)
strs.push(format!("{:#?}", bytes));
}
DataType::String => {
format!("String: {:?}", CStr::from_bytes_until_nul(bytes).ok()?)
if let Ok(cstr) = CStr::from_bytes_until_nul(bytes) {
strs.push(format!("{:?}", cstr));
}
}
}
.into()
strs
}
fn required_len(&self) -> Option<usize> {
@@ -116,7 +148,6 @@ impl DataType {
DataType::Int16 => Some(2),
DataType::Int32 => Some(4),
DataType::Int64 => Some(8),
DataType::Int128 => Some(16),
DataType::Float => Some(4),
DataType::Double => Some(8),
DataType::Bytes => None,
@@ -125,59 +156,162 @@ impl DataType {
}
}
pub trait ObjArch: Send + Sync {
fn process_code(
pub trait Arch: Send + Sync + Debug {
/// Generate a list of instructions references (offset, size, opcode) from the given code.
///
/// The opcode IDs are used to generate the initial diff. Implementations should do as little
/// parsing as possible here: just enough to identify the base instruction opcode, size, and
/// possible branch destination (for visual representation). As needed, instructions are parsed
/// via `process_instruction` to compare their arguments.
fn scan_instructions(
&self,
address: u64,
code: &[u8],
section_index: usize,
relocations: &[ObjReloc],
line_info: &BTreeMap<u64, u32>,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult>;
diff_config: &DiffObjConfig,
) -> Result<Vec<ScannedInstruction>>;
/// Parse an instruction to gather its mnemonic and arguments for more detailed comparison.
///
/// This is called only when we need to compare the arguments of an instruction.
fn process_instruction(
&self,
resolved: ResolvedInstructionRef,
diff_config: &DiffObjConfig,
) -> Result<ParsedInstruction> {
let mut mnemonic = None;
let mut args = Vec::with_capacity(8);
self.display_instruction(resolved, diff_config, &mut |part| {
match part {
InstructionPart::Opcode(m, _) => mnemonic = Some(Cow::Owned(m.into_owned())),
InstructionPart::Arg(arg) => args.push(arg.into_static()),
_ => {}
}
Ok(())
})?;
// If the instruction has a relocation, but we didn't format it in the display, add it to
// the end of the arguments list.
if resolved.relocation.is_some() && !args.contains(&InstructionArg::Reloc) {
args.push(InstructionArg::Reloc);
}
Ok(ParsedInstruction {
ins_ref: resolved.ins_ref,
mnemonic: mnemonic.unwrap_or_default(),
args,
})
}
/// Format an instruction for display.
///
/// Implementations should call the callback for each part of the instruction: usually the
/// mnemonic and arguments, plus any separators and visual formatting.
fn display_instruction(
&self,
resolved: ResolvedInstructionRef,
diff_config: &DiffObjConfig,
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
) -> Result<()>;
fn implcit_addend(
&self,
file: &File<'_>,
section: &ObjSection,
file: &object::File<'_>,
section: &object::Section,
address: u64,
reloc: &Relocation,
relocation: &object::Relocation,
flags: RelocationFlags,
) -> Result<i64>;
fn demangle(&self, _name: &str) -> Option<String> { None }
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
fn reloc_name(&self, _flags: RelocationFlags) -> Option<&'static str> { None }
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
fn data_reloc_size(&self, flags: RelocationFlags) -> usize;
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
fn symbol_address(&self, address: u64, _kind: SymbolKind) -> u64 { address }
fn display_data_type(&self, _ty: DataType, bytes: &[u8]) -> Option<String> {
Some(format!("Bytes: {:#x?}", bytes))
fn extra_symbol_flags(&self, _symbol: &object::Symbol) -> SymbolFlagSet {
SymbolFlagSet::default()
}
// Downcast methods
#[cfg(feature = "ppc")]
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }
fn guess_data_type(&self, _resolved: ResolvedInstructionRef) -> Option<DataType> { None }
fn symbol_hover(&self, _obj: &Object, _symbol_index: usize) -> Vec<HoverItem> { Vec::new() }
fn symbol_context(&self, _obj: &Object, _symbol_index: usize) -> Vec<ContextItem> { Vec::new() }
fn instruction_hover(
&self,
_obj: &Object,
_resolved: ResolvedInstructionRef,
) -> Vec<HoverItem> {
Vec::new()
}
fn instruction_context(
&self,
_obj: &Object,
_resolved: ResolvedInstructionRef,
) -> Vec<ContextItem> {
Vec::new()
}
}
pub struct ProcessCodeResult {
pub ops: Vec<u16>,
pub insts: Vec<ObjIns>,
}
pub fn new_arch(object: &File) -> Result<Box<dyn ObjArch>> {
pub fn new_arch(object: &object::File) -> Result<Box<dyn Arch>> {
use object::Object as _;
Ok(match object.architecture() {
#[cfg(feature = "ppc")]
Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?),
object::Architecture::PowerPc => Box::new(ppc::ArchPpc::new(object)?),
#[cfg(feature = "mips")]
Architecture::Mips => Box::new(mips::ObjArchMips::new(object)?),
object::Architecture::Mips => Box::new(mips::ArchMips::new(object)?),
#[cfg(feature = "x86")]
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
object::Architecture::I386 | object::Architecture::X86_64 => {
Box::new(x86::ArchX86::new(object)?)
}
#[cfg(feature = "arm")]
Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?),
object::Architecture::Arm => Box::new(arm::ArchArm::new(object)?),
#[cfg(feature = "arm64")]
Architecture::Aarch64 => Box::new(arm64::ObjArchArm64::new(object)?),
object::Architecture::Aarch64 => Box::new(arm64::ArchArm64::new(object)?),
arch => bail!("Unsupported architecture: {arch:?}"),
})
}
#[derive(Debug, Default)]
pub struct ArchDummy {}
impl ArchDummy {
pub fn new() -> Box<Self> { Box::new(Self {}) }
}
impl Arch for ArchDummy {
fn scan_instructions(
&self,
_address: u64,
_code: &[u8],
_section_index: usize,
_diff_config: &DiffObjConfig,
) -> Result<Vec<ScannedInstruction>> {
Ok(Vec::new())
}
fn display_instruction(
&self,
_resolved: ResolvedInstructionRef,
_diff_config: &DiffObjConfig,
_cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
) -> Result<()> {
Ok(())
}
fn implcit_addend(
&self,
_file: &object::File<'_>,
_section: &object::Section,
_address: u64,
_relocation: &object::Relocation,
_flags: RelocationFlags,
) -> Result<i64> {
Ok(0)
}
fn data_reloc_size(&self, _flags: RelocationFlags) -> usize { 0 }
}

Some files were not shown because too many files have changed in this diff Show More