Compare commits

...

16 Commits

Author SHA1 Message Date
LagoLunatic
fb1d434bbc Version v3.1.0 2025-09-03 21:44:03 -04:00
LagoLunatic
23009bf9a3 Implement diffing individual data symbols (#244)
* Implement diffing individual data symbols

* Remove unused code for diffing sections

* Data diff view: Make rows show offset within the symbol, not within the section

* Remove SelectedSymbol enum as it only has a single variant now

* Create fake data section symbols to allow diffing entire sections again

* Fix text sections not having their size zeroed out

* Update test snapshots

* Clean up code for inferring section symbol size

* Fix bug where PPC pool references weren't ignoring section symbols

* Update comment

* Always add unique section symbols for data sections

* Update test snapshots

* Remove unnecessary clone in format! call

* Auto-start mapping for unpaired data symbols
2025-09-02 19:37:17 -06:00
Anghelo Carvajal
6fb4bb8855 [MIPS] Fix symbols being filtered out from target side of diff if target object contains .NON_MATCHING markers (#250)
* Fix filtering out symbols from the target side that have a symbol with the same name and a `.NON_MATCHING` suffix.

- Target side: Show all the symbols except the `.NON_MATCHING` ones.
- Base side: Ignore all the `.NON_MATCHING` symbols and also ignore the ones with the same name without the suffix

* fmt

* comment

* tests

* fmt tests

* maybe this could fix wasm?

* Fix wasm?

* fmt

* Move `DiffSide` to `diff` mod

* Update the stuff the advisories CI told me to
2025-09-02 19:13:29 -06:00
a138dfa907 Gracefully handle OOB in symbol_context/symbol_hover
Resolves decomp.me#1576
2025-09-02 19:11:07 -06:00
rjkiv
0b95613768 Hide certain symbols for X360 COFFs (#248)
* hide except_data symbols

* hide unwinds by default

* move COFF filters to obj/read.rs

* cargo fmt read.rs

* clippy moment

* clippy pls
2025-08-31 16:45:21 -06:00
5d4b33a500 Update README.md & config.schema.json 2025-08-30 23:56:46 -06:00
LagoLunatic
f2a591356e Improve automatic symbol pairing for nameless literals (#247)
* Improve automatic symbol pairing for nameless literals

* Fix data reloc diffing when the reloc points to an in-function static symbol

* Only pair up literals that match perfectly

* Clippy

* Do two separate passes when pairing up literals

* Fix partially-matching literal pairups not working right

* Remove duplicate $ splitting code

* Implement $ splitting for section names too

* Minor cleanup
2025-08-30 23:07:43 -06:00
8d24ec6373 Detect x86 instruction size differences
Check raw code length when instructions have same disassembly
but different encodings. Marks as OpMismatch to indicate
encoding difference.

Fixes #242
Fixes #233
2025-08-30 22:57:39 -06:00
58430d947b ci: Remove quotes from CARGO_ZIGBUILD_ZIG_PATH 2025-08-30 15:49:14 -06:00
1533125f9d Resolve objdiff-wasm clippy warnings 2025-08-30 15:02:10 -06:00
5654060dc8 ci: Use uv tool instead of venv 2025-08-30 15:02:10 -06:00
84079c3934 objdiff-wasm build improvements 2025-08-30 15:02:10 -06:00
LagoLunatic
48804dc2e3 Merge pull request #245 from LagoLunatic/subi
PPC pooled data references: Add support for `subi`, `addis`, and `subis` instructions
2025-08-23 18:05:47 -04:00
LagoLunatic
8cfa8b7dab Update default watch patterns to include more extensions (#246) 2025-08-23 15:37:34 -06:00
LagoLunatic
93a4d7e55d PPC pooled data references: Add support for subi, addis, and subis instructions 2025-08-23 16:37:57 -04:00
iFarbod
7c424a7966 Ignore switchD_ labels generated by Ghidra (#241)
Seeing how commonly used boricj's delinker extension is, it makes sense for this one to be included by default, before #238 is considered and worked on.
2025-08-19 10:23:07 -06:00
45 changed files with 1262 additions and 543 deletions

View File

@@ -36,9 +36,9 @@ jobs:
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo check
run: cargo check --all-targets --all-features
run: cargo check --all-targets --all-features --workspace
- name: Cargo clippy
run: cargo clippy --all-targets --all-features
run: cargo clippy --all-targets --all-features --workspace
fmt:
name: Format
@@ -92,7 +92,7 @@ jobs:
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo test
run: cargo test --release --features all
run: cargo test --release --all-features --workspace
build-cli:
name: Build objdiff-cli
@@ -146,13 +146,14 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
if: matrix.build == 'zigbuild'
uses: astral-sh/setup-uv@v6
- name: Install cargo-zigbuild
if: matrix.build == 'zigbuild'
run: |
python3 -m venv .venv
. .venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
pip install ziglang==0.13.0.post1 cargo-zigbuild==0.19.8
uv tool install cargo-zigbuild==0.20.1 --with-executables-from ziglang==0.15.1
echo "CARGO_ZIGBUILD_ZIG_PATH=$(uv tool dir)/cargo-zigbuild/bin/python-zig" >> $GITHUB_ENV
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
@@ -213,13 +214,14 @@ jobs:
sudo apt-get -y install ${{ matrix.packages }}
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
if: matrix.build == 'zigbuild'
uses: astral-sh/setup-uv@v6
- name: Install cargo-zigbuild
if: matrix.build == 'zigbuild'
run: |
python3 -m venv .venv
. .venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
pip install ziglang==0.13.0.post1 cargo-zigbuild==0.19.8
uv tool install cargo-zigbuild==0.20.1 --with-executables-from ziglang==0.15.1
echo "CARGO_ZIGBUILD_ZIG_PATH=$(uv tool dir)/cargo-zigbuild/bin/python-zig" >> $GITHUB_ENV
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:

View File

@@ -24,7 +24,7 @@ repos:
description: Run cargo clippy on all project files.
language: system
entry: cargo
args: ["+nightly", "clippy", "--all-targets", "--all-features"]
args: ["+nightly", "clippy", "--all-targets", "--all-features", "--workspace"]
pass_filenames: false
- id: cargo-deny
name: cargo deny

100
Cargo.lock generated
View File

@@ -1898,8 +1898,8 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
"serde",
]
@@ -2832,11 +2832,11 @@ dependencies = [
[[package]]
name = "matchers"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata 0.1.10",
"regex-automata",
]
[[package]]
@@ -3092,12 +3092,11 @@ checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
version = "0.50.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
dependencies = [
"overload",
"winapi",
"windows-sys 0.52.0",
]
[[package]]
@@ -3436,7 +3435,7 @@ dependencies = [
[[package]]
name = "objdiff-cli"
version = "3.0.0"
version = "3.1.0"
dependencies = [
"anyhow",
"argp",
@@ -3459,7 +3458,7 @@ dependencies = [
[[package]]
name = "objdiff-core"
version = "3.0.0"
version = "3.1.0"
dependencies = [
"anyhow",
"arm-attr",
@@ -3514,7 +3513,7 @@ dependencies = [
[[package]]
name = "objdiff-gui"
version = "3.0.0"
version = "3.1.0"
dependencies = [
"anyhow",
"argp",
@@ -3551,7 +3550,7 @@ dependencies = [
[[package]]
name = "objdiff-wasm"
version = "3.0.0"
version = "3.1.0"
dependencies = [
"log",
"objdiff-core",
@@ -3684,12 +3683,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owned_ttf_parser"
version = "0.25.1"
@@ -4312,17 +4305,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -4333,15 +4317,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -5563,14 +5541,14 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
@@ -5863,9 +5841,9 @@ dependencies = [
[[package]]
name = "wasm-encoder"
version = "0.235.0"
version = "0.236.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3bc393c395cb621367ff02d854179882b9a351b4e0c93d1397e6090b53a5c2a"
checksum = "724fccfd4f3c24b7e589d333fc0429c68042897a7e8a5f8694f31792471841e7"
dependencies = [
"leb128fmt",
"wasmparser",
@@ -5873,9 +5851,9 @@ dependencies = [
[[package]]
name = "wasm-metadata"
version = "0.235.0"
version = "0.236.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b055604ba04189d54b8c0ab2c2fc98848f208e103882d5c0b984f045d5ea4d20"
checksum = "c909f94a49a8de3365f3c0344f064818f1e369ff1740c5b04f455f85d454768e"
dependencies = [
"anyhow",
"indexmap",
@@ -5898,9 +5876,9 @@ dependencies = [
[[package]]
name = "wasmparser"
version = "0.235.0"
version = "0.236.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "161296c618fa2d63f6ed5fffd1112937e803cb9ec71b32b01a76321555660917"
checksum = "a9b1e81f3eb254cf7404a82cee6926a4a3ccc5aad80cc3d43608a070c67aa1d7"
dependencies = [
"bitflags 2.9.1",
"hashbrown",
@@ -6726,19 +6704,19 @@ dependencies = [
[[package]]
name = "wit-bindgen"
version = "0.43.0"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a18712ff1ec5bd09da500fe1e91dec11256b310da0ff33f8b4ec92b927cf0c6"
checksum = "04bd9ed271234163b18c92783b0d406f08ca32c232e972f207a68c7b324c44bf"
dependencies = [
"wit-bindgen-rt 0.43.0",
"wit-bindgen-rt 0.44.0",
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.43.0"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c53468e077362201de11999c85c07c36e12048a990a3e0d69da2bd61da355d0"
checksum = "b4103c7a3e178b75cd8b0b574fa199ed015e8399c9859b003865cc28834b474b"
dependencies = [
"anyhow",
"heck",
@@ -6756,18 +6734,18 @@ dependencies = [
[[package]]
name = "wit-bindgen-rt"
version = "0.43.0"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fd734226eac1fd7c450956964e3a9094c9cee65e9dafdf126feef8c0096db65"
checksum = "653c85dd7aee6fe6f4bded0d242406deadae9819029ce6f7d258c920c384358a"
dependencies = [
"bitflags 2.9.1",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.43.0"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531ebfcec48e56473805285febdb450e270fa75b2dacb92816861d0473b4c15f"
checksum = "95d164b3b6fbd2b0dd8b639b1012110c0bc256519a0a6def410d4020fa8ae106"
dependencies = [
"anyhow",
"heck",
@@ -6781,9 +6759,9 @@ dependencies = [
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.43.0"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7852bf8a9d1ea80884d26b864ddebd7b0c7636697c6ca10f4c6c93945e023966"
checksum = "2c9100a5e1ac85e526dcd4ef49c3ff7689e026fa5e56e2a2047fd377fc682e02"
dependencies = [
"anyhow",
"prettyplease",
@@ -6796,9 +6774,9 @@ dependencies = [
[[package]]
name = "wit-component"
version = "0.235.0"
version = "0.236.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64a57a11109cc553396f89f3a38a158a97d0b1adaec113bd73e0f64d30fb601f"
checksum = "3622959ed7ed6341c38e5aa35af243632534b0a36226852faa802939ce11e00f"
dependencies = [
"anyhow",
"bitflags 2.9.1",
@@ -6840,9 +6818,9 @@ dependencies = [
[[package]]
name = "wit-parser"
version = "0.235.0"
version = "0.236.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a1f95a87d03a33e259af286b857a95911eb46236a0f726cbaec1227b3dfc67a"
checksum = "16e4833a20cd6e85d6abfea0e63a399472d6f88c6262957c17f546879a80ba15"
dependencies = [
"anyhow",
"id-arena",

View File

@@ -5,18 +5,28 @@ members = [
"objdiff-gui",
"objdiff-wasm",
]
default-members = [
"objdiff-cli",
"objdiff-core",
"objdiff-gui",
# Exclude objdiff-wasm by default
]
resolver = "3"
[workspace.package]
version = "3.1.0"
authors = ["Luke Street <luke@street.dev>"]
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
rust-version = "1.88"
[profile.release-lto]
inherits = "release"
lto = "fat"
strip = "debuginfo"
codegen-units = 1
[workspace.package]
version = "3.0.0"
authors = ["Luke Street <luke@street.dev>"]
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
rust-version = "1.88"
[profile.release-min]
inherits = "release-lto"
opt-level = "z"

140
README.md
View File

@@ -7,12 +7,14 @@ A local diffing tool for decompilation projects. Inspired by [decomp.me](https:/
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.
- Compare entire object files: functions and data
- Built-in C++ symbol demangling (GCC, MSVC, CodeWarrior, Itanium)
- Automatic rebuild on source file changes
- Project integration via [configuration file](#configuration)
- Search and filter objects with quick switching
- Click-to-highlight values and registers
- Detailed progress reporting (powers [decomp.dev](https://decomp.dev))
- WebAssembly API, [web interface](https://github.com/encounter/objdiff-web) and [Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=decomp-dev.objdiff) (WIP)
Supports:
@@ -40,7 +42,7 @@ For Linux and macOS, run `chmod +x objdiff-*` to make the binary executable.
### CLI
CLI binaries can be found on the [releases page](https://github.com/encounter/objdiff/releases).
CLI binaries are available on the [releases page](https://github.com/encounter/objdiff/releases).
## Screenshots
@@ -49,33 +51,30 @@ CLI binaries can be found on the [releases page](https://github.com/encounter/ob
## Usage
objdiff works by comparing two relocatable object files (`.o`). The objects are expected to have the same relative path
from the "target" and "base" directories.
objdiff compares two relocatable object files (`.o`). Here's how it works:
For example, if the target ("expected") object is located at `build/asm/MetroTRK/mslsupp.o` and the base ("actual")
object is located at `build/src/MetroTRK/mslsupp.o`, the following configuration would be used:
1. **Create an `objdiff.json` configuration file** in your project root (or generate it with your build script).
This file lists **all objects in the project** with their target ("expected") and base ("current") paths.
- Target build directory: `build/asm`
- Base build directory: `build/src`
- Object: `MetroTRK/mslsupp.o`
2. **Load the project** in objdiff.
objdiff will then execute the build system from the project directory to build both objects:
3. **Select an object** from the sidebar to begin diffing.
```sh
$ make build/asm/MetroTRK/mslsupp.o # Only if "Build target object" is enabled
$ make build/src/MetroTRK/mslsupp.o
```
4. **objdiff automatically:**
- Executes the build system to compile the base object (from current source code)
- Compares the two objects and displays the differences
- Watches for source file changes and rebuilds when detected
The objects will then be compared and the results will be displayed in the UI.
The configuration file allows complete flexibility in project structure - your build directories can have any layout as long as the paths are specified correctly.
See [Configuration](#configuration) for more information.
See [Configuration](#configuration) for setup details.
## Configuration
While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` file to configure the tool automatically. The configuration file must be located in
Projects can add an `objdiff.json` file to configure the tool automatically. The configuration file must be located in
the root project directory.
If your project has a generator script (e.g. `configure.py`), it's recommended to generate the objdiff configuration
If your project has a generator script (e.g. `configure.py`), it's highly recommended to generate the objdiff configuration
file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it from being committed.
```json
@@ -90,17 +89,23 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
"build_base": true,
"watch_patterns": [
"*.c",
"*.cc",
"*.cp",
"*.cpp",
"*.cxx",
"*.c++",
"*.h",
"*.hh",
"*.hp",
"*.hpp",
"*.hxx",
"*.h++",
"*.pch",
"*.pch++",
"*.inc",
"*.s",
"*.S",
"*.asm",
"*.inc",
"*.py",
"*.yml",
"*.txt",
@@ -122,78 +127,69 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
### Schema
View [config.schema.json](config.schema.json) for all available options. The below list is a summary of the most important options.
> [!NOTE]
> View [config.schema.json](config.schema.json) for all available options. Below is a summary of the most important options.
`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.
The build command will be `[custom_make] [custom_args] path/to/object.o`.
#### Build Configuration
`custom_args` _(optional)_: Additional arguments to pass to the build command prior to the object path.
**`custom_make`** _(optional, default: `"make"`)_
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`.
`build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g.
`make path/to/target.o`).
This is useful if the target objects are not built by default or can change based on project configuration or edits
to assembly files.
Requires the build system to be configured properly.
**`custom_args`** _(optional)_
Additional arguments to pass to the build command prior to the object path.
`build_base`: If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).
It's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.
**`build_target`** _(default: `false`)_
If true, objdiff will build the target objects before diffing (e.g. `make path/to/target.o`). Useful if target objects are not built by default or can change based on project configuration. Requires proper build system configuration.
`watch_patterns` _(optional)_: A list of glob patterns to watch for changes.
([Supported syntax](https://docs.rs/globset/latest/globset/#syntax))
If any of these files change, objdiff will automatically rebuild the objects and re-compare them.
If not specified, objdiff will use the default patterns listed above.
**`build_base`** _(default: `true`)_
If true, objdiff will build the base objects before diffing (e.g. `make path/to/base.o`). It's unlikely you'll want to disable this unless using an external tool to rebuild the base object.
`ignore_patterns` _(optional)_: A list of glob patterns to explicitly ignore when watching for changes.
([Supported syntax](https://docs.rs/globset/latest/globset/#syntax))
If not specified, objdiff will use the default patterns listed above.
#### File Watching
`units` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
**`watch_patterns`** _(optional, default: listed above)_
A list of glob patterns to watch for changes ([supported syntax](https://docs.rs/globset/latest/globset/#syntax)). When these files change, objdiff automatically rebuilds and re-compares objects.
> `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used.
>
> `target_path`: Path to the "target" or "expected" object from the project root.
> This object is the **intended result** of the match.
>
> `base_path`: Path to the "base" or "actual" object from the project root.
> This object is built from the **current source code**.
>
> `metadata.auto_generated` _(optional)_: Hides the object from the object list, but still includes it in reports.
>
> `metadata.complete` _(optional)_: Marks the object as "complete" (or "linked") in the object list.
> This is useful for marking objects that are fully decompiled. A value of `false` will mark the object as "incomplete".
**`ignore_patterns`** _(optional, default: listed above)_
A list of glob patterns to explicitly ignore when watching for changes ([supported syntax](https://docs.rs/globset/latest/globset/#syntax)).
#### Units (Objects)
**`units`** _(optional)_
If specified, objdiff displays a list of objects in the sidebar for easy navigation. Each unit contains:
- **`name`** _(optional)_ - The display name in the UI. Defaults to the object's `path`.
- **`target_path`** _(optional)_ - Path to the "target" or "expected" object (the **intended result**).
- **`base_path`** _(optional)_ - Path to the "base" or "current" object (built from **current source code**). Omit if there is no source object yet.
- **`metadata.auto_generated`** _(optional)_ - Hides the object from the sidebar but includes it in progress reports.
- **`metadata.complete`** _(optional)_ - Marks the object as "complete" (linked) when `true` or "incomplete" when `false`.
## Building
Install Rust via [rustup](https://rustup.rs).
```shell
$ git clone https://github.com/encounter/objdiff.git
$ cd objdiff
$ cargo run --release
git clone https://github.com/encounter/objdiff.git
cd objdiff
cargo run --release
```
Or using `cargo install`.
Or install directly with cargo:
```shell
$ cargo install --locked --git https://github.com/encounter/objdiff.git objdiff-gui objdiff-cli
cargo install --locked --git https://github.com/encounter/objdiff.git objdiff-gui objdiff-cli
```
The binaries will be installed to `~/.cargo/bin` as `objdiff` and `objdiff-cli`.
Binaries will be installed to `~/.cargo/bin` as `objdiff` and `objdiff-cli`.
## Installing `pre-commit`
## Contributing
When contributing, it's recommended to install `pre-commit` to automatically run the linter and formatter before a commit.
[`uv`](https://github.com/astral-sh/uv#installation) is recommended to manage Python version and tools.
Rust nightly is required for `cargo +nightly fmt` and `cargo +nightly clippy`.
Install `pre-commit` to run linting and formatting automatically:
```shell
$ cargo install --locked cargo-deny
$ rustup toolchain install nightly
$ uv tool install pre-commit
$ pre-commit install
rustup toolchain install nightly # Required for cargo fmt/clippy
cargo install --locked cargo-deny # https://github.com/EmbarkStudios/cargo-deny
uv tool install pre-commit # https://docs.astral.sh/uv, or use pipx or pip
pre-commit install
```
## License

View File

@@ -15,7 +15,7 @@
},
"custom_make": {
"type": "string",
"description": "By default, objdiff will use make to build the project.\nIf the project uses a different build system (e.g. ninja), specify it here.\nThe build command will be `[custom_make] [custom_args] path/to/object.o`.",
"description": "If the project uses a different build system (e.g. ninja), specify it here.\nThe build command will be `[custom_make] [custom_args] path/to/object.o`.",
"examples": [
"make",
"ninja"
@@ -41,33 +41,39 @@
},
"build_target": {
"type": "boolean",
"description": "If true, objdiff will tell the build system to build the target objects before diffing (e.g. `make path/to/target.o`).\nThis is useful if the target objects are not built by default or can change based on project configuration or edits to assembly files.\nRequires the build system to be configured properly.",
"description": "If true, objdiff will build the target objects before diffing (e.g. `make path/to/target.o`).\nUseful if target objects are not built by default or can change based on project configuration.\nRequires proper build system configuration.",
"default": false
},
"build_base": {
"type": "boolean",
"description": "If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).\nIt's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.",
"description": "If true, objdiff will build the base objects before diffing (e.g. `make path/to/base.o`).\nIt's unlikely you'll want to disable this unless using an external tool to rebuild the base object.",
"default": true
},
"watch_patterns": {
"type": "array",
"description": "List of glob patterns to watch for changes in the project.\nIf any of these files change, objdiff will automatically rebuild the objects and re-compare them.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax",
"description": "A list of glob patterns to watch for changes.\nWhen these files change, objdiff automatically rebuilds and re-compares objects.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax",
"items": {
"type": "string"
},
"default": [
"*.c",
"*.cc",
"*.cp",
"*.cpp",
"*.cxx",
"*.c++",
"*.h",
"*.hh",
"*.hp",
"*.hpp",
"*.hxx",
"*.h++",
"*.pch",
"*.pch++",
"*.inc",
"*.s",
"*.S",
"*.asm",
"*.inc",
"*.py",
"*.yml",
"*.txt",
@@ -76,7 +82,7 @@
},
"ignore_patterns": {
"type": "array",
"description": "List of glob patterns to explicitly ignore when watching for changes.\nFiles matching these patterns will not trigger a rebuild.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax",
"description": "A list of glob patterns to explicitly ignore when watching for changes.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax",
"items": {
"type": "string"
},
@@ -113,7 +119,7 @@
"properties": {
"name": {
"type": "string",
"description": "The name of the object in the UI. If not specified, the object's path will be used."
"description": "The display name in the UI. Defaults to the object's path."
},
"path": {
"type": "string",
@@ -122,11 +128,11 @@
},
"target_path": {
"type": "string",
"description": "Path to the target object from the project root.\nRequired if path is not specified."
"description": "Path to the \"target\" or \"expected\" object (the intended result)."
},
"base_path": {
"type": "string",
"description": "Path to the base object from the project root.\nRequired if path is not specified."
"description": "Path to the \"base\" or \"current\" object (built from current source code).\nOmit if there is no source object yet."
},
"reverse_fn_order": {
"type": "boolean",
@@ -201,7 +207,7 @@
"properties": {
"complete": {
"type": "boolean",
"description": "Marks the object as \"complete\" (or \"linked\") in the object list.\nThis is useful for marking objects that are fully decompiled. A value of `false` will mark the object as \"incomplete\"."
"description": "Marks the object as \"complete\" (linked) when `true` or \"incomplete\" when `false`."
},
"reverse_fn_order": {
"type": "boolean",
@@ -221,7 +227,7 @@
},
"auto_generated": {
"type": "boolean",
"description": "Hides the object from the object list by default, but still includes it in reports."
"description": "Hides the object from the sidebar but includes it in progress reports."
}
}
},

View File

@@ -74,6 +74,7 @@ ignore = [
#"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-0436", reason = "Unmaintained paste crate is an indirect dependency" },
{ id = "RUSTSEC-2025-0052", reason = "Unmaintained async-std crate is an 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.
@@ -241,8 +242,8 @@ allow-git = []
[sources.allow-org]
# github.com organizations to allow git sources for
github = [
"enarx", # flagset
"encounter",
"gimli-rs", # gimli
]
# gitlab.com organizations to allow git sources for
gitlab = []

View File

@@ -8,8 +8,8 @@ use objdiff_core::{
Report, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
},
config::path::platform_path,
diff, obj,
obj::{SectionKind, SymbolFlag},
diff,
obj::{self, SectionKind, SymbolFlag, SymbolKind},
};
use prost::Message;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
@@ -177,14 +177,16 @@ fn report_object(
.target_path
.as_ref()
.map(|p| {
obj::read::read(p.as_ref(), diff_config).with_context(|| format!("Failed to open {p}"))
obj::read::read(p.as_ref(), diff_config, diff::DiffSide::Target)
.with_context(|| format!("Failed to open {p}"))
})
.transpose()?;
let base = object
.base_path
.as_ref()
.map(|p| {
obj::read::read(p.as_ref(), diff_config).with_context(|| format!("Failed to open {p}"))
obj::read::read(p.as_ref(), diff_config, diff::DiffSide::Base)
.with_context(|| format!("Failed to open {p}"))
})
.transpose()?;
let result =
@@ -245,6 +247,7 @@ fn report_object(
|| symbol.size == 0
|| symbol.flags.contains(SymbolFlag::Hidden)
|| symbol.flags.contains(SymbolFlag::Ignored)
|| symbol.kind == SymbolKind::Section
{
continue;
}

View File

@@ -12,7 +12,7 @@ use rabbitizer::{
use crate::{
arch::{Arch, RelocationOverride, RelocationOverrideTarget},
diff::{DiffObjConfig, MipsAbi, MipsInstrCategory, display::InstructionPart},
diff::{DiffObjConfig, DiffSide, MipsAbi, MipsInstrCategory, display::InstructionPart},
obj::{
InstructionArg, InstructionArgValue, InstructionRef, Relocation, RelocationFlags,
ResolvedInstructionRef, ResolvedRelocation, Section, Symbol, SymbolFlag, SymbolFlagSet,
@@ -27,6 +27,7 @@ pub struct ArchMips {
pub ri_gp_value: i32,
pub paired_relocations: Vec<BTreeMap<u64, i64>>,
pub ignored_symbols: BTreeSet<usize>,
pub diff_side: DiffSide,
}
const EF_MIPS_ABI: u32 = 0x0000F000;
@@ -38,7 +39,7 @@ const EF_MIPS_MACH_5900: u32 = 0x00920000;
const R_MIPS15_S3: u32 = 119;
impl ArchMips {
pub fn new(object: &object::File) -> Result<Self> {
pub fn new(object: &object::File, diff_side: DiffSide) -> Result<Self> {
let mut abi = Abi::O32;
let mut isa_extension = None;
match object.flags() {
@@ -124,7 +125,11 @@ impl ArchMips {
let Ok(name) = obj_symbol.name() else { continue };
if let Some(prefix) = name.strip_suffix(".NON_MATCHING") {
ignored_symbols.insert(obj_symbol.index().0);
if let Some(target_symbol) = object.symbol_by_name(prefix) {
// Only remove the prefixless symbols if we are on the Base side of the diff,
// to allow diffing against target objects that contain `.NON_MATCHING` markers.
if diff_side == DiffSide::Base
&& let Some(target_symbol) = object.symbol_by_name(prefix)
{
ignored_symbols.insert(target_symbol.index().0);
}
}
@@ -137,6 +142,7 @@ impl ArchMips {
ri_gp_value,
paired_relocations,
ignored_symbols,
diff_side,
})
}

View File

@@ -17,7 +17,7 @@ use object::Endian as _;
use crate::{
diff::{
DiffObjConfig,
DiffObjConfig, DiffSide,
display::{ContextItem, HoverItem, InstructionPart},
},
obj::{
@@ -418,15 +418,18 @@ pub trait Arch: Any + Debug + Send + Sync {
}
}
pub fn new_arch(object: &object::File) -> Result<Box<dyn Arch>> {
pub fn new_arch(object: &object::File, diff_side: DiffSide) -> Result<Box<dyn Arch>> {
use object::Object as _;
// Avoid unused warnings on non-mips builds
let _ = diff_side;
Ok(match object.architecture() {
#[cfg(feature = "ppc")]
object::Architecture::PowerPc | object::Architecture::PowerPc64 => {
Box::new(ppc::ArchPpc::new(object)?)
}
#[cfg(feature = "mips")]
object::Architecture::Mips => Box::new(mips::ArchMips::new(object)?),
object::Architecture::Mips => Box::new(mips::ArchMips::new(object, diff_side)?),
#[cfg(feature = "x86")]
object::Architecture::I386 | object::Architecture::X86_64 => {
Box::new(x86::ArchX86::new(object)?)

View File

@@ -21,6 +21,7 @@ use crate::{
obj::{
FlowAnalysisResult, InstructionRef, Object, Relocation, RelocationFlags,
ResolvedInstructionRef, ResolvedRelocation, Section, Symbol, SymbolFlag, SymbolFlagSet,
SymbolKind,
},
};
@@ -675,7 +676,7 @@ fn make_symbol_ref(symbol: &object::Symbol) -> Result<ExtabSymbolRef> {
#[derive(Debug)]
struct PoolReference {
addr_src_gpr: powerpc::GPR,
addr_offset: i16,
addr_offset: i64,
addr_dst_gpr: Option<powerpc::GPR>,
}
@@ -683,16 +684,20 @@ struct PoolReference {
// If so, return information pertaining to where the instruction is getting that address from and
// what it's doing with the address (e.g. copying it into another register, adding an offset, etc).
fn get_pool_reference_for_inst(
opcode: powerpc::Opcode,
ins: powerpc::Ins,
simplified: &powerpc::ParsedIns,
) -> Option<PoolReference> {
use powerpc::{Argument, Opcode};
let args = &simplified.args;
if flow_analysis::guess_data_type_from_load_store_inst_op(opcode).is_some() {
if flow_analysis::guess_data_type_from_load_store_inst_op(ins.op).is_some() {
match (args[1], args[2]) {
(Argument::Offset(offset), Argument::GPR(addr_src_gpr)) => {
// e.g. lwz. Immediate offset.
Some(PoolReference { addr_src_gpr, addr_offset: offset.0, addr_dst_gpr: None })
Some(PoolReference {
addr_src_gpr,
addr_offset: offset.0 as i64,
addr_dst_gpr: None,
})
}
(Argument::GPR(addr_src_gpr), Argument::GPR(_offset_gpr)) => {
// e.g. lwzx. The offset is in a register and was likely calculated from an index.
@@ -712,17 +717,51 @@ fn get_pool_reference_for_inst(
// If either of these match, we also want to return the destination register that the
// address is being copied into so that we can detect any future references to that new
// register as well.
match (opcode, args[0], args[1], args[2]) {
match (ins.op, args[0], args[1], args[2]) {
(
// `addi` or `subi`
Opcode::Addi,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::Simm(simm),
) => Some(PoolReference {
addr_src_gpr,
addr_offset: simm.0,
addr_dst_gpr: Some(addr_dst_gpr),
}),
) => {
let offset = if simplified.mnemonic == "addi" { simm.0 } else { -simm.0 };
Some(PoolReference {
addr_src_gpr,
addr_offset: offset as i64,
addr_dst_gpr: Some(addr_dst_gpr),
})
}
(
// `addis`
Opcode::Addis,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::Uimm(uimm), // Note: `addis` uses UIMM, unlike `addi`, `subi`, and `subis`
) => {
assert_eq!(simplified.mnemonic, "addis");
let offset = (uimm.0 as i64) << 16;
Some(PoolReference {
addr_src_gpr,
addr_offset: offset,
addr_dst_gpr: Some(addr_dst_gpr),
})
}
(
// `subis`
Opcode::Addis,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::Simm(simm),
) => {
assert_eq!(simplified.mnemonic, "subis");
let offset = (simm.0 as i64) << 16;
Some(PoolReference {
addr_src_gpr,
addr_offset: offset,
addr_dst_gpr: Some(addr_dst_gpr),
})
}
(
// `mr` or `mr.`
Opcode::Or,
@@ -777,13 +816,13 @@ fn clear_overwritten_gprs(ins: powerpc::Ins, gpr_pool_relocs: &mut BTreeMap<u8,
// Also, if this instruction is accessing the middle of a symbol instead of the start, we add an
// addend to indicate that.
fn make_fake_pool_reloc(
offset: i16,
offset: i64,
cur_addr: u32,
pool_reloc: &Relocation,
symbols: &[Symbol],
) -> Option<Relocation> {
let pool_reloc = resolve_relocation(symbols, pool_reloc);
let offset_from_pool = pool_reloc.relocation.addend + offset as i64;
let offset_from_pool = pool_reloc.relocation.addend + offset;
let target_address = pool_reloc.symbol.address.checked_add_signed(offset_from_pool)?;
let target_symbol;
let addend;
@@ -794,6 +833,7 @@ fn make_fake_pool_reloc(
&& s.size > 0
&& !s.flags.contains(SymbolFlag::Hidden)
&& !s.flags.contains(SymbolFlag::Ignored)
&& s.kind != SymbolKind::Section
&& (s.address..s.address + s.size).contains(&target_address)
})?;
addend = target_address.checked_sub(symbols[target_symbol].address)? as i64;
@@ -946,7 +986,7 @@ fn generate_fake_pool_relocations_for_function(
clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
}
}
} else if let Some(pool_ref) = get_pool_reference_for_inst(ins.op, &simplified) {
} else if let Some(pool_ref) = get_pool_reference_for_inst(ins, &simplified) {
// This instruction doesn't have a real relocation, so it may be a reference to one of
// the already-loaded pools.
if let Some(pool_reloc) = gpr_pool_relocs.get(&pool_ref.addr_src_gpr.0) {
@@ -965,7 +1005,7 @@ fn generate_fake_pool_relocations_for_function(
// with the offset within the .data section of an array variable into r21.
// Then the body of the loop will `lwzx` one of the array elements from r21.
let mut new_reloc = pool_reloc.clone();
new_reloc.addend += pool_ref.addr_offset as i64;
new_reloc.addend += pool_ref.addr_offset;
gpr_pool_relocs.insert(addr_dst_gpr.0, new_reloc);
} else {
clear_overwritten_gprs(ins, &mut gpr_pool_relocs);

View File

@@ -204,8 +204,9 @@ pub struct ScratchConfig {
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
"*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm",
"*.inc", "*.py", "*.yml", "*.txt", "*.json",
"*.c", "*.cc", "*.cp", "*.cpp", "*.cxx", "*.c++", "*.h", "*.hh", "*.hp", "*.hpp", "*.hxx",
"*.h++", "*.pch", "*.pch++", "*.inc", "*.s", "*.S", "*.asm", "*.py", "*.yml", "*.txt",
"*.json",
];
pub const DEFAULT_IGNORE_PATTERNS: &[&str] = &["build/**/*"];

View File

@@ -41,7 +41,13 @@ pub fn no_diff_code(
instruction_rows.push(InstructionDiffRow { ins_ref: Some(*i), ..Default::default() });
}
resolve_branches(&ops, &mut instruction_rows);
Ok(SymbolDiff { target_symbol: None, match_percent: None, diff_score: None, instruction_rows })
Ok(SymbolDiff {
target_symbol: None,
match_percent: None,
diff_score: None,
instruction_rows,
..Default::default()
})
}
const PENALTY_IMM_DIFF: u64 = 1;
@@ -147,12 +153,14 @@ pub fn diff_code(
match_percent: Some(match_percent),
diff_score: Some((diff_score, max_score)),
instruction_rows: left_rows,
..Default::default()
},
SymbolDiff {
target_symbol: Some(left_symbol_idx),
match_percent: Some(match_percent),
diff_score: Some((diff_score, max_score)),
instruction_rows: right_rows,
..Default::default()
},
))
}
@@ -496,6 +504,14 @@ fn diff_instruction(
result.right_args_diff.push(InstructionArgDiffIndex::new(b_diff));
}
}
if result.kind == InstructionDiffKind::None
&& left_resolved.code.len() != right_resolved.code.len()
{
// If everything else matches but the raw code length differs (e.g. x86 instructions
// with same disassembly but different encoding), mark as op mismatch
result.kind = InstructionDiffKind::OpMismatch;
state.diff_score += PENALTY_REG_DIFF;
}
return Ok(result);
}

View File

@@ -24,17 +24,31 @@ pub fn diff_bss_symbol(
target_symbol: Some(right_symbol_ref),
match_percent: Some(percent),
diff_score: None,
instruction_rows: vec![],
..Default::default()
},
SymbolDiff {
target_symbol: Some(left_symbol_ref),
match_percent: Some(percent),
diff_score: None,
instruction_rows: vec![],
..Default::default()
},
))
}
pub fn symbol_name_matches(left_name: &str, right_name: &str) -> bool {
// Match Metrowerks symbol$1234 against symbol$2345
if let Some((prefix, suffix)) = left_name.split_once('$') {
if !suffix.chars().all(char::is_numeric) {
return false;
}
right_name
.split_once('$')
.is_some_and(|(p, s)| p == prefix && s.chars().all(char::is_numeric))
} else {
left_name == right_name
}
}
fn reloc_eq(
left_obj: &Object,
right_obj: &Object,
@@ -45,8 +59,8 @@ fn reloc_eq(
return false;
}
let symbol_name_addend_matches =
left.symbol.name == right.symbol.name && left.relocation.addend == right.relocation.addend;
let symbol_name_addend_matches = symbol_name_matches(&left.symbol.name, &right.symbol.name)
&& left.relocation.addend == right.relocation.addend;
match (left.symbol.section, right.symbol.section) {
(Some(sl), Some(sr)) => {
// Match if section and name+addend or address match
@@ -70,7 +84,83 @@ pub fn resolve_relocation<'obj>(
ResolvedRelocation { relocation: reloc, symbol }
}
/// Compares relocations contained with a certain data range.
/// Compares the bytes within a certain data range.
fn diff_data_range(left_data: &[u8], right_data: &[u8]) -> (f32, Vec<DataDiff>, Vec<DataDiff>) {
let ops = capture_diff_slices(Algorithm::Patience, left_data, right_data);
let bytes_match_ratio = get_diff_ratio(&ops, left_data.len(), right_data.len());
let mut left_data_diff = Vec::<DataDiff>::new();
let mut right_data_diff = Vec::<DataDiff>::new();
for op in ops {
let (tag, left_range, right_range) = op.as_tag_tuple();
let left_len = left_range.len();
let right_len = right_range.len();
let mut len = left_len.max(right_len);
let kind = match tag {
similar::DiffTag::Equal => DataDiffKind::None,
similar::DiffTag::Delete => DataDiffKind::Delete,
similar::DiffTag::Insert => DataDiffKind::Insert,
similar::DiffTag::Replace => {
// Ensure replacements are equal length
len = left_len.min(right_len);
DataDiffKind::Replace
}
};
let left_data = &left_data[left_range];
let right_data = &right_data[right_range];
left_data_diff.push(DataDiff {
data: left_data[..len.min(left_data.len())].to_vec(),
kind,
len,
..Default::default()
});
right_data_diff.push(DataDiff {
data: right_data[..len.min(right_data.len())].to_vec(),
kind,
len,
..Default::default()
});
if kind == DataDiffKind::Replace {
match left_len.cmp(&right_len) {
Ordering::Less => {
let len = right_len - left_len;
left_data_diff.push(DataDiff {
data: vec![],
kind: DataDiffKind::Insert,
len,
..Default::default()
});
right_data_diff.push(DataDiff {
data: right_data[left_len..right_len].to_vec(),
kind: DataDiffKind::Insert,
len,
..Default::default()
});
}
Ordering::Greater => {
let len = left_len - right_len;
left_data_diff.push(DataDiff {
data: left_data[right_len..left_len].to_vec(),
kind: DataDiffKind::Delete,
len,
..Default::default()
});
right_data_diff.push(DataDiff {
data: vec![],
kind: DataDiffKind::Delete,
len,
..Default::default()
});
}
Ordering::Equal => {}
}
}
}
(bytes_match_ratio, left_data_diff, right_data_diff)
}
/// Compares relocations contained within a certain data range.
fn diff_data_relocs_for_range<'left, 'right>(
left_obj: &'left Object,
right_obj: &'right Object,
@@ -172,76 +262,10 @@ pub fn diff_data_section(
.min(right_section.size);
let left_data = &left_section.data[..left_max as usize];
let right_data = &right_section.data[..right_max as usize];
let ops = capture_diff_slices(Algorithm::Patience, left_data, right_data);
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
let mut left_data_diff = Vec::<DataDiff>::new();
let mut right_data_diff = Vec::<DataDiff>::new();
for op in ops {
let (tag, left_range, right_range) = op.as_tag_tuple();
let left_len = left_range.len();
let right_len = right_range.len();
let mut len = left_len.max(right_len);
let kind = match tag {
similar::DiffTag::Equal => DataDiffKind::None,
similar::DiffTag::Delete => DataDiffKind::Delete,
similar::DiffTag::Insert => DataDiffKind::Insert,
similar::DiffTag::Replace => {
// Ensure replacements are equal length
len = left_len.min(right_len);
DataDiffKind::Replace
}
};
let left_data = &left_section.data[left_range];
let right_data = &right_section.data[right_range];
left_data_diff.push(DataDiff {
data: left_data[..len.min(left_data.len())].to_vec(),
kind,
len,
..Default::default()
});
right_data_diff.push(DataDiff {
data: right_data[..len.min(right_data.len())].to_vec(),
kind,
len,
..Default::default()
});
if kind == DataDiffKind::Replace {
match left_len.cmp(&right_len) {
Ordering::Less => {
let len = right_len - left_len;
left_data_diff.push(DataDiff {
data: vec![],
kind: DataDiffKind::Insert,
len,
..Default::default()
});
right_data_diff.push(DataDiff {
data: right_data[left_len..right_len].to_vec(),
kind: DataDiffKind::Insert,
len,
..Default::default()
});
}
Ordering::Greater => {
let len = left_len - right_len;
left_data_diff.push(DataDiff {
data: left_data[right_len..left_len].to_vec(),
kind: DataDiffKind::Delete,
len,
..Default::default()
});
right_data_diff.push(DataDiff {
data: vec![],
kind: DataDiffKind::Delete,
len,
..Default::default()
});
}
Ordering::Equal => {}
}
}
}
let (bytes_match_ratio, left_data_diff, right_data_diff) =
diff_data_range(left_data, right_data);
let match_percent = bytes_match_ratio * 100.0;
let mut left_reloc_diffs = Vec::new();
let mut right_reloc_diffs = Vec::new();
@@ -300,6 +324,55 @@ pub fn diff_data_section(
Ok((left_section_diff, right_section_diff))
}
pub fn no_diff_data_symbol(obj: &Object, symbol_index: usize) -> Result<SymbolDiff> {
let symbol = &obj.symbols[symbol_index];
let section_idx = symbol.section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
let section = &obj.sections[section_idx];
let start = symbol
.address
.checked_sub(section.address)
.ok_or_else(|| anyhow!("Symbol address out of section bounds"))?;
let end = start + symbol.size;
if end > section.size {
return Err(anyhow!(
"Symbol {} size out of section bounds ({} > {})",
symbol.name,
end,
section.size
));
}
let range = start as usize..end as usize;
let data = &section.data[range.clone()];
let len = symbol.size as usize;
let data_diff =
vec![DataDiff { data: data.to_vec(), kind: DataDiffKind::None, len, ..Default::default() }];
let mut reloc_diffs = Vec::new();
for reloc in section.relocations.iter() {
if !range.contains(&(reloc.address as usize)) {
continue;
}
let reloc_len = obj.arch.data_reloc_size(reloc.flags);
let range = reloc.address as usize..reloc.address as usize + reloc_len;
reloc_diffs.push(DataRelocationDiff {
reloc: reloc.clone(),
kind: DataDiffKind::None,
range,
});
}
Ok(SymbolDiff {
target_symbol: None,
match_percent: None,
diff_score: None,
data_diff,
data_reloc_diff: reloc_diffs,
..Default::default()
})
}
pub fn diff_data_symbol(
left_obj: &Object,
right_obj: &Object,
@@ -348,6 +421,9 @@ pub fn diff_data_symbol(
let left_data = &left_section.data[left_range.clone()];
let right_data = &right_section.data[right_range.clone()];
let (bytes_match_ratio, left_data_diff, right_data_diff) =
diff_data_range(left_data, right_data);
let reloc_diffs = diff_data_relocs_for_range(
left_obj,
right_obj,
@@ -357,10 +433,9 @@ pub fn diff_data_symbol(
right_range,
);
let ops = capture_diff_slices(Algorithm::Patience, left_data, right_data);
let bytes_match_ratio = get_diff_ratio(&ops, left_data.len(), right_data.len());
let mut match_ratio = bytes_match_ratio;
let mut left_reloc_diffs = Vec::new();
let mut right_reloc_diffs = Vec::new();
if !reloc_diffs.is_empty() {
let mut total_reloc_bytes = 0;
let mut matching_reloc_bytes = 0;
@@ -376,6 +451,27 @@ pub fn diff_data_symbol(
if diff_kind == DataDiffKind::None {
matching_reloc_bytes += reloc_diff_len;
}
if let Some(left_reloc) = left_reloc {
let len = left_obj.arch.data_reloc_size(left_reloc.relocation.flags);
let range = left_reloc.relocation.address as usize
..left_reloc.relocation.address as usize + len;
left_reloc_diffs.push(DataRelocationDiff {
reloc: left_reloc.relocation.clone(),
kind: diff_kind,
range,
});
}
if let Some(right_reloc) = right_reloc {
let len = right_obj.arch.data_reloc_size(right_reloc.relocation.flags);
let range = right_reloc.relocation.address as usize
..right_reloc.relocation.address as usize + len;
right_reloc_diffs.push(DataRelocationDiff {
reloc: right_reloc.relocation.clone(),
kind: diff_kind,
range,
});
}
}
if total_reloc_bytes > 0 {
let relocs_match_ratio = matching_reloc_bytes as f32 / total_reloc_bytes as f32;
@@ -397,13 +493,17 @@ pub fn diff_data_symbol(
target_symbol: Some(right_symbol_idx),
match_percent: Some(match_percent),
diff_score: None,
instruction_rows: vec![],
data_diff: left_data_diff,
data_reloc_diff: left_reloc_diffs,
..Default::default()
},
SymbolDiff {
target_symbol: Some(left_symbol_idx),
match_percent: Some(match_percent),
diff_score: None,
instruction_rows: vec![],
data_diff: right_data_diff,
data_reloc_diff: right_reloc_diffs,
..Default::default()
},
))
}

View File

@@ -379,7 +379,9 @@ pub enum HoverItem {
}
pub fn symbol_context(obj: &Object, symbol_index: usize) -> Vec<ContextItem> {
let symbol = &obj.symbols[symbol_index];
let Some(symbol) = obj.symbols.get(symbol_index) else {
return Vec::new();
};
let mut out = Vec::new();
out.push(ContextItem::Copy { value: symbol.name.clone(), label: None });
if let Some(name) = &symbol.demangled_name {
@@ -403,7 +405,9 @@ pub fn symbol_hover(
addend: i64,
override_color: Option<HoverItemColor>,
) -> Vec<HoverItem> {
let symbol = &obj.symbols[symbol_index];
let Some(symbol) = obj.symbols.get(symbol_index) else {
return Vec::new();
};
let addend_str = match addend.cmp(&0i64) {
Ordering::Greater => format!("+{addend:x}"),
Ordering::Less => format!("-{:x}", -addend),

View File

@@ -13,7 +13,8 @@ use crate::{
code::{diff_code, no_diff_code},
data::{
diff_bss_section, diff_bss_symbol, diff_data_section, diff_data_symbol,
diff_generic_section, no_diff_bss_section, no_diff_data_section,
diff_generic_section, no_diff_bss_section, no_diff_data_section, no_diff_data_symbol,
symbol_name_matches,
},
},
obj::{InstructionRef, Object, Relocation, SectionKind, Symbol, SymbolFlag},
@@ -44,6 +45,8 @@ pub struct SymbolDiff {
pub match_percent: Option<f32>,
pub diff_score: Option<(u64, u64)>,
pub instruction_rows: Vec<InstructionDiffRow>,
pub data_diff: Vec<DataDiff>,
pub data_reloc_diff: Vec<DataRelocationDiff>,
}
#[derive(Debug, Clone, Default)]
@@ -163,7 +166,7 @@ impl ObjectDiff {
target_symbol: None,
match_percent: None,
diff_score: None,
instruction_rows: vec![],
..Default::default()
});
}
for _ in obj.sections.iter() {
@@ -262,7 +265,11 @@ pub fn diff_objs(
left_out.symbols[left_symbol_ref] =
no_diff_code(left_obj, left_symbol_ref, diff_config)?;
}
SectionKind::Data | SectionKind::Bss | SectionKind::Common => {
SectionKind::Data => {
left_out.symbols[left_symbol_ref] =
no_diff_data_symbol(left_obj, left_symbol_ref)?;
}
SectionKind::Bss | SectionKind::Common => {
// Nothing needs to be done
}
SectionKind::Unknown => unreachable!(),
@@ -275,7 +282,11 @@ pub fn diff_objs(
right_out.symbols[right_symbol_ref] =
no_diff_code(right_obj, right_symbol_ref, diff_config)?;
}
SectionKind::Data | SectionKind::Bss | SectionKind::Common => {
SectionKind::Data => {
right_out.symbols[right_symbol_ref] =
no_diff_data_symbol(right_obj, right_symbol_ref)?;
}
SectionKind::Bss | SectionKind::Common => {
// Nothing needs to be done
}
SectionKind::Unknown => unreachable!(),
@@ -575,47 +586,60 @@ fn matching_symbols(
&mut matches,
)?;
}
for (symbol_idx, symbol) in left.symbols.iter().enumerate() {
if symbol.size == 0 || symbol.flags.contains(SymbolFlag::Ignored) {
continue;
}
let section_kind = symbol_section_kind(left, symbol);
if section_kind == SectionKind::Unknown {
continue;
}
if left_used.contains(&symbol_idx) {
continue;
}
let symbol_match = SymbolMatch {
left: Some(symbol_idx),
right: find_symbol(right, left, symbol, Some(&right_used)),
prev: find_symbol(prev, left, symbol, None),
section_kind,
};
matches.push(symbol_match);
if let Some(right) = symbol_match.right {
right_used.insert(right);
// Do two passes for nameless literals. The first only pairs up perfect matches to ensure
// those are correct first, while the second pass catches near matches.
for fuzzy_literals in [false, true] {
for (symbol_idx, symbol) in left.symbols.iter().enumerate() {
if symbol.size == 0 || symbol.flags.contains(SymbolFlag::Ignored) {
continue;
}
let section_kind = symbol_section_kind(left, symbol);
if section_kind == SectionKind::Unknown {
continue;
}
if left_used.contains(&symbol_idx) {
continue;
}
let symbol_match = SymbolMatch {
left: Some(symbol_idx),
right: find_symbol(right, left, symbol_idx, Some(&right_used), fuzzy_literals),
prev: find_symbol(prev, left, symbol_idx, None, fuzzy_literals),
section_kind,
};
matches.push(symbol_match);
if let Some(right) = symbol_match.right {
left_used.insert(symbol_idx);
right_used.insert(right);
}
}
}
}
if let Some(right) = right {
for (symbol_idx, symbol) in right.symbols.iter().enumerate() {
if symbol.size == 0 || symbol.flags.contains(SymbolFlag::Ignored) {
continue;
// Do two passes for nameless literals. The first only pairs up perfect matches to ensure
// those are correct first, while the second pass catches near matches.
for fuzzy_literals in [false, true] {
for (symbol_idx, symbol) in right.symbols.iter().enumerate() {
if symbol.size == 0 || symbol.flags.contains(SymbolFlag::Ignored) {
continue;
}
let section_kind = symbol_section_kind(right, symbol);
if section_kind == SectionKind::Unknown {
continue;
}
if right_used.contains(&symbol_idx) {
continue;
}
let symbol_match = SymbolMatch {
left: None,
right: Some(symbol_idx),
prev: find_symbol(prev, right, symbol_idx, None, fuzzy_literals),
section_kind,
};
matches.push(symbol_match);
if symbol_match.prev.is_some() {
right_used.insert(symbol_idx);
}
}
let section_kind = symbol_section_kind(right, symbol);
if section_kind == SectionKind::Unknown {
continue;
}
if right_used.contains(&symbol_idx) {
continue;
}
matches.push(SymbolMatch {
left: None,
right: Some(symbol_idx),
prev: find_symbol(prev, right, symbol, None),
section_kind,
});
}
}
Ok(matches)
@@ -637,7 +661,10 @@ where
fn symbol_section<'obj>(obj: &'obj Object, symbol: &Symbol) -> Option<(&'obj str, SectionKind)> {
if let Some(section) = symbol.section.and_then(|section_idx| obj.sections.get(section_idx)) {
Some((section.name.as_str(), section.kind))
// Match x86 .rdata$r against .rdata$rs
let section_name =
section.name.split_once('$').map_or(section.name.as_str(), |(prefix, _)| prefix);
Some((section_name, section.kind))
} else if symbol.flags.contains(SymbolFlag::Common) {
Some((".comm", SectionKind::Common))
} else {
@@ -653,52 +680,94 @@ fn symbol_section_kind(obj: &Object, symbol: &Symbol) -> SectionKind {
}
}
/// Check if a symbol is a compiler-generated literal like @1234.
fn is_symbol_compiler_generated_literal(symbol: &Symbol) -> bool {
if !symbol.name.starts_with('@') {
return false;
}
if !symbol.name[1..].chars().all(char::is_numeric) {
// Exclude @stringBase0, @GUARD@, etc.
return false;
}
true
}
fn find_symbol(
obj: Option<&Object>,
in_obj: &Object,
in_symbol: &Symbol,
in_symbol_idx: usize,
used: Option<&BTreeSet<usize>>,
fuzzy_literals: bool,
) -> Option<usize> {
let in_symbol = &in_obj.symbols[in_symbol_idx];
let obj = obj?;
let (section_name, section_kind) = symbol_section(in_obj, in_symbol)?;
// Match compiler-generated symbols against each other (e.g. @251 -> @60)
// If they are in the same section and have the same value
if is_symbol_compiler_generated_literal(in_symbol)
&& matches!(section_kind, SectionKind::Data | SectionKind::Bss)
{
let mut closest_match_symbol_idx = None;
let mut closest_match_percent = 0.0;
for (symbol_idx, symbol) in unmatched_symbols(obj, used) {
let Some(section_index) = symbol.section else {
continue;
};
if obj.sections[section_index].name != section_name {
continue;
}
if !is_symbol_compiler_generated_literal(symbol) {
continue;
}
match section_kind {
SectionKind::Data => {
// For data, pick the first symbol with exactly matching bytes and relocations.
// If no symbols match exactly, and `fuzzy_literals` is true, pick the closest
// plausible match instead.
if let Ok((left_diff, _right_diff)) =
diff_data_symbol(in_obj, obj, in_symbol_idx, symbol_idx)
&& let Some(match_percent) = left_diff.match_percent
&& (match_percent == 100.0
|| (fuzzy_literals
&& match_percent >= 50.0
&& match_percent > closest_match_percent))
{
closest_match_symbol_idx = Some(symbol_idx);
closest_match_percent = match_percent;
if match_percent == 100.0 {
break;
}
}
}
SectionKind::Bss => {
// For BSS, pick the first symbol that has the exact matching size.
if in_symbol.size == symbol.size {
closest_match_symbol_idx = Some(symbol_idx);
break;
}
}
_ => unreachable!(),
}
}
return closest_match_symbol_idx;
}
// Try to find an exact name match
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|(_, symbol)| {
symbol.name == in_symbol.name && symbol_section_kind(obj, symbol) == section_kind
}) {
return Some(symbol_idx);
}
// Match compiler-generated symbols against each other (e.g. @251 -> @60)
// If they are at the same address in the same section
if in_symbol.name.starts_with('@')
&& matches!(section_kind, SectionKind::Data | SectionKind::Bss)
&& let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|(_, symbol)| {
let Some(section_index) = symbol.section else {
return false;
};
symbol.name.starts_with('@')
&& symbol.address == in_symbol.address
&& obj.sections[section_index].name == section_name
})
{
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|&(_, symbol)| {
symbol_name_matches(&in_symbol.name, &symbol.name)
&& symbol_section_kind(obj, symbol) == section_kind
&& symbol_section(obj, symbol).is_some_and(|(name, _)| name == section_name)
}) {
return Some(symbol_idx);
}
// Match Metrowerks symbol$1234 against symbol$2345
if let Some((prefix, suffix)) = in_symbol.name.split_once('$') {
if !suffix.chars().all(char::is_numeric) {
return None;
}
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|&(_, symbol)| {
if let Some((p, s)) = symbol.name.split_once('$') {
prefix == p
&& s.chars().all(char::is_numeric)
&& symbol_section_kind(obj, symbol) == section_kind
} else {
false
}
}) {
return Some(symbol_idx);
}
}
None
}
@@ -749,3 +818,11 @@ fn find_section(
s.kind == section_kind && s.name == name && !matches.iter().any(|m| m.right == Some(i))
})
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DiffSide {
/// The target/expected side of the diff.
Target,
/// The base side of the diff.
Base,
}

View File

@@ -170,13 +170,6 @@ pub enum JobResult {
CreateScratch(Option<Box<CreateScratchResult>>),
}
fn should_cancel(rx: &Receiver<()>) -> bool {
match rx.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => true,
Err(_) => false,
}
}
fn start_job(
waker: Waker,
title: &str,
@@ -203,7 +196,6 @@ fn start_job(
}
});
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
// log::info!("Started job {}", id); TODO
JobState { id, kind, handle: Some(handle), context, cancel: tx }
}
@@ -228,3 +220,10 @@ fn update_status(
context.waker.wake_by_ref();
Ok(())
}
fn should_cancel(rx: &Receiver<()>) -> bool {
match rx.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => true,
Err(_) => false,
}
}

View File

@@ -6,7 +6,7 @@ use typed_path::Utf8PlatformPathBuf;
use crate::{
build::{BuildConfig, BuildStatus, run_make},
diff::{DiffObjConfig, MappingConfig, ObjectDiff, diff_objs},
diff::{DiffObjConfig, DiffSide, MappingConfig, ObjectDiff, diff_objs},
jobs::{Job, JobContext, JobResult, JobState, start_job, update_status},
obj::{Object, read},
};
@@ -117,7 +117,7 @@ fn run_build(
&cancel,
)?;
step_idx += 1;
match read::read(target_path.as_ref(), &config.diff_obj_config) {
match read::read(target_path.as_ref(), &config.diff_obj_config, DiffSide::Target) {
Ok(obj) => Some(obj),
Err(e) => {
first_status = BuildStatus {
@@ -141,7 +141,7 @@ fn run_build(
Some(base_path) if second_status.success => {
update_status(context, format!("Loading base {base_path}"), step_idx, total, &cancel)?;
step_idx += 1;
match read::read(base_path.as_ref(), &config.diff_obj_config) {
match read::read(base_path.as_ref(), &config.diff_obj_config, DiffSide::Base) {
Ok(obj) => Some(obj),
Err(e) => {
second_status = BuildStatus {

View File

@@ -12,10 +12,10 @@ use object::{Object as _, ObjectSection as _, ObjectSymbol as _};
use crate::{
arch::{Arch, RelocationOverride, RelocationOverrideTarget, new_arch},
diff::DiffObjConfig,
diff::{DiffObjConfig, DiffSide},
obj::{
FlowAnalysisResult, Object, Relocation, RelocationFlags, Section, SectionData, SectionFlag,
SectionKind, Symbol, SymbolFlag, SymbolKind,
SectionKind, Symbol, SymbolFlag, SymbolFlagSet, SymbolKind,
split_meta::{SPLITMETA_SECTION, SplitMeta},
},
util::{align_data_slice_to, align_u64_to, read_u16, read_u32},
@@ -74,6 +74,14 @@ fn map_symbol(
{
flags |= SymbolFlag::Hidden;
}
if file.format() == object::BinaryFormat::Coff
&& let Ok(name) = symbol.name()
&& (name.starts_with("except_data_")
|| name.starts_with("__unwind")
|| name.starts_with("__catch"))
{
flags |= SymbolFlag::Hidden;
}
let kind = match symbol.kind() {
object::SymbolKind::Text => SymbolKind::Function,
@@ -110,7 +118,7 @@ fn map_symbols(
split_meta: Option<&SplitMeta>,
) -> Result<(Vec<Symbol>, Vec<usize>)> {
let symbol_count = obj_file.symbols().count();
let mut symbols = Vec::<Symbol>::with_capacity(symbol_count);
let mut symbols = Vec::<Symbol>::with_capacity(symbol_count + obj_file.sections().count());
let mut symbol_indices = Vec::<usize>::with_capacity(symbol_count + 1);
for obj_symbol in obj_file.symbols() {
if symbol_indices.len() <= obj_symbol.index().0 {
@@ -127,10 +135,56 @@ fn map_symbols(
Ok((symbols, symbol_indices))
}
/// Add an extra fake symbol to the start of each data section in order to allow the user to diff
/// all of the data in the section at once by clicking on this fake symbol at the top of the list.
fn add_section_symbols(sections: &[Section], symbols: &mut Vec<Symbol>) {
for (section_idx, section) in sections.iter().enumerate() {
if section.kind != SectionKind::Data {
continue;
}
// Instead of naming the fake section symbol after `section.name` (e.g. ".data") we use
// `section.id` (e.g. ".data-0") so that it is unique when multiple sections with the same
// name exist and it also doesn't conflict with any real section symbols from the object.
let name = if section.flags.contains(SectionFlag::Combined) {
// For combined sections, `section.id` (e.g. ".data-combined") is inconsistent with
// uncombined section IDs, so we add the "-0" suffix to the name to enable proper
// pairing when one side had multiple sections combined and the other only had one
// section to begin with.
format!("[{}-0]", section.name)
} else {
format!("[{}]", section.id)
};
// `section.size` can include extra padding, so instead prefer using the address that the
// last symbol ends at when there are any symbols in the section.
let size = symbols
.iter()
.filter(|s| {
s.section == Some(section_idx) && s.kind == SymbolKind::Object && s.size > 0
})
.map(|s| s.address + s.size)
.max()
.unwrap_or(section.size);
symbols.push(Symbol {
name,
demangled_name: None,
address: 0,
size,
kind: SymbolKind::Section,
section: Some(section_idx),
flags: SymbolFlagSet::default() | SymbolFlag::Local,
align: None,
virtual_address: None,
});
}
}
/// When inferring a symbol's size, we ignore symbols that start with specific prefixes. They are
/// usually emitted as branch targets and do not represent the start of a function or object.
fn is_local_label(symbol: &Symbol) -> bool {
const LABEL_PREFIXES: &[&str] = &[".L", "LAB_"];
const LABEL_PREFIXES: &[&str] = &[".L", "LAB_", "switchD_"];
symbol.size == 0
&& symbol.flags.contains(SymbolFlag::Local)
&& LABEL_PREFIXES.iter().any(|p| symbol.name.starts_with(p))
@@ -208,7 +262,11 @@ fn infer_symbol_sizes(arch: &dyn Arch, symbols: &mut [Symbol], sections: &[Secti
let section = &sections[section_idx];
let next_address =
next_symbol.map(|s| s.address).unwrap_or_else(|| section.address + section.size);
let new_size = if section.kind == SectionKind::Code {
let new_size = if symbol.kind == SymbolKind::Section && section.kind == SectionKind::Data {
// Data sections already have always-visible section symbols created by objdiff to allow
// diffing them, so no need to unhide these.
0
} else if section.kind == SectionKind::Code {
arch.infer_function_size(symbol, section, next_address)?
} else {
next_address.saturating_sub(symbol.address)
@@ -917,21 +975,25 @@ fn do_combine_sections(
}
#[cfg(feature = "std")]
pub fn read(obj_path: &std::path::Path, config: &DiffObjConfig) -> Result<Object> {
pub fn read(
obj_path: &std::path::Path,
config: &DiffObjConfig,
diff_side: DiffSide,
) -> Result<Object> {
let (data, timestamp) = {
let file = std::fs::File::open(obj_path)?;
let timestamp = filetime::FileTime::from_last_modification_time(&file.metadata()?);
(unsafe { memmap2::Mmap::map(&file) }?, timestamp)
};
let mut obj = parse(&data, config)?;
let mut obj = parse(&data, config, diff_side)?;
obj.path = Some(obj_path.to_path_buf());
obj.timestamp = Some(timestamp);
Ok(obj)
}
pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<Object> {
pub fn parse(data: &[u8], config: &DiffObjConfig, diff_side: DiffSide) -> Result<Object> {
let obj_file = object::File::parse(data)?;
let mut arch = new_arch(&obj_file)?;
let mut arch = new_arch(&obj_file, diff_side)?;
let split_meta = parse_split_meta(&obj_file)?;
let (mut sections, section_indices) =
map_sections(arch.as_ref(), &obj_file, split_meta.as_ref())?;
@@ -942,6 +1004,7 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<Object> {
if config.combine_data_sections || config.combine_text_sections {
combine_sections(&mut sections, &mut symbols, config)?;
}
add_section_symbols(&sections, &mut symbols);
arch.post_init(&sections, &symbols);
let mut obj = Object {
arch,

View File

@@ -6,7 +6,12 @@ mod common;
#[cfg(feature = "arm")]
fn read_arm() {
let diff_config = diff::DiffObjConfig { ..Default::default() };
let obj = obj::read::parse(include_object!("data/arm/LinkStateItem.o"), &diff_config).unwrap();
let obj = obj::read::parse(
include_object!("data/arm/LinkStateItem.o"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
insta::assert_debug_snapshot!(obj);
let symbol_idx =
obj.symbols.iter().position(|s| s.name == "_ZN13LinkStateItem12OnStateLeaveEi").unwrap();
@@ -20,7 +25,9 @@ fn read_arm() {
#[cfg(feature = "arm")]
fn read_thumb() {
let diff_config = diff::DiffObjConfig { ..Default::default() };
let obj = obj::read::parse(include_object!("data/arm/thumb.o"), &diff_config).unwrap();
let obj =
obj::read::parse(include_object!("data/arm/thumb.o"), &diff_config, diff::DiffSide::Base)
.unwrap();
insta::assert_debug_snapshot!(obj);
let symbol_idx = obj
.symbols
@@ -37,7 +44,12 @@ fn read_thumb() {
#[cfg(feature = "arm")]
fn combine_text_sections() {
let diff_config = diff::DiffObjConfig { combine_text_sections: true, ..Default::default() };
let obj = obj::read::parse(include_object!("data/arm/enemy300.o"), &diff_config).unwrap();
let obj = obj::read::parse(
include_object!("data/arm/enemy300.o"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
let symbol_idx = obj.symbols.iter().position(|s| s.name == "Enemy300Draw").unwrap();
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
insta::assert_debug_snapshot!(diff.instruction_rows);

View File

@@ -6,7 +6,9 @@ mod common;
#[cfg(feature = "mips")]
fn read_mips() {
let diff_config = diff::DiffObjConfig { mips_register_prefix: true, ..Default::default() };
let obj = obj::read::parse(include_object!("data/mips/main.c.o"), &diff_config).unwrap();
let obj =
obj::read::parse(include_object!("data/mips/main.c.o"), &diff_config, diff::DiffSide::Base)
.unwrap();
insta::assert_debug_snapshot!(obj);
let symbol_idx = obj.symbols.iter().position(|s| s.name == "ControlEntry").unwrap();
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
@@ -19,9 +21,19 @@ fn read_mips() {
#[cfg(feature = "mips")]
fn cross_endian_diff() {
let diff_config = diff::DiffObjConfig::default();
let obj_be = obj::read::parse(include_object!("data/mips/code_be.o"), &diff_config).unwrap();
let obj_be = obj::read::parse(
include_object!("data/mips/code_be.o"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
assert_eq!(obj_be.endianness, object::Endianness::Big);
let obj_le = obj::read::parse(include_object!("data/mips/code_le.o"), &diff_config).unwrap();
let obj_le = obj::read::parse(
include_object!("data/mips/code_le.o"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
assert_eq!(obj_le.endianness, object::Endianness::Little);
let left_symbol_idx = obj_be.symbols.iter().position(|s| s.name == "func_00000000").unwrap();
let right_symbol_idx =
@@ -42,6 +54,11 @@ fn cross_endian_diff() {
#[cfg(feature = "mips")]
fn filter_non_matching() {
let diff_config = diff::DiffObjConfig::default();
let obj = obj::read::parse(include_object!("data/mips/vw_main.c.o"), &diff_config).unwrap();
let obj = obj::read::parse(
include_object!("data/mips/vw_main.c.o"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
insta::assert_debug_snapshot!(obj.symbols);
}

View File

@@ -10,7 +10,9 @@ mod common;
#[cfg(feature = "ppc")]
fn read_ppc() {
let diff_config = diff::DiffObjConfig::default();
let obj = obj::read::parse(include_object!("data/ppc/IObj.o"), &diff_config).unwrap();
let obj =
obj::read::parse(include_object!("data/ppc/IObj.o"), &diff_config, diff::DiffSide::Base)
.unwrap();
insta::assert_debug_snapshot!(obj);
let symbol_idx =
obj.symbols.iter().position(|s| s.name == "Type2Text__10SObjectTagFUi").unwrap();
@@ -24,7 +26,12 @@ fn read_ppc() {
#[cfg(feature = "ppc")]
fn read_dwarf1_line_info() {
let diff_config = diff::DiffObjConfig::default();
let obj = obj::read::parse(include_object!("data/ppc/m_Do_hostIO.o"), &diff_config).unwrap();
let obj = obj::read::parse(
include_object!("data/ppc/m_Do_hostIO.o"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
let line_infos = obj
.sections
.iter()
@@ -38,7 +45,12 @@ fn read_dwarf1_line_info() {
#[cfg(feature = "ppc")]
fn read_extab() {
let diff_config = diff::DiffObjConfig::default();
let obj = obj::read::parse(include_object!("data/ppc/NMWException.o"), &diff_config).unwrap();
let obj = obj::read::parse(
include_object!("data/ppc/NMWException.o"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
insta::assert_debug_snapshot!(obj);
}
@@ -47,12 +59,18 @@ fn read_extab() {
fn diff_ppc() {
let diff_config = diff::DiffObjConfig::default();
let mapping_config = diff::MappingConfig::default();
let target_obj =
obj::read::parse(include_object!("data/ppc/CDamageVulnerability_target.o"), &diff_config)
.unwrap();
let base_obj =
obj::read::parse(include_object!("data/ppc/CDamageVulnerability_base.o"), &diff_config)
.unwrap();
let target_obj = obj::read::parse(
include_object!("data/ppc/CDamageVulnerability_target.o"),
&diff_config,
diff::DiffSide::Target,
)
.unwrap();
let base_obj = obj::read::parse(
include_object!("data/ppc/CDamageVulnerability_base.o"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
let diff =
diff::diff_objs(Some(&target_obj), Some(&base_obj), None, &diff_config, &mapping_config)
.unwrap();
@@ -90,7 +108,12 @@ fn diff_ppc() {
#[cfg(feature = "ppc")]
fn read_vmx128_coff() {
let diff_config = diff::DiffObjConfig { combine_data_sections: true, ..Default::default() };
let obj = obj::read::parse(include_object!("data/ppc/vmx128.obj"), &diff_config).unwrap();
let obj = obj::read::parse(
include_object!("data/ppc/vmx128.obj"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
insta::assert_debug_snapshot!(obj);
let symbol_idx =
obj.symbols.iter().position(|s| s.name == "?FloatingPointExample@@YAXXZ").unwrap();

View File

@@ -6,7 +6,12 @@ mod common;
#[cfg(feature = "x86")]
fn read_x86() {
let diff_config = diff::DiffObjConfig::default();
let obj = obj::read::parse(include_object!("data/x86/staticdebug.obj"), &diff_config).unwrap();
let obj = obj::read::parse(
include_object!("data/x86/staticdebug.obj"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
insta::assert_debug_snapshot!(obj);
let symbol_idx = obj.symbols.iter().position(|s| s.name == "?PrintThing@@YAXXZ").unwrap();
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
@@ -23,7 +28,9 @@ fn read_x86_combine_sections() {
combine_text_sections: true,
..Default::default()
};
let obj = obj::read::parse(include_object!("data/x86/rtest.obj"), &diff_config).unwrap();
let obj =
obj::read::parse(include_object!("data/x86/rtest.obj"), &diff_config, diff::DiffSide::Base)
.unwrap();
insta::assert_debug_snapshot!(obj.sections);
}
@@ -31,7 +38,12 @@ fn read_x86_combine_sections() {
#[cfg(feature = "x86")]
fn read_x86_64() {
let diff_config = diff::DiffObjConfig::default();
let obj = obj::read::parse(include_object!("data/x86_64/vs2022.o"), &diff_config).unwrap();
let obj = obj::read::parse(
include_object!("data/x86_64/vs2022.o"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
insta::assert_debug_snapshot!(obj);
let symbol_idx =
obj.symbols.iter().position(|s| s.name == "?Dot@Vector@@QEAAMPEAU1@@Z").unwrap();
@@ -45,7 +57,12 @@ fn read_x86_64() {
#[cfg(feature = "x86")]
fn display_section_ordering() {
let diff_config = diff::DiffObjConfig::default();
let obj = obj::read::parse(include_object!("data/x86/basenode.obj"), &diff_config).unwrap();
let obj = obj::read::parse(
include_object!("data/x86/basenode.obj"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
let obj_diff =
diff::diff_objs(Some(&obj), None, None, &diff_config, &diff::MappingConfig::default())
.unwrap()
@@ -60,7 +77,12 @@ fn display_section_ordering() {
#[cfg(feature = "x86")]
fn read_x86_jumptable() {
let diff_config = diff::DiffObjConfig::default();
let obj = obj::read::parse(include_object!("data/x86/jumptable.o"), &diff_config).unwrap();
let obj = obj::read::parse(
include_object!("data/x86/jumptable.o"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
insta::assert_debug_snapshot!(obj);
let symbol_idx = obj.symbols.iter().position(|s| s.name == "?test@@YAHH@Z").unwrap();
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
@@ -74,6 +96,11 @@ fn read_x86_jumptable() {
#[cfg(feature = "x86")]
fn read_x86_local_labels() {
let diff_config = diff::DiffObjConfig::default();
let obj = obj::read::parse(include_object!("data/x86/local_labels.obj"), &diff_config).unwrap();
let obj = obj::read::parse(
include_object!("data/x86/local_labels.obj"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
insta::assert_debug_snapshot!(obj);
}

View File

@@ -1507,6 +1507,19 @@ Object {
align: None,
virtual_address: None,
},
Symbol {
name: "[.data-0]",
demangled_name: None,
address: 0,
size: 76,
kind: Section,
section: Some(
2,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
],
sections: [
Section {

View File

@@ -449,4 +449,17 @@ expression: obj.symbols
align: None,
virtual_address: None,
},
Symbol {
name: "[.data-0]",
demangled_name: None,
address: 0,
size: 0,
kind: Section,
section: Some(
2,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
]

View File

@@ -10,10 +10,10 @@ expression: output
[(Address(20), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "xglSleep", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
[(Address(24), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 113), Normal, 10), (Eol, Normal, 0)]
[(Address(28), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lw", 26), Normal, 10), (Argument(Opaque("$a1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%gp_rel("), Normal, 0), (Symbol(Symbol { name: "WorkEnd", demangled_name: None, address: 64, size: 4, kind: Object, section: Some(8), flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("$gp")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Address(32), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lui", 20), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%hi("), Normal, 0), (Symbol(Symbol { name: "[.sdata]", demangled_name: None, address: 0, size: 64, kind: Section, section: Some(8), flags: FlagSet(Local), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Address(32), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lui", 20), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%hi("), Normal, 0), (Symbol(Symbol { name: "[.sdata]", demangled_name: None, address: 0, size: 0, kind: Section, section: Some(8), flags: FlagSet(Local), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Address(36), Normal, 5), (Spacing(4), Normal, 0), (Opcode("daddu", 97), Normal, 10), (Argument(Opaque("$a2")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$zero")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$zero")), Normal, 0), (Eol, Normal, 0)]
[(Address(40), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "xglSoundLoadEffect", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
[(Address(44), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addiu", 12), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%lo("), Normal, 0), (Symbol(Symbol { name: "[.sdata]", demangled_name: None, address: 0, size: 64, kind: Section, section: Some(8), flags: FlagSet(Local), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Address(44), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addiu", 12), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%lo("), Normal, 0), (Symbol(Symbol { name: "[.sdata]", demangled_name: None, address: 0, size: 0, kind: Section, section: Some(8), flags: FlagSet(Local), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Address(48), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lui", 20), Normal, 10), (Argument(Opaque("$a0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%hi("), Normal, 0), (Symbol(Symbol { name: "PacketBottomNewVu1DropMicroCode", demangled_name: None, address: 0, size: 12, kind: Object, section: Some(7), flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Address(52), Normal, 5), (Spacing(4), Normal, 0), (Opcode("lw", 26), Normal, 10), (Argument(Opaque("$a1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("%gp_rel("), Normal, 0), (Symbol(Symbol { name: "WorkEnd", demangled_name: None, address: 64, size: 4, kind: Object, section: Some(8), flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Basic(")"), Normal, 0), (Basic("("), Normal, 0), (Argument(Opaque("$gp")), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Address(56), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jal", 2), Normal, 10), (Symbol(Symbol { name: "xglSoundLoadSwd", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]

View File

@@ -1,5 +1,6 @@
---
source: objdiff-core/tests/arch_mips.rs
assertion_line: 10
expression: obj
---
Object {
@@ -51,6 +52,7 @@ Object {
{},
],
ignored_symbols: {},
diff_side: Base,
},
endianness: Little,
symbols: [
@@ -110,7 +112,7 @@ Object {
name: "[.sdata]",
demangled_name: None,
address: 0,
size: 64,
size: 0,
kind: Section,
section: Some(
8,
@@ -671,6 +673,45 @@ Object {
align: None,
virtual_address: None,
},
Symbol {
name: "[.data-0]",
demangled_name: None,
address: 0,
size: 0,
kind: Section,
section: Some(
2,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.rodata-0]",
demangled_name: None,
address: 0,
size: 12,
kind: Section,
section: Some(
7,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.sdata-0]",
demangled_name: None,
address: 0,
size: 76,
kind: Section,
section: Some(
8,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
],
sections: [
Section {

View File

@@ -2645,6 +2645,8 @@ expression: "(target_symbol_diff, base_symbol_diff)"
arg_diff: [],
},
],
data_diff: [],
data_reloc_diff: [],
},
SymbolDiff {
target_symbol: Some(
@@ -5288,5 +5290,7 @@ expression: "(target_symbol_diff, base_symbol_diff)"
arg_diff: [],
},
],
data_diff: [],
data_reloc_diff: [],
},
)

View File

@@ -1,6 +1,5 @@
---
source: objdiff-core/tests/arch_ppc.rs
assertion_line: 70
expression: sections_display
---
[
@@ -37,7 +36,7 @@ expression: sections_display
),
symbols: [
SectionDisplaySymbol {
symbol: 2,
symbol: 16,
is_mapping_symbol: false,
},
],

View File

@@ -308,6 +308,32 @@ Object {
align: None,
virtual_address: None,
},
Symbol {
name: "[extab-0]",
demangled_name: None,
address: 0,
size: 40,
kind: Section,
section: Some(
1,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[extabindex-0]",
demangled_name: None,
address: 0,
size: 36,
kind: Section,
section: Some(
2,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
],
sections: [
Section {

View File

@@ -43,7 +43,7 @@ Object {
name: "[.ctors]",
demangled_name: None,
address: 0,
size: 4,
size: 0,
kind: Section,
section: Some(
1,
@@ -157,6 +157,19 @@ Object {
0,
),
},
Symbol {
name: "[.ctors-0]",
demangled_name: None,
address: 0,
size: 4,
kind: Section,
section: Some(
1,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
],
sections: [
Section {

View File

@@ -1101,6 +1101,45 @@ Object {
align: None,
virtual_address: None,
},
Symbol {
name: "[.XBLD$W-0]",
demangled_name: None,
address: 0,
size: 16,
kind: Section,
section: Some(
2,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.rdata-0]",
demangled_name: None,
address: 0,
size: 416,
kind: Section,
section: Some(
4,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.pdata-0]",
demangled_name: None,
address: 0,
size: 40,
kind: Section,
section: Some(
6,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
],
sections: [
Section {

View File

@@ -124,6 +124,19 @@ Object {
align: None,
virtual_address: None,
},
Symbol {
name: "[.data-0]",
demangled_name: None,
address: 0,
size: 10,
kind: Section,
section: Some(
1,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
],
sections: [
Section {

View File

@@ -854,6 +854,201 @@ Object {
align: None,
virtual_address: None,
},
Symbol {
name: "[.xdata-0]",
demangled_name: None,
address: 0,
size: 8,
kind: Section,
section: Some(
7,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.pdata-0]",
demangled_name: None,
address: 0,
size: 12,
kind: Section,
section: Some(
8,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.xdata-1]",
demangled_name: None,
address: 0,
size: 8,
kind: Section,
section: Some(
9,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.pdata-1]",
demangled_name: None,
address: 0,
size: 12,
kind: Section,
section: Some(
10,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.xdata-2]",
demangled_name: None,
address: 0,
size: 8,
kind: Section,
section: Some(
11,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.pdata-2]",
demangled_name: None,
address: 0,
size: 12,
kind: Section,
section: Some(
12,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.xdata-3]",
demangled_name: None,
address: 0,
size: 8,
kind: Section,
section: Some(
13,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.pdata-3]",
demangled_name: None,
address: 0,
size: 12,
kind: Section,
section: Some(
14,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.rdata-0]",
demangled_name: None,
address: 0,
size: 256,
kind: Section,
section: Some(
15,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.xdata-4]",
demangled_name: None,
address: 0,
size: 20,
kind: Section,
section: Some(
16,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.pdata-4]",
demangled_name: None,
address: 0,
size: 12,
kind: Section,
section: Some(
17,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.rtc$IMZ-0]",
demangled_name: None,
address: 0,
size: 8,
kind: Section,
section: Some(
19,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.rtc$TMZ-0]",
demangled_name: None,
address: 0,
size: 8,
kind: Section,
section: Some(
20,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.rdata-1]",
demangled_name: None,
address: 0,
size: 4,
kind: Section,
section: Some(
21,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
Symbol {
name: "[.rdata-2]",
demangled_name: None,
address: 0,
size: 4,
kind: Section,
section: Some(
22,
),
flags: FlagSet(Local),
align: None,
virtual_address: None,
},
],
sections: [
Section {

View File

@@ -115,7 +115,8 @@ fn get_hover_item_color_for_diff_kind(diff_kind: DataDiffKind) -> HoverItemColor
pub(crate) fn data_row_ui(
ui: &mut egui::Ui,
obj: Option<&Object>,
address: usize,
base_address: usize,
row_address: usize,
diffs: &[(DataDiff, Vec<DataRelocationDiff>)],
appearance: &Appearance,
column: usize,
@@ -127,7 +128,7 @@ pub(crate) fn data_row_ui(
}
let mut job = LayoutJob::default();
write_text(
format!("{address:08x}: ").as_str(),
format!("{row_address:08x}: ").as_str(),
appearance.text_color,
&mut job,
appearance.code_font.clone(),
@@ -135,7 +136,7 @@ pub(crate) fn data_row_ui(
// The offset shown on the side of the GUI, shifted by insertions/deletions.
let mut cur_addr = 0usize;
// The offset into the actual bytes of the section on this side, ignoring differences.
let mut cur_addr_actual = address;
let mut cur_addr_actual = base_address + row_address;
for (diff, reloc_diffs) in diffs {
let base_color = get_color_for_diff_kind(diff.kind, appearance);
if diff.data.is_empty() {
@@ -211,13 +212,14 @@ pub(crate) fn data_row_ui(
pub(crate) fn split_diffs(
diffs: &[DataDiff],
reloc_diffs: &[DataRelocationDiff],
symbol_offset_in_section: usize,
) -> Vec<Vec<(DataDiff, Vec<DataRelocationDiff>)>> {
let mut split_diffs = Vec::<Vec<(DataDiff, Vec<DataRelocationDiff>)>>::new();
let mut row_diffs = Vec::<(DataDiff, Vec<DataRelocationDiff>)>::new();
// The offset shown on the side of the GUI, shifted by insertions/deletions.
let mut cur_addr = 0usize;
// The offset into the actual bytes of the section on this side, ignoring differences.
let mut cur_addr_actual = 0usize;
let mut cur_addr_actual = symbol_offset_in_section;
for diff in diffs {
let mut cur_len = 0usize;
while cur_len < diff.len {

View File

@@ -2,10 +2,10 @@ use egui::{Id, Layout, RichText, ScrollArea, TextEdit, Ui, Widget, text::LayoutJ
use objdiff_core::{
build::BuildStatus,
diff::{
DiffObjConfig, ObjectDiff, SectionDiff, SymbolDiff,
DiffObjConfig, ObjectDiff, SymbolDiff,
display::{ContextItem, HoverItem, HoverItemColor, SymbolFilter, SymbolNavigationKind},
},
obj::{Object, Section, Symbol},
obj::{Object, Symbol},
};
use time::format_description;
@@ -25,17 +25,10 @@ use crate::{
},
};
#[derive(Clone, Copy)]
enum SelectedSymbol {
Symbol(usize),
Section(usize),
}
#[derive(Clone, Copy)]
struct DiffColumnContext<'a> {
status: &'a BuildStatus,
obj: Option<&'a (Object, ObjectDiff)>,
section: Option<(&'a Section, &'a SectionDiff, usize)>,
symbol: Option<(&'a Symbol, &'a SymbolDiff, usize)>,
}
@@ -46,49 +39,28 @@ impl<'a> DiffColumnContext<'a> {
obj: Option<&'a (Object, ObjectDiff)>,
selected_symbol: Option<&SymbolRefByName>,
) -> Self {
let selected_symbol = match view {
let selected_symbol_idx = match view {
View::SymbolDiff => None,
View::FunctionDiff | View::ExtabDiff => match (obj, selected_symbol) {
(Some(obj), Some(s)) => {
obj.0.symbol_by_name(&s.symbol_name).map(SelectedSymbol::Symbol)
}
_ => None,
},
View::DataDiff => match (obj, selected_symbol) {
(Some(obj), Some(SymbolRefByName { section_name: Some(section_name), .. })) => {
find_section(&obj.0, section_name).map(SelectedSymbol::Section)
}
View::FunctionDiff | View::DataDiff | View::ExtabDiff => match (obj, selected_symbol) {
(Some(obj), Some(s)) => obj.0.symbol_by_name(&s.symbol_name),
_ => None,
},
};
let (section, symbol) = match (obj, selected_symbol) {
(Some((obj, obj_diff)), Some(SelectedSymbol::Symbol(symbol_ref))) => {
let symbol = match (obj, selected_symbol_idx) {
(Some((obj, obj_diff)), Some(symbol_ref)) => {
let symbol = &obj.symbols[symbol_ref];
(
symbol.section.map(|section_idx| {
(&obj.sections[section_idx], &obj_diff.sections[section_idx], section_idx)
}),
Some((symbol, &obj_diff.symbols[symbol_ref], symbol_ref)),
)
Some((symbol, &obj_diff.symbols[symbol_ref], symbol_ref))
}
(Some((obj, obj_diff)), Some(SelectedSymbol::Section(section_idx))) => (
Some((&obj.sections[section_idx], &obj_diff.sections[section_idx], section_idx)),
None,
),
_ => (None, None),
_ => None,
};
Self { status, obj, section, symbol }
Self { status, obj, symbol }
}
#[inline]
pub fn has_symbol(&self) -> bool { self.section.is_some() || self.symbol.is_some() }
pub fn has_symbol(&self) -> bool { self.symbol.is_some() }
#[inline]
pub fn id(&self) -> Option<&str> {
self.symbol
.map(|(symbol, _, _)| symbol.name.as_str())
.or_else(|| self.section.map(|(section, _, _)| section.name.as_str()))
}
pub fn id(&self) -> Option<&str> { self.symbol.map(|(symbol, _, _)| symbol.name.as_str()) }
}
#[must_use]
@@ -133,10 +105,7 @@ pub fn diff_view_ui(
{
navigation.right_symbol = Some(target_symbol_ref);
}
} else if navigation.left_symbol.is_some()
&& left_ctx.obj.is_some()
&& left_ctx.section.is_none()
{
} else if navigation.left_symbol.is_some() && left_ctx.obj.is_some() {
// Clear selection if symbol goes missing
navigation.left_symbol = None;
}
@@ -147,10 +116,7 @@ pub fn diff_view_ui(
{
navigation.left_symbol = Some(target_symbol_ref);
}
} else if navigation.right_symbol.is_some()
&& right_ctx.obj.is_some()
&& right_ctx.section.is_none()
{
} else if navigation.right_symbol.is_some() && right_ctx.obj.is_some() {
// Clear selection if symbol goes missing
navigation.right_symbol = None;
}
@@ -225,12 +191,6 @@ pub fn diff_view_ui(
{
ret = Some(action);
}
} else if let Some((section, _, _)) = left_ctx.section {
ui.label(
RichText::new(section.name.clone())
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else if right_ctx.has_symbol() {
ui.label(
RichText::new("Choose target symbol")
@@ -363,12 +323,6 @@ pub fn diff_view_ui(
{
ret = Some(action);
}
} else if let Some((section, _, _)) = right_ctx.section {
ui.label(
RichText::new(section.name.clone())
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else if left_ctx.has_symbol() {
ui.label(
RichText::new("Choose base symbol")
@@ -509,17 +463,17 @@ pub fn diff_view_ui(
View::DataDiff,
Some((left_obj, _left_diff)),
Some((right_obj, _right_diff)),
Some((_left_section, left_section_diff, _left_symbol_idx)),
Some((_right_section, right_section_diff, _right_symbol_idx)),
Some((left_symbol, left_symbol_diff, _left_symbol_idx)),
Some((right_symbol, right_symbol_diff, _right_symbol_idx)),
) =
(state.current_view, left_ctx.obj, right_ctx.obj, left_ctx.section, right_ctx.section)
(state.current_view, left_ctx.obj, right_ctx.obj, left_ctx.symbol, right_ctx.symbol)
{
// Joint diff view
hotkeys::check_scroll_hotkeys(ui, true);
let left_total_bytes =
left_section_diff.data_diff.iter().fold(0usize, |accum, item| accum + item.len);
left_symbol_diff.data_diff.iter().fold(0usize, |accum, item| accum + item.len);
let right_total_bytes =
right_section_diff.data_diff.iter().fold(0usize, |accum, item| accum + item.len);
right_symbol_diff.data_diff.iter().fold(0usize, |accum, item| accum + item.len);
if left_total_bytes != right_total_bytes {
ui.label("Data size mismatch");
return;
@@ -528,10 +482,16 @@ pub fn diff_view_ui(
return;
}
let total_rows = (left_total_bytes - 1) / BYTES_PER_ROW + 1;
let left_diffs =
split_diffs(&left_section_diff.data_diff, &left_section_diff.reloc_diff);
let right_diffs =
split_diffs(&right_section_diff.data_diff, &right_section_diff.reloc_diff);
let left_diffs = split_diffs(
&left_symbol_diff.data_diff,
&left_symbol_diff.data_reloc_diff,
left_symbol.address as usize,
);
let right_diffs = split_diffs(
&right_symbol_diff.data_diff,
&right_symbol_diff.data_reloc_diff,
right_symbol.address as usize,
);
render_table(
ui,
available_width,
@@ -540,13 +500,14 @@ pub fn diff_view_ui(
total_rows,
|row, column| {
let i = row.index();
let address = i * BYTES_PER_ROW;
let row_offset = i * BYTES_PER_ROW;
row.col(|ui| {
if column == 0 {
data_row_ui(
ui,
Some(left_obj),
address,
left_symbol.address as usize,
row_offset,
&left_diffs[i],
appearance,
column,
@@ -555,7 +516,8 @@ pub fn diff_view_ui(
data_row_ui(
ui,
Some(right_obj),
address,
right_symbol.address as usize,
row_offset,
&right_diffs[i],
appearance,
column,
@@ -649,11 +611,46 @@ fn diff_col_ui(
if !ctx.status.success {
build_log_ui(ui, ctx.status, appearance);
} else if let Some((obj, diff)) = ctx.obj {
if let Some((_symbol, symbol_diff, symbol_idx)) = ctx.symbol {
if let Some((symbol, symbol_diff, symbol_idx)) = ctx.symbol {
hotkeys::check_scroll_hotkeys(ui, false);
let ctx = FunctionDiffContext { obj, diff, symbol_ref: Some(symbol_idx) };
if state.current_view == View::ExtabDiff {
extab_ui(ui, ctx, appearance, column);
} else if state.current_view == View::DataDiff {
hotkeys::check_scroll_hotkeys(ui, false);
let total_bytes =
symbol_diff.data_diff.iter().fold(0usize, |accum, item| accum + item.len);
if total_bytes == 0 {
return ret;
}
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
let diffs = split_diffs(
&symbol_diff.data_diff,
&symbol_diff.data_reloc_diff,
symbol.address as usize,
);
render_table(
ui,
available_width / 2.0,
1,
appearance.code_font.size,
total_rows,
|row, _column| {
let i = row.index();
let row_offset = i * BYTES_PER_ROW;
row.col(|ui| {
data_row_ui(
ui,
Some(obj),
symbol.address as usize,
row_offset,
&diffs[i],
appearance,
column,
);
});
},
);
} else {
render_table(
ui,
@@ -678,29 +675,6 @@ fn diff_col_ui(
},
);
}
} else if let Some((_section, section_diff, _section_idx)) = ctx.section {
hotkeys::check_scroll_hotkeys(ui, false);
let total_bytes =
section_diff.data_diff.iter().fold(0usize, |accum, item| accum + item.len);
if total_bytes == 0 {
return ret;
}
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
let diffs = split_diffs(&section_diff.data_diff, &section_diff.reloc_diff);
render_table(
ui,
available_width / 2.0,
1,
appearance.code_font.size,
total_rows,
|row, _column| {
let i = row.index();
let address = i * BYTES_PER_ROW;
row.col(|ui| {
data_row_ui(ui, Some(obj), address, &diffs[i], appearance, column);
});
},
);
} else if let Some((_other_symbol, _other_symbol_diff, other_symbol_idx)) = other_ctx.symbol
{
if let Some(action) = symbol_list_ui(
@@ -796,10 +770,6 @@ fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) {
});
}
fn find_section(obj: &Object, section_name: &str) -> Option<usize> {
obj.sections.iter().position(|section| section.name == section_name)
}
pub fn hover_items_ui(ui: &mut Ui, items: Vec<HoverItem>, appearance: &Appearance) {
for item in items {
match item {

View File

@@ -233,7 +233,6 @@ impl DiffViewState {
let resolved_nav = resolve_navigation(nav.kind, resolved_left, resolved_right);
if (resolved_nav.left_symbol.is_some() && resolved_nav.right_symbol.is_some())
|| (resolved_nav.left_symbol.is_none() && resolved_nav.right_symbol.is_none())
|| resolved_nav.view != View::FunctionDiff
{
// Regular navigation
if state.is_selecting_symbol() {
@@ -416,14 +415,8 @@ fn resolve_navigation(
},
(SectionKind::Data, SectionKind::Data) => ResolvedNavigation {
view: View::DataDiff,
left_symbol: Some(SymbolRefByName {
symbol_name: "".to_string(),
section_name: Some(left.section.name.clone()),
}),
right_symbol: Some(SymbolRefByName {
symbol_name: "".to_string(),
section_name: Some(right.section.name.clone()),
}),
left_symbol: Some(left.symbol_ref),
right_symbol: Some(right.symbol_ref),
},
_ => ResolvedNavigation::default(),
},
@@ -438,14 +431,8 @@ fn resolve_navigation(
},
SectionKind::Data => ResolvedNavigation {
view: View::DataDiff,
left_symbol: Some(SymbolRefByName {
symbol_name: "".to_string(),
section_name: Some(left.section.name.clone()),
}),
right_symbol: Some(SymbolRefByName {
symbol_name: "".to_string(),
section_name: Some(left.section.name.clone()),
}),
left_symbol: Some(left.symbol_ref),
right_symbol: None,
},
_ => ResolvedNavigation::default(),
},
@@ -460,14 +447,8 @@ fn resolve_navigation(
},
SectionKind::Data => ResolvedNavigation {
view: View::DataDiff,
left_symbol: Some(SymbolRefByName {
symbol_name: "".to_string(),
section_name: Some(right.section.name.clone()),
}),
right_symbol: Some(SymbolRefByName {
symbol_name: "".to_string(),
section_name: Some(right.section.name.clone()),
}),
left_symbol: None,
right_symbol: Some(right.symbol_ref),
},
_ => ResolvedNavigation::default(),
},

View File

@@ -0,0 +1,6 @@
[build]
target = "wasm32-wasip2"
[unstable]
build-std = ["panic_abort", "core", "alloc"]
build-std-features = ["compiler-builtins-mem"]

View File

@@ -17,12 +17,13 @@ build = "build.rs"
crate-type = ["cdylib"]
[features]
default = ["std"]
default = []
std = ["objdiff-core/std"]
[dependencies]
log = { version = "0.4", default-features = false }
regex = { version = "1.11", default-features = false, features = ["unicode-case"] }
wit-bindgen = { version = "0.44", default-features = false, features = ["macros"] }
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
[dependencies.objdiff-core]
@@ -33,8 +34,5 @@ features = ["arm", "arm64", "mips", "ppc", "superh", "x86", "dwarf"]
[target.'cfg(target_family = "wasm")'.dependencies]
talc = { version = "4.4", default-features = false, features = ["lock_api"] }
[target.'cfg(target_os = "wasi")'.dependencies]
wit-bindgen = { version = "0.43", default-features = false, features = ["macros"] }
[build-dependencies]
wit-deps = "0.5"

View File

@@ -1,12 +1,12 @@
{
"name": "objdiff-wasm",
"version": "3.0.0",
"version": "3.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "objdiff-wasm",
"version": "3.0.0",
"version": "3.1.0",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@biomejs/biome": "^1.9.3",

View File

@@ -1,6 +1,6 @@
{
"name": "objdiff-wasm",
"version": "3.0.0",
"version": "3.1.0",
"description": "A local diffing tool for decompilation projects.",
"author": {
"name": "Luke Street",
@@ -19,8 +19,8 @@
"types": "dist/objdiff.d.ts",
"scripts": {
"build": "npm run build:wasm && npm run build:transpile && npm run build:lib",
"build:wasm": "cargo +nightly -Zbuild-std=panic_abort,core,alloc -Zbuild-std-features=compiler-builtins-mem build --target wasm32-wasip2 --release --no-default-features",
"build:transpile": "jco transpile ../target/wasm32-wasip2/release/objdiff_wasm.wasm --no-nodejs-compat --no-wasi-shim --no-namespaced-exports --map wasi:logging/logging=./wasi-logging.js --optimize -o pkg --name objdiff",
"build:wasm": "cargo build --profile release-min --no-default-features",
"build:transpile": "jco transpile ../target/wasm32-wasip2/release-min/objdiff_wasm.wasm --no-nodejs-compat --no-wasi-shim --no-namespaced-exports --map wasi:logging/logging=./wasi-logging.js --optimize -o pkg --name objdiff",
"build:lib": "rslib build"
},
"devDependencies": {

View File

@@ -0,0 +1,4 @@
[toolchain]
channel = "nightly"
components = ["rust-src"]
targets = ["wasm32-wasip2"]

View File

@@ -1,3 +1,4 @@
#![allow(clippy::derivable_impls)]
use alloc::{
format,
rc::{Rc, Weak},
@@ -23,7 +24,7 @@ wit_bindgen::generate!({
use exports::objdiff::core::{
diff::{
DiffConfigBorrow, DiffResult, Guest as GuestDiff, GuestDiffConfig, GuestObject,
DiffConfigBorrow, DiffResult, DiffSide, Guest as GuestDiff, GuestDiffConfig, GuestObject,
GuestObjectDiff, MappingConfig, Object, ObjectBorrow, ObjectDiff, ObjectDiffBorrow,
SymbolFlags, SymbolInfo, SymbolKind, SymbolRef,
},
@@ -223,7 +224,7 @@ impl GuestDisplay for Component {
let symbol_display = from_symbol_ref(symbol_ref);
diff::display::symbol_context(obj, symbol_display.symbol as usize)
.into_iter()
.map(|item| ContextItem::from(item))
.map(ContextItem::from)
.collect()
}
@@ -235,7 +236,7 @@ impl GuestDisplay for Component {
let symbol_display = from_symbol_ref(symbol_ref);
diff::display::symbol_hover(obj, symbol_display.symbol as usize, addend, override_color)
.into_iter()
.map(|item| HoverItem::from(item))
.map(HoverItem::from)
.collect()
}
@@ -282,7 +283,7 @@ impl GuestDisplay for Component {
};
diff::display::instruction_context(obj, resolved, &ins)
.into_iter()
.map(|item| ContextItem::from(item))
.map(ContextItem::from)
.collect()
}
@@ -331,7 +332,7 @@ impl GuestDisplay for Component {
};
diff::display::instruction_hover(obj, resolved, &ins)
.into_iter()
.map(|item| HoverItem::from(item))
.map(HoverItem::from)
.collect()
}
}
@@ -469,8 +470,21 @@ unsafe impl Sync for ObjectCache {}
static OBJECT_CACHE: ObjectCache = ObjectCache::new();
impl From<DiffSide> for objdiff_core::diff::DiffSide {
fn from(value: DiffSide) -> Self {
match value {
DiffSide::Target => Self::Target,
DiffSide::Base => Self::Base,
}
}
}
impl GuestObject for ResourceObject {
fn parse(data: Vec<u8>, diff_config: DiffConfigBorrow) -> Result<Object, String> {
fn parse(
data: Vec<u8>,
diff_config: DiffConfigBorrow,
diff_side: DiffSide,
) -> Result<Object, String> {
let hash = xxh3_64(&data);
let mut cached = None;
OBJECT_CACHE.borrow_mut().retain(|c| {
@@ -486,7 +500,9 @@ impl GuestObject for ResourceObject {
return Ok(Object::new(ResourceObject(obj, hash)));
}
let diff_config = diff_config.get::<ResourceDiffConfig>().0.borrow();
let obj = Rc::new(obj::read::parse(&data, &diff_config).map_err(|e| e.to_string())?);
let obj = Rc::new(
obj::read::parse(&data, &diff_config, diff_side.into()).map_err(|e| e.to_string())?,
);
OBJECT_CACHE.borrow_mut().push(CachedObject(Rc::downgrade(&obj), hash));
Ok(Object::new(ResourceObject(obj, hash)))
}
@@ -527,9 +543,7 @@ impl GuestObjectDiff for ResourceObjectDiff {
fn get_symbol(&self, symbol_ref: SymbolRef) -> Option<SymbolInfo> {
let obj = self.0.as_ref();
let symbol_display = from_symbol_ref(symbol_ref);
let Some(symbol) = obj.symbols.get(symbol_display.symbol) else {
return None;
};
let symbol = obj.symbols.get(symbol_display.symbol)?;
Some(SymbolInfo {
id: to_symbol_ref(symbol_display),
name: symbol.name.clone(),

View File

@@ -1,9 +1,7 @@
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
#[cfg(target_os = "wasi")]
mod api;
#[cfg(target_os = "wasi")]
mod logging;
#[cfg(all(target_os = "wasi", not(feature = "std")))]

View File

@@ -19,6 +19,7 @@ interface diff {
parse: static func(
data: list<u8>,
config: borrow<diff-config>,
side: diff-side,
) -> result<object, string>;
hash: func() -> u64;
@@ -80,6 +81,11 @@ interface diff {
config: borrow<diff-config>,
mapping: mapping-config,
) -> result<diff-result, string>;
enum diff-side {
target,
base,
}
}
interface display {