mirror of
https://github.com/encounter/objdiff.git
synced 2026-07-10 12:18:36 -07:00
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8250d26b77 | |||
| fd555a6e0f | |||
| 3710b6a91e | |||
| faebddbc5e | |||
| a733a950a3 | |||
| cad9b70632 | |||
| cf937b0be9 | |||
| 23b6d33a98 | |||
| f17ee83622 | |||
| 615ec4c50a | |||
| 2cc10b0d06 | |||
| 8091941448 | |||
| de74dfdba7 | |||
| 177bd5e895 | |||
| e1ccee1e73 | |||
| 952b6a63c3 | |||
| 09cc9952df | |||
| fc598af329 | |||
| 871407622d | |||
| e3fff7b0dc | |||
| 75b0e7d9e5 | |||
| 9f71ce9fea | |||
| d9fb48853e | |||
| 233839346a | |||
| 95615c2ec5 | |||
| 97981160f4 | |||
| 1fd901a863 | |||
| 759d55994a | |||
| 9710ccc38a | |||
| 79cd460333 | |||
| a5a6a3928e | |||
| fc54e93681 | |||
| c9b11db2fa | |||
| b991960080 | |||
| 425dc8546b | |||
| 9e04357d9f | |||
| 6037c12ad0 | |||
| b15f643713 | |||
| 3f82c1a50f | |||
| 0ea6242669 | |||
| 0c20a0d9cd | |||
| f30b3cfae2 | |||
| 9e57a66a05 | |||
| e254af5acf | |||
| 320efcb8cb | |||
| 7148b51fe0 | |||
| dc0c170db9 | |||
| 31e9c14681 | |||
| 94f1f07b00 | |||
| f5b5a612fc | |||
| 22a24f37f5 | |||
| 854dc9e4f5 | |||
| 5bfaaaaf65 | |||
| 8b36fa4fc6 | |||
| 660e6c879e | |||
| db726a68a6 | |||
| b457453639 | |||
| 3e5008524e | |||
| 2c46286aff | |||
| 106652ae7d | |||
| 30d14870ef | |||
| e7991cb28d | |||
| 4dfc28fc68 | |||
| 3c74b89f15 | |||
| 1343f4fd2b | |||
| 9df98f263e | |||
| bbe49eb8b4 | |||
| aecb078b2a | |||
| a5668b484b | |||
| ef41e393d4 | |||
| 20e42a499a | |||
| c39795ae2c | |||
| 49ee9b44aa | |||
| 341c1d4b33 | |||
| 9f4a1e86cd | |||
| ed5d092b11 | |||
| 023dd7a55b | |||
| 3b1249e1ab | |||
| cb13638e07 | |||
| 37ddbb7f4a | |||
| b80d361e91 | |||
| fd27f4d0cd | |||
| 5cfd04fd4f | |||
| 5b9ac93c08 | |||
| 39a13f4d36 | |||
| 28348606bf | |||
| fb24063c54 | |||
| cff6a230a3 | |||
| 9a7d2bcebf | |||
| 4eba5f71b0 | |||
| 0a85c498c5 | |||
| c2fcf2797b | |||
| e88a58ba39 | |||
| 02f521a528 | |||
| 197d1247a8 | |||
| eef9598e76 | |||
| 405a2a82db | |||
| 4cdad8a519 | |||
| b74a49ed0c | |||
| e1079db93a | |||
| 879e03eed5 | |||
| 53e6e0c7c4 | |||
| 67cea2a8d9 | |||
| e4f97adbdd | |||
| 0ec7bf078b | |||
| 74e89130a8 | |||
| 236e4d8d26 | |||
| b900ae5a00 | |||
| 261e1b8e07 | |||
| a29e913b45 | |||
| 49257dc73c | |||
| dc9eec66b0 | |||
| 7b58f9a269 | |||
| d9e7dacb6d | |||
| 04b4fdcd21 | |||
| 803eaafee6 | |||
| e1dc84698f | |||
| e68629c339 | |||
| bb9ff4b928 | |||
| 57392daaeb |
@@ -9,7 +9,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_BIN_NAME: objdiff
|
||||
BUILD_PROFILE: release-lto
|
||||
CARGO_TARGET_DIR: target
|
||||
|
||||
jobs:
|
||||
@@ -24,15 +24,39 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install libgtk-3-dev
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
components: clippy
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.4
|
||||
- name: Cargo check
|
||||
run: cargo check --all-features
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: cargo check
|
||||
- name: Cargo clippy
|
||||
run: cargo clippy --all-features
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: cargo clippy
|
||||
|
||||
fmt:
|
||||
name: Format
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
# We use nightly options in rustfmt.toml
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: Cargo fmt
|
||||
run: cargo fmt --all --check
|
||||
|
||||
deny:
|
||||
name: Deny
|
||||
@@ -45,13 +69,14 @@ jobs:
|
||||
# Prevent new advisories from failing CI
|
||||
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
with:
|
||||
command: check ${{ matrix.checks }}
|
||||
|
||||
test:
|
||||
name: Test
|
||||
if: 'false' # No tests yet
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
@@ -64,11 +89,16 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install libgtk-3-dev
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.4
|
||||
- name: Cargo test
|
||||
run: cargo test --release --all-features
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: cargo test --release
|
||||
|
||||
build:
|
||||
name: Build
|
||||
@@ -79,15 +109,19 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
name: linux-x86_64
|
||||
packages: libgtk-3-dev
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
name: windows-x86_64
|
||||
features: default
|
||||
- platform: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
name: macos-x86_64
|
||||
features: default
|
||||
- platform: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
name: macos-arm64
|
||||
features: default
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
@@ -97,22 +131,29 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install ${{ matrix.packages }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.4
|
||||
- name: Cargo build
|
||||
run: cargo build --release --all-features --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }}
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: >
|
||||
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
--bin objdiff-cli --bin objdiff --features ${{ matrix.features }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
path: |
|
||||
${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }}.exe
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }}.exe
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff-cli
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff-cli.exe
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff.exe
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
@@ -120,17 +161,31 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build ]
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Rename artifacts
|
||||
working-directory: artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir ../out
|
||||
for i in */*/release/$CARGO_BIN_NAME*; do
|
||||
mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/release\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")"
|
||||
for dir in */; do
|
||||
for file in "$dir"*; do
|
||||
base=$(basename "$file")
|
||||
name="${base%.*}"
|
||||
ext="${base##*.}"
|
||||
if [ "$ext" = "$base" ]; then
|
||||
ext=""
|
||||
else
|
||||
ext=".$ext"
|
||||
fi
|
||||
dst="../out/${name}-${dir%/}${ext}"
|
||||
mv "$file" "$dst"
|
||||
done
|
||||
done
|
||||
ls -R ../out
|
||||
- name: Release
|
||||
|
||||
@@ -22,3 +22,4 @@ android.keystore
|
||||
*.frag
|
||||
*.vert
|
||||
*.metal
|
||||
.vscode/launch.json
|
||||
|
||||
Generated
+2361
-1461
File diff suppressed because it is too large
Load Diff
+9
-84
@@ -1,87 +1,12 @@
|
||||
[package]
|
||||
name = "objdiff"
|
||||
version = "0.4.4"
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/objdiff"
|
||||
readme = "README.md"
|
||||
description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
publish = false
|
||||
build = "build.rs"
|
||||
[workspace]
|
||||
members = [
|
||||
"objdiff-cli",
|
||||
"objdiff-core",
|
||||
"objdiff-gui",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release]
|
||||
[profile.release-lto]
|
||||
inherits = "release"
|
||||
lto = "thin"
|
||||
strip = "debuginfo"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
wgpu = ["eframe/wgpu"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.71"
|
||||
byteorder = "1.4.3"
|
||||
bytes = "1.4.0"
|
||||
cfg-if = "1.0.0"
|
||||
const_format = "0.2.31"
|
||||
cwdemangle = "0.1.6"
|
||||
dirs = "5.0.1"
|
||||
eframe = { version = "0.22.0", features = ["persistence"] }
|
||||
egui = "0.22.0"
|
||||
egui_extras = "0.22.0"
|
||||
flagset = "0.4.3"
|
||||
globset = { version = "0.4.13", features = ["serde1"] }
|
||||
log = "0.4.19"
|
||||
memmap2 = "0.7.1"
|
||||
notify = "6.0.1"
|
||||
object = { version = "0.31.1", features = ["read_core", "std", "elf"], default-features = false }
|
||||
png = "0.17.9"
|
||||
ppc750cl = { git = "https://github.com/terorie/ppc750cl", rev = "9ae36eef34aa6d74e00972c7671f547a2acfd0aa" }
|
||||
rabbitizer = "1.7.4"
|
||||
rfd = { version = "0.11.4" } #, default-features = false, features = ['xdg-portal']
|
||||
ron = "0.8.0"
|
||||
semver = "1.0.17"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.104"
|
||||
serde_yaml = "0.9.25"
|
||||
tempfile = "3.6.0"
|
||||
thiserror = "1.0.41"
|
||||
time = { version = "0.3.22", features = ["formatting", "local-offset"] }
|
||||
toml = "0.7.6"
|
||||
twox-hash = "1.6.3"
|
||||
|
||||
# For Linux static binaries, use rustls
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
reqwest = { version = "0.11.18", default-features = false, features = ["blocking", "json", "rustls"] }
|
||||
self_update = { version = "0.37.0", default-features = false, features = ["rustls"] }
|
||||
|
||||
# For all other platforms, use native TLS
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
reqwest = "0.11.18"
|
||||
self_update = "0.37.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
path-slash = "0.2.1"
|
||||
winapi = "0.3.9"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1.12"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
exec = "0.3.1"
|
||||
|
||||
# native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
# web:
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
tracing-wasm = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.71"
|
||||
vergen = { version = "8.2.4", features = ["build", "cargo", "git", "gitcl"] }
|
||||
|
||||
@@ -3,11 +3,21 @@
|
||||
[Build Status]: https://github.com/encounter/objdiff/actions/workflows/build.yaml/badge.svg
|
||||
[actions]: https://github.com/encounter/objdiff/actions
|
||||
|
||||
A local diffing tool for decompilation projects.
|
||||
A local diffing tool for decompilation projects. Inspired by [decomp.me](https://decomp.me) and [asm-differ](https://github.com/simonlindholm/asm-differ).
|
||||
|
||||
Features:
|
||||
- Compare entire object files: functions and data.
|
||||
- Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC)
|
||||
- Automatic rebuild on source file changes.
|
||||
- Project integration via [configuration file](#configuration).
|
||||
- Search and filter all of a project's objects and quickly switch.
|
||||
- Click to highlight all instances of values and registers.
|
||||
|
||||
Supports:
|
||||
- PowerPC 750CL (GameCube & Wii)
|
||||
- MIPS (Nintendo 64)
|
||||
- PowerPC 750CL (GameCube, Wii)
|
||||
- MIPS (N64, PS1, PS2, PSP)
|
||||
- x86 (COFF only at the moment)
|
||||
- ARM (GBA, DS, 3DS)
|
||||
|
||||
See [Usage](#usage) for more information.
|
||||
|
||||
@@ -50,6 +60,10 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
|
||||
// objdiff.json
|
||||
{
|
||||
"custom_make": "ninja",
|
||||
"custom_args": [
|
||||
"-d",
|
||||
"keeprsp"
|
||||
],
|
||||
|
||||
// Only required if objects use "path" instead of "target_path" and "base_path".
|
||||
"target_dir": "build/asm",
|
||||
@@ -83,7 +97,10 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
|
||||
```
|
||||
|
||||
`custom_make` _(optional)_: By default, objdiff will use `make` to build the project.
|
||||
If the project uses a different build system (e.g. `ninja`), specify it here.
|
||||
If the project uses a different build system (e.g. `ninja`), specify it here.
|
||||
The build command will be `[custom_make] [custom_args] path/to/object.o`.
|
||||
|
||||
`custom_args` _(optional)_: Additional arguments to pass to the build command prior to the object path.
|
||||
|
||||
`target_dir` _(optional)_: Relative from the root of the project, this where the "target" or "expected" objects are located.
|
||||
These are the **intended result** of the match.
|
||||
@@ -118,7 +135,17 @@ If not specified, objdiff will use the default patterns listed above.
|
||||
> `reverse_fn_order` _(optional)_: Displays function symbols in reversed order.
|
||||
Used to support MWCC's `-inline deferred` option, which reverses the order of functions in the object file.
|
||||
|
||||
## Building
|
||||
|
||||
Install Rust via [rustup](https://rustup.rs).
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/encounter/objdiff.git
|
||||
$ cd objdiff
|
||||
$ cargo run --release
|
||||
# or, for wgpu backend (recommended on macOS)
|
||||
$ cargo run --release --features wgpu
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -47,13 +47,7 @@ yanked = "warn"
|
||||
notice = "warn"
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
"RUSTSEC-2023-0022",
|
||||
"RUSTSEC-2023-0023",
|
||||
"RUSTSEC-2023-0024",
|
||||
"RUSTSEC-2023-0034",
|
||||
"RUSTSEC-2023-0044",
|
||||
]
|
||||
ignore = []
|
||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||
# lower than the range specified will be ignored. Note that ignored advisories
|
||||
# will still output a note when they are encountered.
|
||||
@@ -76,6 +70,7 @@ unlicensed = "deny"
|
||||
allow = [
|
||||
"MIT",
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"ISC",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
@@ -159,7 +154,7 @@ registries = [
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "warn"
|
||||
multiple-versions = "allow"
|
||||
# Lint level for when a crate version requirement is `*`
|
||||
wildcards = "allow"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "objdiff-cli"
|
||||
version = "2.0.0-beta.4"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/objdiff"
|
||||
readme = "../README.md"
|
||||
description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
publish = false
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
argp = "0.3.0"
|
||||
crossterm = "0.27.0"
|
||||
enable-ansi-support = "0.2.1"
|
||||
memmap2 = "0.9.4"
|
||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||
pbjson = "0.7.0"
|
||||
prost = "0.13.1"
|
||||
ratatui = "0.26.2"
|
||||
rayon = "1.10.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
supports-color = "3.0.0"
|
||||
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.13.1"
|
||||
pbjson-build = "0.7.0"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.expect("Failed to execute git");
|
||||
let rev = String::from_utf8(output.stdout).expect("Failed to parse git output");
|
||||
println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}");
|
||||
println!("cargo:rustc-rerun-if-changed=.git/HEAD");
|
||||
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
|
||||
let descriptor_path = root.join("proto_descriptor.bin");
|
||||
println!("cargo:rerun-if-changed={}", descriptor_path.display());
|
||||
let descriptor_mtime = std::fs::metadata(&descriptor_path)
|
||||
.map(|m| m.modified().unwrap())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
|
||||
let mut run_protoc = false;
|
||||
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) {
|
||||
Ok(m) => m.modified().unwrap(),
|
||||
Err(e) => panic!("Failed to stat proto file {}: {:?}", proto_file.display(), e),
|
||||
};
|
||||
if mtime > descriptor_mtime {
|
||||
run_protoc = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn prost_config(descriptor_path: &Path, run_protoc: bool) -> prost_build::Config {
|
||||
let mut config = prost_build::Config::new();
|
||||
config.file_descriptor_set_path(descriptor_path);
|
||||
// If our cached descriptor is up-to-date, we don't need to run protoc.
|
||||
// This is helpful so that users don't need to have protoc installed
|
||||
// unless they're updating the protos.
|
||||
if !run_protoc {
|
||||
config.skip_protoc_run();
|
||||
}
|
||||
config
|
||||
}
|
||||
if let Err(e) =
|
||||
prost_config(&descriptor_path, run_protoc).compile_protos(&proto_files, &[root.as_path()])
|
||||
{
|
||||
if e.kind() == std::io::ErrorKind::NotFound && e.to_string().contains("protoc") {
|
||||
eprintln!("protoc not found, skipping protobuf compilation");
|
||||
prost_config(&descriptor_path, false)
|
||||
.compile_protos(&proto_files, &[root.as_path()])
|
||||
.expect("Failed to compile protos");
|
||||
} else {
|
||||
panic!("Failed to compile protos: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,135 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package objdiff.report;
|
||||
|
||||
// Progress info for a report or unit
|
||||
message Measures {
|
||||
// Overall match percent, including partially matched functions and data
|
||||
float fuzzy_match_percent = 1;
|
||||
// Total size of code in bytes
|
||||
uint64 total_code = 2;
|
||||
// Fully matched code size in bytes
|
||||
uint64 matched_code = 3;
|
||||
// Fully matched code percent
|
||||
float matched_code_percent = 4;
|
||||
// Total size of data in bytes
|
||||
uint64 total_data = 5;
|
||||
// Fully matched data size in bytes
|
||||
uint64 matched_data = 6;
|
||||
// Fully matched data percent
|
||||
float matched_data_percent = 7;
|
||||
// Total number of functions
|
||||
uint32 total_functions = 8;
|
||||
// Fully matched functions
|
||||
uint32 matched_functions = 9;
|
||||
// Fully matched functions percent
|
||||
float matched_functions_percent = 10;
|
||||
}
|
||||
|
||||
// Project progress report
|
||||
message Report {
|
||||
// Overall progress info
|
||||
Measures measures = 1;
|
||||
// Units within this report
|
||||
repeated ReportUnit units = 2;
|
||||
}
|
||||
|
||||
// A unit of the report (usually a translation unit)
|
||||
message ReportUnit {
|
||||
// The name of the unit
|
||||
string name = 1;
|
||||
// Progress info for this unit
|
||||
Measures measures = 2;
|
||||
// Sections within this unit
|
||||
repeated ReportItem sections = 3;
|
||||
// Functions within this unit
|
||||
repeated ReportItem functions = 4;
|
||||
// Extra metadata for this unit
|
||||
optional ReportUnitMetadata metadata = 5;
|
||||
}
|
||||
|
||||
// Extra metadata for a unit
|
||||
message ReportUnitMetadata {
|
||||
// Whether this unit is marked as complete (or "linked")
|
||||
optional bool complete = 1;
|
||||
// The name of the module this unit belongs to
|
||||
optional string module_name = 2;
|
||||
// The ID of the module this unit belongs to
|
||||
optional uint32 module_id = 3;
|
||||
// The path to the source file of this unit
|
||||
optional string source_path = 4;
|
||||
}
|
||||
|
||||
// A section or function within a unit
|
||||
message ReportItem {
|
||||
// The name of the item
|
||||
string name = 1;
|
||||
// The size of the item in bytes
|
||||
uint64 size = 2;
|
||||
// The overall match percent for this item
|
||||
float fuzzy_match_percent = 3;
|
||||
// Extra metadata for this item
|
||||
optional ReportItemMetadata metadata = 4;
|
||||
}
|
||||
|
||||
// Extra metadata for an item
|
||||
message ReportItemMetadata {
|
||||
// The demangled name of the function
|
||||
optional string demangled_name = 1;
|
||||
// The virtual address of the function or section
|
||||
optional uint64 virtual_address = 2;
|
||||
}
|
||||
|
||||
// A pair of reports to compare and generate changes
|
||||
message ChangesInput {
|
||||
// The previous report
|
||||
Report from = 1;
|
||||
// The current report
|
||||
Report to = 2;
|
||||
}
|
||||
|
||||
// Changes between two reports
|
||||
message Changes {
|
||||
// The progress info for the previous report
|
||||
Measures from = 1;
|
||||
// The progress info for the current report
|
||||
Measures to = 2;
|
||||
// Units that changed
|
||||
repeated ChangeUnit units = 3;
|
||||
}
|
||||
|
||||
// A changed unit
|
||||
message ChangeUnit {
|
||||
// The name of the unit
|
||||
string name = 1;
|
||||
// The previous progress info (omitted if new)
|
||||
optional Measures from = 2;
|
||||
// The current progress info (omitted if removed)
|
||||
optional Measures to = 3;
|
||||
// Sections that changed
|
||||
repeated ChangeItem sections = 4;
|
||||
// Functions that changed
|
||||
repeated ChangeItem functions = 5;
|
||||
// Extra metadata for this unit
|
||||
optional ReportUnitMetadata metadata = 6;
|
||||
}
|
||||
|
||||
// A changed section or function
|
||||
message ChangeItem {
|
||||
// The name of the item
|
||||
string name = 1;
|
||||
// The previous progress info (omitted if new)
|
||||
optional ChangeItemInfo from = 2;
|
||||
// The current progress info (omitted if removed)
|
||||
optional ChangeItemInfo to = 3;
|
||||
// Extra metadata for this item
|
||||
optional ReportItemMetadata metadata = 4;
|
||||
}
|
||||
|
||||
// Progress info for a section or function
|
||||
message ChangeItemInfo {
|
||||
// The overall match percent for this item
|
||||
float fuzzy_match_percent = 1;
|
||||
// The size of the item in bytes
|
||||
uint64 size = 2;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Originally from https://gist.github.com/suluke/e0c672492126be0a4f3b4f0e1115d77c
|
||||
//! Extend `argp` to be better integrated with the `cargo` ecosystem
|
||||
//!
|
||||
//! For now, this only adds a --version/-V option which causes early-exit.
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use argp::{parser::ParseGlobalOptions, EarlyExit, FromArgs, TopLevelCommand};
|
||||
|
||||
struct ArgsOrVersion<T>(T)
|
||||
where T: FromArgs;
|
||||
|
||||
impl<T> TopLevelCommand for ArgsOrVersion<T> where T: FromArgs {}
|
||||
|
||||
impl<T> FromArgs for ArgsOrVersion<T>
|
||||
where T: FromArgs
|
||||
{
|
||||
fn _from_args(
|
||||
command_name: &[&str],
|
||||
args: &[&OsStr],
|
||||
parent: Option<&mut dyn ParseGlobalOptions>,
|
||||
) -> Result<Self, EarlyExit> {
|
||||
/// Also use argp for catching `--version`-only invocations
|
||||
#[derive(FromArgs)]
|
||||
struct Version {
|
||||
/// Print version information and exit.
|
||||
#[argp(switch, short = 'V')]
|
||||
pub version: bool,
|
||||
}
|
||||
|
||||
match Version::from_args(command_name, args) {
|
||||
Ok(v) => {
|
||||
if v.version {
|
||||
println!(
|
||||
"{} {} {}",
|
||||
command_name.first().unwrap_or(&""),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
env!("GIT_COMMIT_SHA"),
|
||||
);
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
// Pass through empty arguments
|
||||
T::_from_args(command_name, args, parent).map(Self)
|
||||
}
|
||||
}
|
||||
Err(exit) => match exit {
|
||||
EarlyExit::Help(_help) => {
|
||||
// TODO: Chain help info from Version
|
||||
// For now, we just put the switch on T as well
|
||||
T::from_args(command_name, &["--help"]).map(Self)
|
||||
}
|
||||
EarlyExit::Err(_) => T::_from_args(command_name, args, parent).map(Self),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `FromArgs` type from the current process’s `env::args`.
|
||||
///
|
||||
/// This function will exit early from the current process if argument parsing was unsuccessful or if information like `--help` was requested.
|
||||
/// Error messages will be printed to stderr, and `--help` output to stdout.
|
||||
pub fn from_env<T>() -> T
|
||||
where T: TopLevelCommand {
|
||||
argp::parse_args_or_exit::<ArgsOrVersion<T>>(argp::DEFAULT).0
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
pub mod diff;
|
||||
pub mod report;
|
||||
@@ -0,0 +1,471 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{BufWriter, Read, Write},
|
||||
ops::DerefMut,
|
||||
path::{Path, PathBuf},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use argp::FromArgs;
|
||||
use objdiff_core::{
|
||||
config::ProjectObject,
|
||||
diff, obj,
|
||||
obj::{ObjSectionKind, ObjSymbolFlags},
|
||||
};
|
||||
use prost::Message;
|
||||
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::util::report::{
|
||||
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report, ReportItem,
|
||||
ReportItemMetadata, ReportUnit, ReportUnitMetadata,
|
||||
};
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Commands for processing NVIDIA Shield TV alf files.
|
||||
#[argp(subcommand, name = "report")]
|
||||
pub struct Args {
|
||||
#[argp(subcommand)]
|
||||
command: SubCommand,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argp(subcommand)]
|
||||
pub enum SubCommand {
|
||||
Generate(GenerateArgs),
|
||||
Changes(ChangesArgs),
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Generate a report from a project.
|
||||
#[argp(subcommand, name = "generate")]
|
||||
pub struct GenerateArgs {
|
||||
#[argp(option, short = 'p')]
|
||||
/// Project directory
|
||||
project: Option<PathBuf>,
|
||||
#[argp(option, short = 'o')]
|
||||
/// Output file
|
||||
output: Option<PathBuf>,
|
||||
#[argp(switch, short = 'd')]
|
||||
/// Deduplicate global and weak symbols (runs single-threaded)
|
||||
deduplicate: bool,
|
||||
#[argp(option, short = 'f')]
|
||||
/// Output format (json or proto, default json)
|
||||
format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// List any changes from a previous report.
|
||||
#[argp(subcommand, name = "changes")]
|
||||
pub struct ChangesArgs {
|
||||
#[argp(positional)]
|
||||
/// Previous report file
|
||||
previous: PathBuf,
|
||||
#[argp(positional)]
|
||||
/// Current report file
|
||||
current: PathBuf,
|
||||
#[argp(option, short = 'o')]
|
||||
/// Output file
|
||||
output: Option<PathBuf>,
|
||||
#[argp(option, short = 'f')]
|
||||
/// Output format (json or proto, default json)
|
||||
format: Option<String>,
|
||||
}
|
||||
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
match args.command {
|
||||
SubCommand::Generate(args) => generate(args),
|
||||
SubCommand::Changes(args) => changes(args),
|
||||
}
|
||||
}
|
||||
|
||||
enum OutputFormat {
|
||||
Json,
|
||||
Proto,
|
||||
}
|
||||
|
||||
impl OutputFormat {
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"json" => Ok(Self::Json),
|
||||
"binpb" | "pb" | "proto" | "protobuf" => Ok(Self::Proto),
|
||||
_ => bail!("Invalid output format: {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate(args: GenerateArgs) -> Result<()> {
|
||||
let output_format = if let Some(format) = &args.format {
|
||||
OutputFormat::from_str(format)?
|
||||
} else {
|
||||
OutputFormat::Json
|
||||
};
|
||||
|
||||
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
|
||||
info!("Loading project {}", project_dir.display());
|
||||
|
||||
let config = objdiff_core::config::try_project_config(project_dir);
|
||||
let Some((Ok(mut project), _)) = config else {
|
||||
bail!("No project configuration found");
|
||||
};
|
||||
info!(
|
||||
"Generating report for {} units (using {} threads)",
|
||||
project.objects.len(),
|
||||
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
||||
);
|
||||
|
||||
let start = Instant::now();
|
||||
let mut units = vec![];
|
||||
let mut existing_functions: HashSet<String> = HashSet::new();
|
||||
if args.deduplicate {
|
||||
// If deduplicating, we need to run single-threaded
|
||||
for object in &mut project.objects {
|
||||
if let Some(unit) = report_object(
|
||||
object,
|
||||
project_dir,
|
||||
project.target_dir.as_deref(),
|
||||
project.base_dir.as_deref(),
|
||||
Some(&mut existing_functions),
|
||||
)? {
|
||||
units.push(unit);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let vec = project
|
||||
.objects
|
||||
.par_iter_mut()
|
||||
.map(|object| {
|
||||
report_object(
|
||||
object,
|
||||
project_dir,
|
||||
project.target_dir.as_deref(),
|
||||
project.base_dir.as_deref(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<Option<ReportUnit>>>>()?;
|
||||
units = vec.into_iter().flatten().collect();
|
||||
}
|
||||
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
|
||||
let report = Report { measures: Some(measures), units };
|
||||
let duration = start.elapsed();
|
||||
info!("Report generated in {}.{:03}s", duration.as_secs(), duration.subsec_millis());
|
||||
write_output(&report, args.output.as_deref(), output_format)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
|
||||
where T: serde::Serialize + prost::Message {
|
||||
if let Some(output) = output {
|
||||
info!("Writing to {}", output.display());
|
||||
let file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(output)
|
||||
.with_context(|| format!("Failed to create file {}", output.display()))?;
|
||||
match format {
|
||||
OutputFormat::Json => {
|
||||
let mut output = BufWriter::new(file);
|
||||
serde_json::to_writer_pretty(&mut output, input)
|
||||
.context("Failed to write output file")?;
|
||||
output.flush().context("Failed to flush output file")?;
|
||||
}
|
||||
OutputFormat::Proto => {
|
||||
file.set_len(input.encoded_len() as u64)?;
|
||||
let map =
|
||||
unsafe { memmap2::Mmap::map(&file) }.context("Failed to map output file")?;
|
||||
let mut output = map.make_mut().context("Failed to remap output file")?;
|
||||
input.encode(&mut output.deref_mut()).context("Failed to encode output")?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match format {
|
||||
OutputFormat::Json => {
|
||||
serde_json::to_writer_pretty(std::io::stdout(), input)?;
|
||||
}
|
||||
OutputFormat::Proto => {
|
||||
std::io::stdout().write_all(&input.encode_to_vec())?;
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn report_object(
|
||||
object: &mut ProjectObject,
|
||||
project_dir: &Path,
|
||||
target_dir: Option<&Path>,
|
||||
base_dir: Option<&Path>,
|
||||
mut existing_functions: Option<&mut HashSet<String>>,
|
||||
) -> Result<Option<ReportUnit>> {
|
||||
object.resolve_paths(project_dir, target_dir, base_dir);
|
||||
match (&object.target_path, &object.base_path) {
|
||||
(None, Some(_)) if object.complete != Some(true) => {
|
||||
warn!("Skipping object without target: {}", object.name());
|
||||
return Ok(None);
|
||||
}
|
||||
(None, None) => {
|
||||
warn!("Skipping object without target or base: {}", object.name());
|
||||
return Ok(None);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
|
||||
let target = object
|
||||
.target_path
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
let base = object
|
||||
.base_path
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
|
||||
|
||||
let metadata = ReportUnitMetadata {
|
||||
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),
|
||||
source_path: None, // TODO
|
||||
};
|
||||
let mut measures = Measures::default();
|
||||
let mut sections = vec![];
|
||||
let mut functions = vec![];
|
||||
|
||||
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) {
|
||||
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 == Some(true) {
|
||||
100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
});
|
||||
sections.push(ReportItem {
|
||||
name: section.name.clone(),
|
||||
fuzzy_match_percent: section_match_percent,
|
||||
size: section.size,
|
||||
metadata: Some(ReportItemMetadata {
|
||||
demangled_name: None,
|
||||
virtual_address: section.virtual_address,
|
||||
}),
|
||||
});
|
||||
|
||||
match section.kind {
|
||||
ObjSectionKind::Data | ObjSectionKind::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(§ion_diff.symbols) {
|
||||
if symbol.size == 0 {
|
||||
continue;
|
||||
}
|
||||
if let Some(existing_functions) = &mut existing_functions {
|
||||
if (symbol.flags.0.contains(ObjSymbolFlags::Global)
|
||||
|| symbol.flags.0.contains(ObjSymbolFlags::Weak))
|
||||
&& !existing_functions.insert(symbol.name.clone())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let match_percent = symbol_diff.match_percent.unwrap_or_else(|| {
|
||||
// Support cases where we don't have a target object,
|
||||
// assume complete means 100% match
|
||||
if object.complete == Some(true) {
|
||||
100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
});
|
||||
measures.fuzzy_match_percent += match_percent * symbol.size as f32;
|
||||
measures.total_code += symbol.size;
|
||||
if match_percent == 100.0 {
|
||||
measures.matched_code += symbol.size;
|
||||
}
|
||||
functions.push(ReportItem {
|
||||
name: symbol.name.clone(),
|
||||
size: symbol.size,
|
||||
fuzzy_match_percent: match_percent,
|
||||
metadata: Some(ReportItemMetadata {
|
||||
demangled_name: symbol.demangled_name.clone(),
|
||||
virtual_address: symbol.virtual_address,
|
||||
}),
|
||||
});
|
||||
if match_percent == 100.0 {
|
||||
measures.matched_functions += 1;
|
||||
}
|
||||
measures.total_functions += 1;
|
||||
}
|
||||
}
|
||||
measures.calc_fuzzy_match_percent();
|
||||
measures.calc_matched_percent();
|
||||
Ok(Some(ReportUnit {
|
||||
name: object.name().to_string(),
|
||||
measures: Some(measures),
|
||||
sections,
|
||||
functions,
|
||||
metadata: Some(metadata),
|
||||
}))
|
||||
}
|
||||
|
||||
impl From<&ReportItem> for ChangeItemInfo {
|
||||
fn from(value: &ReportItem) -> Self {
|
||||
Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size }
|
||||
}
|
||||
}
|
||||
|
||||
fn changes(args: ChangesArgs) -> Result<()> {
|
||||
let output_format = if let Some(format) = &args.format {
|
||||
OutputFormat::from_str(format)?
|
||||
} else {
|
||||
OutputFormat::Json
|
||||
};
|
||||
|
||||
let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") {
|
||||
// Special case for comparing two reports from stdin
|
||||
let mut data = vec![];
|
||||
std::io::stdin().read_to_end(&mut data)?;
|
||||
let input = ChangesInput::decode(data.as_slice())?;
|
||||
(input.from.unwrap(), input.to.unwrap())
|
||||
} else {
|
||||
let previous = read_report(&args.previous)?;
|
||||
let current = read_report(&args.current)?;
|
||||
(previous, current)
|
||||
};
|
||||
let mut changes = Changes { from: previous.measures, to: current.measures, units: vec![] };
|
||||
for prev_unit in &previous.units {
|
||||
let curr_unit = current.units.iter().find(|u| u.name == prev_unit.name);
|
||||
let sections = process_items(prev_unit, curr_unit, |u| &u.sections);
|
||||
let functions = process_items(prev_unit, curr_unit, |u| &u.functions);
|
||||
|
||||
let prev_measures = prev_unit.measures;
|
||||
let curr_measures = curr_unit.and_then(|u| u.measures);
|
||||
if !functions.is_empty() || prev_measures != curr_measures {
|
||||
changes.units.push(ChangeUnit {
|
||||
name: prev_unit.name.clone(),
|
||||
from: prev_measures,
|
||||
to: curr_measures,
|
||||
sections,
|
||||
functions,
|
||||
metadata: curr_unit
|
||||
.as_ref()
|
||||
.and_then(|u| u.metadata.clone())
|
||||
.or_else(|| prev_unit.metadata.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
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: curr_unit.measures,
|
||||
sections: process_new_items(&curr_unit.sections),
|
||||
functions: process_new_items(&curr_unit.functions),
|
||||
metadata: curr_unit.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
write_output(&changes, args.output.as_deref(), output_format)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
|
||||
prev_unit: &ReportUnit,
|
||||
curr_unit: Option<&ReportUnit>,
|
||||
getter: F,
|
||||
) -> Vec<ChangeItem> {
|
||||
let prev_items = getter(prev_unit);
|
||||
let mut items = vec![];
|
||||
if let Some(curr_unit) = curr_unit {
|
||||
let curr_items = getter(curr_unit);
|
||||
for prev_func in prev_items {
|
||||
let prev_func_info = ChangeItemInfo::from(prev_func);
|
||||
let curr_func = curr_items.iter().find(|f| f.name == prev_func.name);
|
||||
let curr_func_info = curr_func.map(ChangeItemInfo::from);
|
||||
if let Some(curr_func_info) = curr_func_info {
|
||||
if prev_func_info != curr_func_info {
|
||||
items.push(ChangeItem {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(prev_func_info),
|
||||
to: Some(curr_func_info),
|
||||
metadata: curr_func.as_ref().unwrap().metadata.clone(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push(ChangeItem {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(prev_func_info),
|
||||
to: None,
|
||||
metadata: prev_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
for curr_func in curr_items {
|
||||
if !prev_items.iter().any(|f| f.name == curr_func.name) {
|
||||
items.push(ChangeItem {
|
||||
name: curr_func.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeItemInfo::from(curr_func)),
|
||||
metadata: curr_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for prev_func in prev_items {
|
||||
items.push(ChangeItem {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(ChangeItemInfo::from(prev_func)),
|
||||
to: None,
|
||||
metadata: prev_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
|
||||
items
|
||||
.iter()
|
||||
.map(|item| ChangeItem {
|
||||
name: item.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeItemInfo::from(item)),
|
||||
metadata: item.metadata.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_report(path: &Path) -> Result<Report> {
|
||||
if path == Path::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()))
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
mod argp_version;
|
||||
mod cmd;
|
||||
mod util;
|
||||
|
||||
use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
|
||||
|
||||
use anyhow::{Error, Result};
|
||||
use argp::{FromArgValue, FromArgs};
|
||||
use enable_ansi_support::enable_ansi_support;
|
||||
use supports_color::Stream;
|
||||
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
enum LogLevel {
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
impl FromStr for LogLevel {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"error" => Self::Error,
|
||||
"warn" => Self::Warn,
|
||||
"info" => Self::Info,
|
||||
"debug" => Self::Debug,
|
||||
"trace" => Self::Trace,
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LogLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
LogLevel::Error => "error",
|
||||
LogLevel::Warn => "warn",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Trace => "trace",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArgValue for LogLevel {
|
||||
fn from_arg_value(value: &OsStr) -> Result<Self, String> {
|
||||
String::from_arg_value(value)
|
||||
.and_then(|s| Self::from_str(&s).map_err(|_| "Invalid log level".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Yet another GameCube/Wii decompilation toolkit.
|
||||
struct TopLevel {
|
||||
#[argp(subcommand)]
|
||||
command: SubCommand,
|
||||
#[argp(option, short = 'C')]
|
||||
/// Change working directory.
|
||||
chdir: Option<PathBuf>,
|
||||
#[argp(option, short = 'L')]
|
||||
/// Minimum logging level. (Default: info)
|
||||
/// Possible values: error, warn, info, debug, trace
|
||||
log_level: Option<LogLevel>,
|
||||
/// Print version information and exit.
|
||||
#[argp(switch, short = 'V')]
|
||||
version: bool,
|
||||
/// Disable color output. (env: NO_COLOR)
|
||||
#[argp(switch)]
|
||||
no_color: bool,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argp(subcommand)]
|
||||
enum SubCommand {
|
||||
Diff(cmd::diff::Args),
|
||||
Report(cmd::report::Args),
|
||||
}
|
||||
|
||||
// Duplicated from supports-color so we can check early.
|
||||
fn env_no_color() -> bool {
|
||||
match env::var("NO_COLOR").as_deref() {
|
||||
Ok("") | Ok("0") | Err(_) => false,
|
||||
Ok(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: TopLevel = argp_version::from_env();
|
||||
let use_colors = if args.no_color || env_no_color() {
|
||||
false
|
||||
} else {
|
||||
// Try to enable ANSI support on Windows.
|
||||
let _ = enable_ansi_support();
|
||||
// Disable isatty check for supports-color. (e.g. when used with ninja)
|
||||
unsafe { env::set_var("IGNORE_IS_TERMINAL", "1") };
|
||||
supports_color::on(Stream::Stdout).is_some_and(|c| c.has_basic)
|
||||
};
|
||||
|
||||
let format =
|
||||
tracing_subscriber::fmt::format().with_ansi(use_colors).with_target(false).without_time();
|
||||
let builder = tracing_subscriber::fmt().event_format(format).with_writer(std::io::stderr);
|
||||
if let Some(level) = args.log_level {
|
||||
builder
|
||||
.with_max_level(match level {
|
||||
LogLevel::Error => LevelFilter::ERROR,
|
||||
LogLevel::Warn => LevelFilter::WARN,
|
||||
LogLevel::Info => LevelFilter::INFO,
|
||||
LogLevel::Debug => LevelFilter::DEBUG,
|
||||
LogLevel::Trace => LevelFilter::TRACE,
|
||||
})
|
||||
.init();
|
||||
} else {
|
||||
builder
|
||||
.with_env_filter(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy(),
|
||||
)
|
||||
.init();
|
||||
}
|
||||
|
||||
let mut result = Ok(());
|
||||
if let Some(dir) = &args.chdir {
|
||||
result = env::set_current_dir(dir).map_err(|e| {
|
||||
Error::new(e)
|
||||
.context(format!("Failed to change working directory to '{}'", dir.display()))
|
||||
});
|
||||
}
|
||||
result = result.and_then(|_| match args.command {
|
||||
SubCommand::Diff(c_args) => cmd::diff::run(c_args),
|
||||
SubCommand::Report(c_args) => cmd::report::run(c_args),
|
||||
});
|
||||
if let Err(e) = result {
|
||||
eprintln!("Failed: {e:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod report;
|
||||
pub mod term;
|
||||
@@ -0,0 +1,226 @@
|
||||
use anyhow::{bail, Result};
|
||||
use prost::Message;
|
||||
use serde_json::error::Category;
|
||||
|
||||
// Protobuf report types
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
|
||||
|
||||
impl Report {
|
||||
pub fn parse(data: &[u8]) -> Result<Self> {
|
||||
if data.is_empty() {
|
||||
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
|
||||
}
|
||||
if data[0] == b'{' {
|
||||
// Load as JSON
|
||||
Self::from_json(data).map_err(anyhow::Error::new)
|
||||
} else {
|
||||
// Load as binary protobuf
|
||||
Self::decode(data).map_err(anyhow::Error::new)
|
||||
}
|
||||
}
|
||||
|
||||
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
|
||||
match serde_json::from_slice::<Self>(bytes) {
|
||||
Ok(report) => Ok(report),
|
||||
Err(e) => {
|
||||
match e.classify() {
|
||||
Category::Io | Category::Eof | Category::Syntax => Err(e),
|
||||
Category::Data => {
|
||||
// Try to load as legacy report
|
||||
match serde_json::from_slice::<LegacyReport>(bytes) {
|
||||
Ok(legacy_report) => Ok(Report::from(legacy_report)),
|
||||
Err(_) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Measures {
|
||||
/// Average the fuzzy match percentage over total code bytes.
|
||||
pub fn calc_fuzzy_match_percent(&mut self) {
|
||||
if self.total_code == 0 {
|
||||
self.fuzzy_match_percent = 100.0;
|
||||
} else {
|
||||
self.fuzzy_match_percent /= self.total_code as f32;
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the percentage of matched code, data, and functions.
|
||||
pub fn calc_matched_percent(&mut self) {
|
||||
self.matched_code_percent = if self.total_code == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.matched_code as f32 / self.total_code as f32 * 100.0
|
||||
};
|
||||
self.matched_data_percent = if self.total_data == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.matched_data as f32 / self.total_data as f32 * 100.0
|
||||
};
|
||||
self.matched_functions_percent = if self.total_functions == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.matched_functions as f32 / self.total_functions as f32 * 100.0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows [collect](Iterator::collect) to be used on an iterator of [Measures].
|
||||
impl FromIterator<Measures> for Measures {
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where T: IntoIterator<Item = Measures> {
|
||||
let mut measures = Measures::default();
|
||||
for other in iter {
|
||||
measures.fuzzy_match_percent += other.fuzzy_match_percent * other.total_code as f32;
|
||||
measures.total_code += other.total_code;
|
||||
measures.matched_code += other.matched_code;
|
||||
measures.total_data += other.total_data;
|
||||
measures.matched_data += other.matched_data;
|
||||
measures.total_functions += other.total_functions;
|
||||
measures.matched_functions += other.matched_functions;
|
||||
}
|
||||
measures.calc_fuzzy_match_percent();
|
||||
measures.calc_matched_percent();
|
||||
measures
|
||||
}
|
||||
}
|
||||
|
||||
// Older JSON report types
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct LegacyReport {
|
||||
fuzzy_match_percent: f32,
|
||||
total_code: u64,
|
||||
matched_code: u64,
|
||||
matched_code_percent: f32,
|
||||
total_data: u64,
|
||||
matched_data: u64,
|
||||
matched_data_percent: f32,
|
||||
total_functions: u32,
|
||||
matched_functions: u32,
|
||||
matched_functions_percent: f32,
|
||||
units: Vec<LegacyReportUnit>,
|
||||
}
|
||||
|
||||
impl From<LegacyReport> for Report {
|
||||
fn from(value: LegacyReport) -> Self {
|
||||
Self {
|
||||
measures: Some(Measures {
|
||||
fuzzy_match_percent: value.fuzzy_match_percent,
|
||||
total_code: value.total_code,
|
||||
matched_code: value.matched_code,
|
||||
matched_code_percent: value.matched_code_percent,
|
||||
total_data: value.total_data,
|
||||
matched_data: value.matched_data,
|
||||
matched_data_percent: value.matched_data_percent,
|
||||
total_functions: value.total_functions,
|
||||
matched_functions: value.matched_functions,
|
||||
matched_functions_percent: value.matched_functions_percent,
|
||||
}),
|
||||
units: value.units.into_iter().map(ReportUnit::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct LegacyReportUnit {
|
||||
name: String,
|
||||
fuzzy_match_percent: f32,
|
||||
total_code: u64,
|
||||
matched_code: u64,
|
||||
total_data: u64,
|
||||
matched_data: u64,
|
||||
total_functions: u32,
|
||||
matched_functions: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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>,
|
||||
sections: Vec<LegacyReportItem>,
|
||||
functions: Vec<LegacyReportItem>,
|
||||
}
|
||||
|
||||
impl From<LegacyReportUnit> for ReportUnit {
|
||||
fn from(value: LegacyReportUnit) -> Self {
|
||||
let mut measures = Measures {
|
||||
fuzzy_match_percent: value.fuzzy_match_percent,
|
||||
total_code: value.total_code,
|
||||
matched_code: value.matched_code,
|
||||
total_data: value.total_data,
|
||||
matched_data: value.matched_data,
|
||||
total_functions: value.total_functions,
|
||||
matched_functions: value.matched_functions,
|
||||
..Default::default()
|
||||
};
|
||||
measures.calc_matched_percent();
|
||||
Self {
|
||||
name: value.name.clone(),
|
||||
measures: Some(measures),
|
||||
sections: value.sections.into_iter().map(ReportItem::from).collect(),
|
||||
functions: value.functions.into_iter().map(ReportItem::from).collect(),
|
||||
metadata: Some(ReportUnitMetadata {
|
||||
complete: value.complete,
|
||||
module_name: value.module_name.clone(),
|
||||
module_id: value.module_id,
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct LegacyReportItem {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
demangled_name: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "serialize_hex",
|
||||
deserialize_with = "deserialize_hex"
|
||||
)]
|
||||
address: Option<u64>,
|
||||
size: u64,
|
||||
fuzzy_match_percent: f32,
|
||||
}
|
||||
|
||||
impl From<LegacyReportItem> for ReportItem {
|
||||
fn from(value: LegacyReportItem) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
size: value.size,
|
||||
fuzzy_match_percent: value.fuzzy_match_percent,
|
||||
metadata: Some(ReportItemMetadata {
|
||||
demangled_name: value.demangled_name,
|
||||
virtual_address: value.address,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use std::{io::stdout, panic};
|
||||
|
||||
use crossterm::{
|
||||
cursor::Show,
|
||||
event::DisableMouseCapture,
|
||||
terminal::{disable_raw_mode, LeaveAlternateScreen},
|
||||
};
|
||||
|
||||
pub fn crossterm_panic_handler() {
|
||||
let original_hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
let _ = crossterm::execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture, Show);
|
||||
let _ = disable_raw_mode();
|
||||
original_hook(panic_info);
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
[package]
|
||||
name = "objdiff-core"
|
||||
version = "2.0.0-beta.4"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/objdiff"
|
||||
readme = "../README.md"
|
||||
description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
|
||||
[features]
|
||||
all = ["config", "dwarf", "mips", "ppc", "x86", "arm"]
|
||||
any-arch = [] # Implicit, used to check if any arch is enabled
|
||||
config = ["globset", "semver", "serde_json", "serde_yaml"]
|
||||
dwarf = ["gimli"]
|
||||
mips = ["any-arch", "rabbitizer"]
|
||||
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
|
||||
x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"]
|
||||
arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
byteorder = "1.5.0"
|
||||
filetime = "0.2.23"
|
||||
flagset = "0.4.5"
|
||||
log = "0.4.21"
|
||||
memmap2 = "0.9.4"
|
||||
num-traits = "0.2.18"
|
||||
object = { version = "0.35.0", features = ["read_core", "std", "elf", "pe"], default-features = false }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
similar = { version = "2.5.0", default-features = false }
|
||||
strum = { version = "0.26.2", features = ["derive"] }
|
||||
|
||||
# config
|
||||
globset = { version = "0.4.14", features = ["serde1"], optional = true }
|
||||
semver = { version = "1.0.22", optional = true }
|
||||
serde_json = { version = "1.0.116", optional = true }
|
||||
serde_yaml = { version = "0.9.34", optional = true }
|
||||
|
||||
# dwarf
|
||||
gimli = { version = "0.29.0", default-features = false, features = ["read-all"], optional = true }
|
||||
|
||||
# ppc
|
||||
cwdemangle = { version = "1.0.0", optional = true }
|
||||
cwextab = { version = "0.2.3", optional = true }
|
||||
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "6cbd7d888c7082c2c860f66cbb9848d633f753ed", optional = true }
|
||||
|
||||
# mips
|
||||
rabbitizer = { version = "1.11.0", optional = true }
|
||||
|
||||
# x86
|
||||
cpp_demangle = { version = "0.4.3", optional = true }
|
||||
iced-x86 = { version = "1.21.0", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
|
||||
msvc-demangler = { version = "0.10.0", optional = true }
|
||||
|
||||
# arm
|
||||
unarm = { version = "1.4.0", optional = true }
|
||||
arm-attr = { version = "0.1.1", optional = true }
|
||||
@@ -0,0 +1,440 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, HashMap},
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use arm_attr::{enums::CpuArch, tag::Tag, BuildAttrs};
|
||||
use object::{
|
||||
elf::{self, SHT_ARM_ATTRIBUTES},
|
||||
Endian, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, SectionIndex,
|
||||
SectionKind, Symbol, SymbolKind,
|
||||
};
|
||||
use unarm::{
|
||||
args::{Argument, OffsetImm, OffsetReg, Register},
|
||||
parse::{ArmVersion, ParseMode, Parser},
|
||||
DisplayOptions, ParseFlags, ParsedIns, RegNames,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig},
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
||||
};
|
||||
|
||||
pub struct ObjArchArm {
|
||||
/// Maps section index, to list of disasm modes (arm, thumb or data) sorted by address
|
||||
disasm_modes: HashMap<SectionIndex, Vec<DisasmMode>>,
|
||||
detected_version: Option<ArmVersion>,
|
||||
endianness: object::Endianness,
|
||||
}
|
||||
|
||||
impl ObjArchArm {
|
||||
pub fn new(file: &File) -> Result<Self> {
|
||||
let endianness = file.endianness();
|
||||
match file {
|
||||
File::Elf32(_) => {
|
||||
let disasm_modes = Self::elf_get_mapping_symbols(file);
|
||||
let detected_version = Self::elf_detect_arm_version(file)?;
|
||||
Ok(Self { disasm_modes, detected_version, endianness })
|
||||
}
|
||||
_ => bail!("Unsupported file format {:?}", file.format()),
|
||||
}
|
||||
}
|
||||
|
||||
fn elf_detect_arm_version(file: &File) -> Result<Option<ArmVersion>> {
|
||||
// Check ARM attributes
|
||||
if let Some(arm_attrs) = file.sections().find(|s| {
|
||||
s.kind() == SectionKind::Elf(SHT_ARM_ATTRIBUTES) && s.name() == Ok(".ARM.attributes")
|
||||
}) {
|
||||
let attr_data = arm_attrs.uncompressed_data()?;
|
||||
let build_attrs = BuildAttrs::new(&attr_data, match file.endianness() {
|
||||
object::Endianness::Little => arm_attr::Endian::Little,
|
||||
object::Endianness::Big => arm_attr::Endian::Big,
|
||||
})?;
|
||||
for subsection in build_attrs.subsections() {
|
||||
let subsection = subsection?;
|
||||
if !subsection.is_aeabi() {
|
||||
continue;
|
||||
}
|
||||
// Only checking first CpuArch tag. Others may exist, but that's very unlikely.
|
||||
let cpu_arch = subsection.into_public_tag_iter()?.find_map(|(_, tag)| {
|
||||
if let Tag::CpuArch(cpu_arch) = tag {
|
||||
Some(cpu_arch)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
match cpu_arch {
|
||||
Some(CpuArch::V4T) => return Ok(Some(ArmVersion::V4T)),
|
||||
Some(CpuArch::V5TE) => return Ok(Some(ArmVersion::V5Te)),
|
||||
Some(CpuArch::V6K) => return Ok(Some(ArmVersion::V6K)),
|
||||
Some(arch) => bail!("ARM arch {} not supported", arch),
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn elf_get_mapping_symbols(file: &File) -> HashMap<SectionIndex, Vec<DisasmMode>> {
|
||||
file.sections()
|
||||
.filter(|s| s.kind() == SectionKind::Text)
|
||||
.map(|s| {
|
||||
let index = s.index();
|
||||
let mut mapping_symbols: Vec<_> = file
|
||||
.symbols()
|
||||
.filter(|s| s.section_index().map(|i| i == index).unwrap_or(false))
|
||||
.filter_map(|s| DisasmMode::from_symbol(&s))
|
||||
.collect();
|
||||
mapping_symbols.sort_unstable_by_key(|x| x.address);
|
||||
(s.index(), mapping_symbols)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjArch for ObjArchArm {
|
||||
fn symbol_address(&self, symbol: &Symbol) -> u64 {
|
||||
let address = symbol.address();
|
||||
if symbol.kind() == SymbolKind::Text {
|
||||
address & !1
|
||||
} else {
|
||||
address
|
||||
}
|
||||
}
|
||||
|
||||
fn process_code(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u64>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let start_addr = address as u32;
|
||||
let end_addr = start_addr + code.len() as u32;
|
||||
|
||||
// Mapping symbols decide what kind of data comes after it. $a for ARM code, $t for Thumb code and $d for data.
|
||||
let fallback_mappings = [DisasmMode { address: start_addr, mapping: ParseMode::Arm }];
|
||||
let mapping_symbols = self
|
||||
.disasm_modes
|
||||
.get(&SectionIndex(section_index))
|
||||
.map(|x| x.as_slice())
|
||||
.unwrap_or(&fallback_mappings);
|
||||
let first_mapping_idx =
|
||||
match mapping_symbols.binary_search_by_key(&start_addr, |x| x.address) {
|
||||
Ok(idx) => idx,
|
||||
Err(idx) => idx - 1,
|
||||
};
|
||||
let first_mapping = mapping_symbols[first_mapping_idx].mapping;
|
||||
|
||||
let mut mappings_iter =
|
||||
mapping_symbols.iter().skip(first_mapping_idx + 1).take_while(|x| x.address < end_addr);
|
||||
let mut next_mapping = mappings_iter.next();
|
||||
|
||||
let ins_count = code.len() / first_mapping.instruction_size(start_addr);
|
||||
let mut ops = Vec::<u16>::with_capacity(ins_count);
|
||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||
|
||||
let version = match config.arm_arch_version {
|
||||
ArmArchVersion::Auto => self.detected_version.unwrap_or(ArmVersion::V5Te),
|
||||
ArmArchVersion::V4T => ArmVersion::V4T,
|
||||
ArmArchVersion::V5TE => ArmVersion::V5Te,
|
||||
ArmArchVersion::V6K => ArmVersion::V6K,
|
||||
};
|
||||
let endian = match self.endianness {
|
||||
object::Endianness::Little => unarm::Endian::Little,
|
||||
object::Endianness::Big => unarm::Endian::Big,
|
||||
};
|
||||
|
||||
let parse_flags = ParseFlags { ual: config.arm_unified_syntax };
|
||||
|
||||
let mut parser = Parser::new(version, first_mapping, start_addr, endian, parse_flags, code);
|
||||
|
||||
let display_options = DisplayOptions {
|
||||
reg_names: RegNames {
|
||||
av_registers: config.arm_av_registers,
|
||||
r9_use: match config.arm_r9_usage {
|
||||
ArmR9Usage::GeneralPurpose => unarm::R9Use::GeneralPurpose,
|
||||
ArmR9Usage::Sb => unarm::R9Use::Pid,
|
||||
ArmR9Usage::Tr => unarm::R9Use::Tls,
|
||||
},
|
||||
explicit_stack_limit: config.arm_sl_usage,
|
||||
frame_pointer: config.arm_fp_usage,
|
||||
ip: config.arm_ip_usage,
|
||||
},
|
||||
};
|
||||
|
||||
while let Some((address, op, ins)) = parser.next() {
|
||||
if let Some(next) = next_mapping {
|
||||
let next_address = parser.address;
|
||||
if next_address >= next.address {
|
||||
// Change mapping
|
||||
parser.mode = next.mapping;
|
||||
next_mapping = mappings_iter.next();
|
||||
}
|
||||
}
|
||||
let line = line_info.range(..=address as u64).last().map(|(_, &b)| b);
|
||||
|
||||
let reloc = relocations.iter().find(|r| (r.address as u32 & !1) == address).cloned();
|
||||
|
||||
let mut reloc_arg = None;
|
||||
if let Some(reloc) = &reloc {
|
||||
match reloc.flags {
|
||||
// Calls
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_THM_XPC22 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
|
||||
reloc_arg =
|
||||
ins.args.iter().rposition(|a| matches!(a, Argument::BranchDest(_)));
|
||||
}
|
||||
// Data
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_ABS32 } => {
|
||||
reloc_arg = ins.args.iter().rposition(|a| matches!(a, Argument::UImm(_)));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
};
|
||||
|
||||
let (args, branch_dest) = if reloc.is_some() && parser.mode == ParseMode::Data {
|
||||
(vec![ObjInsArg::Reloc], None)
|
||||
} else {
|
||||
push_args(&ins, config, reloc_arg, address, display_options)?
|
||||
};
|
||||
|
||||
ops.push(op.id());
|
||||
insts.push(ObjIns {
|
||||
address: address as u64,
|
||||
size: (parser.address - address) as u8,
|
||||
op: op.id(),
|
||||
mnemonic: ins.mnemonic.to_string(),
|
||||
args,
|
||||
reloc,
|
||||
branch_dest,
|
||||
line,
|
||||
formatted: ins.display(display_options).to_string(),
|
||||
orig: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ProcessCodeResult { ops, insts })
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> anyhow::Result<i64> {
|
||||
let address = address as usize;
|
||||
Ok(match reloc.flags() {
|
||||
// ARM calls
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
|
||||
let data = section.data[address..address + 4].try_into()?;
|
||||
let addend = self.endianness.read_i32_bytes(data);
|
||||
let imm24 = addend & 0xffffff;
|
||||
(imm24 << 2) << 8 >> 8
|
||||
}
|
||||
|
||||
// Thumb calls
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_XPC22 } => {
|
||||
let data = section.data[address..address + 2].try_into()?;
|
||||
let high = self.endianness.read_i16_bytes(data) as i32;
|
||||
let data = section.data[address + 2..address + 4].try_into()?;
|
||||
let low = self.endianness.read_i16_bytes(data) as i32;
|
||||
|
||||
let imm22 = ((high & 0x7ff) << 11) | (low & 0x7ff);
|
||||
(imm22 << 1) << 9 >> 9
|
||||
}
|
||||
|
||||
// Data
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_ABS32 } => {
|
||||
let data = section.data[address..address + 4].try_into()?;
|
||||
self.endianness.read_i32_bytes(data)
|
||||
}
|
||||
|
||||
flags => bail!("Unsupported ARM implicit relocation {flags:?}"),
|
||||
} as i64)
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
}
|
||||
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
||||
Cow::Owned(format!("<{flags:?}>"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct DisasmMode {
|
||||
address: u32,
|
||||
mapping: ParseMode,
|
||||
}
|
||||
|
||||
impl DisasmMode {
|
||||
fn from_symbol<'a>(sym: &Symbol<'a, '_, &'a [u8]>) -> Option<Self> {
|
||||
if let Ok(name) = sym.name() {
|
||||
ParseMode::from_mapping_symbol(name)
|
||||
.map(|mapping| DisasmMode { address: sym.address() as u32, mapping })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_args(
|
||||
parsed_ins: &ParsedIns,
|
||||
config: &DiffObjConfig,
|
||||
reloc_arg: Option<usize>,
|
||||
cur_addr: u32,
|
||||
display_options: DisplayOptions,
|
||||
) -> Result<(Vec<ObjInsArg>, Option<u64>)> {
|
||||
let mut args = vec![];
|
||||
let mut branch_dest = None;
|
||||
let mut writeback = false;
|
||||
let mut deref = false;
|
||||
for (i, arg) in parsed_ins.args_iter().enumerate() {
|
||||
// Emit punctuation before separator
|
||||
if deref {
|
||||
match arg {
|
||||
Argument::OffsetImm(OffsetImm { post_indexed: true, value: _ })
|
||||
| Argument::OffsetReg(OffsetReg { add: _, post_indexed: true, reg: _ })
|
||||
| Argument::CoOption(_) => {
|
||||
deref = false;
|
||||
args.push(ObjInsArg::PlainText("]".into()));
|
||||
if writeback {
|
||||
writeback = false;
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
args.push(ObjInsArg::PlainText(config.separator().into()));
|
||||
}
|
||||
|
||||
if reloc_arg == Some(i) {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
} else {
|
||||
match arg {
|
||||
Argument::None => {}
|
||||
Argument::Reg(reg) => {
|
||||
if reg.deref {
|
||||
deref = true;
|
||||
args.push(ObjInsArg::PlainText("[".into()));
|
||||
}
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
reg.reg.display(display_options.reg_names).to_string().into(),
|
||||
)));
|
||||
if reg.writeback {
|
||||
if reg.deref {
|
||||
writeback = true;
|
||||
} else {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
}
|
||||
Argument::RegList(reg_list) => {
|
||||
args.push(ObjInsArg::PlainText("{".into()));
|
||||
let mut first = true;
|
||||
for i in 0..16 {
|
||||
if (reg_list.regs & (1 << i)) != 0 {
|
||||
if !first {
|
||||
args.push(ObjInsArg::PlainText(config.separator().into()));
|
||||
}
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
Register::parse(i)
|
||||
.display(display_options.reg_names)
|
||||
.to_string()
|
||||
.into(),
|
||||
)));
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
args.push(ObjInsArg::PlainText("}".into()));
|
||||
if reg_list.user_mode {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("^".to_string().into())));
|
||||
}
|
||||
}
|
||||
Argument::UImm(value) | Argument::CoOpcode(value) | Argument::SatImm(value) => {
|
||||
args.push(ObjInsArg::PlainText("#".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
|
||||
}
|
||||
Argument::SImm(value)
|
||||
| Argument::OffsetImm(OffsetImm { post_indexed: _, value }) => {
|
||||
args.push(ObjInsArg::PlainText("#".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(*value as i64)));
|
||||
}
|
||||
Argument::BranchDest(value) => {
|
||||
let dest = cur_addr.wrapping_add_signed(*value) as u64;
|
||||
args.push(ObjInsArg::BranchDest(dest));
|
||||
branch_dest = Some(dest);
|
||||
}
|
||||
Argument::CoOption(value) => {
|
||||
args.push(ObjInsArg::PlainText("{".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
|
||||
args.push(ObjInsArg::PlainText("}".into()));
|
||||
}
|
||||
Argument::CoprocNum(value) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(format!("p{}", value).into())));
|
||||
}
|
||||
Argument::ShiftImm(shift) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(shift.op.to_string().into())));
|
||||
args.push(ObjInsArg::PlainText(" #".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(shift.imm as u64)));
|
||||
}
|
||||
Argument::ShiftReg(shift) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(shift.op.to_string().into())));
|
||||
args.push(ObjInsArg::PlainText(" ".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
shift.reg.display(display_options.reg_names).to_string().into(),
|
||||
)));
|
||||
}
|
||||
Argument::OffsetReg(offset) => {
|
||||
if !offset.add {
|
||||
args.push(ObjInsArg::PlainText("-".into()));
|
||||
}
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
offset.reg.display(display_options.reg_names).to_string().into(),
|
||||
)));
|
||||
}
|
||||
Argument::CpsrMode(mode) => {
|
||||
args.push(ObjInsArg::PlainText("#".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(mode.mode as u64)));
|
||||
if mode.writeback {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
Argument::CoReg(_)
|
||||
| Argument::StatusReg(_)
|
||||
| Argument::StatusMask(_)
|
||||
| Argument::Shift(_)
|
||||
| Argument::CpsrFlags(_)
|
||||
| Argument::Endian(_) => args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
arg.display(display_options).to_string().into(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
if deref {
|
||||
args.push(ObjInsArg::PlainText("]".into()));
|
||||
if writeback {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
Ok((args, branch_dest))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user