mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-15 08:06:25 +00:00
Compare commits
270 Commits
v2.0.0-bet
...
v3.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 03a578c1bb | |||
| d09ef8e207 | |||
| 63552a58ae | |||
| 827f4a42bd | |||
|
|
b2dcecc5d8 | ||
|
|
67b237eab6 | ||
|
|
66da80ff69 | ||
| 0a85f540f2 | |||
|
|
d79b5b1233 | ||
|
|
8f5519cb6a | ||
|
|
19ec08be9a | ||
| 2ad0898efa | |||
|
|
03f2bcb8b1 | ||
|
|
781071761a | ||
| 572afa8551 | |||
| 90e81fad7e | |||
| 7a8efb4c88 | |||
| 56dac46280 | |||
| 1866158092 | |||
|
|
fe8e7029f6 | ||
|
|
e2c70342c9 | ||
|
|
e6035b00df | ||
| 97bcfe23d4 | |||
| d684b622b5 | |||
| c698750068 | |||
|
|
a06d3455ae | ||
| fbdaa89cc0 | |||
| f7cb494a62 | |||
|
|
7cc6ed2b43 | ||
|
|
532b684682 | ||
|
|
fb1d434bbc | ||
|
|
23009bf9a3 | ||
|
|
6fb4bb8855 | ||
| a138dfa907 | |||
|
|
0b95613768 | ||
| 5d4b33a500 | |||
|
|
f2a591356e | ||
| 8d24ec6373 | |||
| 58430d947b | |||
| 1533125f9d | |||
| 5654060dc8 | |||
| 84079c3934 | |||
|
|
48804dc2e3 | ||
|
|
8cfa8b7dab | ||
|
|
93a4d7e55d | ||
|
|
7c424a7966 | ||
| 678210d58a | |||
| 4302821615 | |||
| c4b4244b59 | |||
| 52c138bf06 | |||
| 813c8aa539 | |||
| 0f0aaab795 | |||
| b21892be31 | |||
| 247d6da94b | |||
| bd95faa9c3 | |||
| 2c57e4960f | |||
| cff4be2979 | |||
|
|
e1da90943c | ||
|
|
b5713db333 | ||
|
|
4c3f5e9836 | ||
|
|
e9ce02feb0 | ||
|
|
9a378d85ed | ||
|
|
c8ff45f2c8 | ||
|
|
34e4513c69 | ||
| a015971c20 | |||
| e67d5998b3 | |||
| 91bc23edfc | |||
| c9c3b32376 | |||
| 0dc123b064 | |||
| 1e62d4664c | |||
| 1205e8ceb4 | |||
| c917cad5f0 | |||
| dd653329f5 | |||
| f5d3d5f10a | |||
| c327ed3ea8 | |||
|
|
85fb18a21a | ||
| 00ad0d8094 | |||
| 8fac63c42c | |||
| 0fb7f3901c | |||
|
|
3385f58341 | ||
| 60b227f45e | |||
|
|
127ae5ae44 | ||
| 5f48e69775 | |||
| 8756eee07b | |||
| bd3ed0d5ad | |||
|
|
e638d0b17a | ||
|
|
f58616b6dd | ||
|
|
e9762e24c2 | ||
| dab79d96a1 | |||
| a57e5db983 | |||
|
|
d0afd3b83e | ||
|
|
a367af612b | ||
|
|
22052ea10b | ||
| f7c3501eae | |||
| 07ef93f16a | |||
| 8e8ab6bef8 | |||
| e865f3d598 | |||
| 2b13e9886a | |||
| 1750af736a | |||
|
|
731b604c24 | ||
| 2d643eb071 | |||
| 0c48d711c7 | |||
| 34220a8e26 | |||
| 0c9e5526d4 | |||
| 3db0727469 | |||
| 8b5bf21f38 | |||
| b77df77000 | |||
| 7e08f9715b | |||
| 3c05852d00 | |||
| a51ff44be1 | |||
| d225cac205 | |||
| 737b3782db | |||
|
|
1d782243e0 | ||
| a1499f475d | |||
|
|
f263e490e3 | ||
|
|
d0e6c5c057 | ||
|
|
e1c51ac297 | ||
| 39b1b49985 | |||
| 6c7160ab7e | |||
|
|
644d4762f0 | ||
|
|
b40fae5140 | ||
|
|
fbf85632ab | ||
| 73a89d2768 | |||
| a162c2f840 | |||
| a474b27d55 | |||
| 3d7f2b70dc | |||
| fe886f862d | |||
| 2bcbc34850 | |||
|
|
9b557e4c8e | ||
|
|
b9ba5796ed | ||
|
|
e101610416 | ||
|
|
196c003a92 | ||
| 7b00a9e9f2 | |||
| 311de887ec | |||
| 485b259c32 | |||
| d8fdfaa2c0 | |||
| 2612cda1fb | |||
| bc46e17824 | |||
| e735adbd3d | |||
| 6768df9d80 | |||
| eaba2391e0 | |||
| bd8f5ede23 | |||
| acf46c6b54 | |||
| 9358d8ec60 | |||
| 809e2ffddc | |||
| 87fa29e8b0 | |||
| 42d4c38079 | |||
| fa26200ed7 | |||
| a0e7f9bc37 | |||
| 5898d7aebf | |||
| ffb38d1bb0 | |||
| d56dda72f0 | |||
| c6971f3f2d | |||
| 3965a035fa | |||
| f1fc29f77e | |||
| 7c4f1c5d13 | |||
| fa4a6cadbb | |||
| 799971d54e | |||
| 8eef37e8df | |||
| 5f36916087 | |||
| ee667a2dde | |||
|
|
cf5fc54cfa | ||
| 1cdfa1e857 | |||
| 3f157f33a5 | |||
|
|
a1ea2919f8 | ||
| 9c31c82a37 | |||
| 4f34dfa194 | |||
| ae6d37a10b | |||
| 06e54f3456 | |||
| 6ed07bfaf1 | |||
| 80e939653a | |||
| 48305e0380 | |||
| 2eafbb218b | |||
| a1f2a535e5 | |||
| 8461b35cd7 | |||
| 95868f1d19 | |||
| 506c251d68 | |||
| f3c157ff06 | |||
|
|
6d3c63ccd8 | ||
| bbd8d9714f | |||
| 3c66ac3d54 | |||
| 561a9107e2 | |||
| e8de35b78e | |||
| d938988d43 | |||
|
|
3e6efb7736 | ||
|
|
674c942d7d | ||
|
|
6b7dcabbed | ||
| e202c3ef95 | |||
|
|
b7730b3d00 | ||
|
|
a4fdb61f04 | ||
|
|
2876be37a3 | ||
| 11171763eb | |||
| 6037a79ba2 | |||
| f7efe5fdff | |||
| 0692deac59 | |||
| c3e3d175c5 | |||
| c45f4bbc99 | |||
| b0c5431ac5 | |||
|
|
9ab246367b | ||
|
|
dcafe51eda | ||
| c65e87c382 | |||
| 1756b9f6c5 | |||
| 303f2938a2 | |||
| 526e031251 | |||
|
|
10b2a9c129 | ||
|
|
abe68ef2f2 | ||
|
|
304df96411 | ||
| 7aa878b48e | |||
| a119d9a6dd | |||
|
|
ebf653816a | ||
| 424434edd6 | |||
| 7f14b684bf | |||
| c5da7f7dd5 | |||
| 2fd655850a | |||
| 79bd7317c1 | |||
| 21f8f2407c | |||
| d2b7a9ef25 | |||
| 2cf9cf24d6 | |||
|
|
5ef3416457 | ||
|
|
6ff8d002f7 | ||
| 9ca157d717 | |||
|
|
67b63311fc | ||
| 72ea1c8911 | |||
| d4a540857d | |||
| 676488433f | |||
| 83de98b5ee | |||
| c1ba4e91d1 | |||
| 575900024d | |||
| cbe299e859 | |||
| 741d93e211 | |||
| 603dbd6882 | |||
| 6fb0a63de2 | |||
| ab2e84a2c6 | |||
| 9596051cb4 | |||
| a5d9d8282e | |||
|
|
3287a0f65c | ||
|
|
fab9c62dfb | ||
| 08cd768260 | |||
| 8acaaf528c | |||
| 6e881a74e1 | |||
| cc1bc44e69 | |||
| c7b85518ab | |||
| bb039a1445 | |||
| 8fc142d316 | |||
| b0123b3f83 | |||
| 2ec17aee9b | |||
| ec9731e1e5 | |||
|
|
a06382c27e | ||
| e013638c5a | |||
| 70ab82f1f7 | |||
| c5896689cf | |||
| 67719dd93e | |||
| 258e141017 | |||
| dbdda55065 | |||
|
|
a43320af1f | ||
|
|
35bbd40f5d | ||
|
|
c1cb4b0b19 | ||
| 2379853faa | |||
| 5e1aff180f | |||
| 3846a7d315 | |||
| dcf209aac5 | |||
| c7e6394628 | |||
| 235dc7f517 | |||
|
|
199c07e975 | ||
| 56a5a61825 | |||
| 3d2236de82 | |||
| bcc5871cd8 | |||
|
|
7d0d7df54c | ||
| 0221a2d54d | |||
|
|
bc687173c0 |
4
.cargo/config.toml
Normal file
4
.cargo/config.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
# statically link the C runtime so the executable does not depend on
|
||||
# that shared/dynamic library.
|
||||
[target.'cfg(all(target_env = "msvc", target_os = "windows"))']
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
2
.editorconfig
Normal file
2
.editorconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
222
.github/workflows/build.yaml
vendored
222
.github/workflows/build.yaml
vendored
@@ -4,13 +4,17 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- 'LICENSE*'
|
||||
- "*.md"
|
||||
- "LICENSE*"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
# For npm publish provenance
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
BUILD_PROFILE: release-lto
|
||||
CARGO_TARGET_DIR: target
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
jobs:
|
||||
check:
|
||||
@@ -25,37 +29,16 @@ jobs:
|
||||
sudo apt-get -y install libgtk-3-dev
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Check git tag against Cargo version
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
shell: bash
|
||||
run: |
|
||||
set -eou pipefail
|
||||
tag='${{github.ref}}'
|
||||
tag="${tag#refs/tags/}"
|
||||
for file in */Cargo.toml; do
|
||||
version=$(grep '^version' $file | head -1 | awk -F' = ' '{print $2}' | tr -d '"')
|
||||
version="v$version"
|
||||
if [ "$tag" != "$version" ]; then
|
||||
echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.4
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Cargo check
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: cargo check
|
||||
run: cargo check --all-targets --all-features --workspace
|
||||
- name: Cargo clippy
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: cargo clippy
|
||||
run: cargo clippy --all-targets --all-features --workspace
|
||||
|
||||
fmt:
|
||||
name: Format
|
||||
@@ -85,13 +68,12 @@ jobs:
|
||||
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check ${{ matrix.checks }}
|
||||
|
||||
test:
|
||||
name: Test
|
||||
if: 'false' # No tests yet
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
@@ -107,13 +89,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.4
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Cargo test
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: cargo test --release
|
||||
run: cargo test --release --all-features --workspace
|
||||
|
||||
build-cli:
|
||||
name: Build objdiff-cli
|
||||
@@ -137,10 +116,10 @@ jobs:
|
||||
name: linux-aarch64
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: ubuntu-latest
|
||||
target: armv7-unknown-linux-musleabi
|
||||
name: linux-armv7l
|
||||
build: zigbuild
|
||||
- platform: windows-latest
|
||||
target: i686-pc-windows-msvc
|
||||
name: windows-x86
|
||||
build: build
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
@@ -167,24 +146,33 @@ 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: pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
|
||||
run: |
|
||||
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:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.target }}
|
||||
- name: Cargo build
|
||||
run: >
|
||||
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
|
||||
path: |
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
build-gui:
|
||||
@@ -195,21 +183,26 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- platform: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
target: x86_64-unknown-linux-gnu.2.31
|
||||
target_base: x86_64-unknown-linux-gnu
|
||||
name: linux-x86_64
|
||||
packages: libgtk-3-dev
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
name: windows-x86_64
|
||||
build: build
|
||||
features: default
|
||||
- platform: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
name: macos-x86_64
|
||||
build: build
|
||||
features: default
|
||||
- platform: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
name: macos-arm64
|
||||
build: build
|
||||
features: default
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.platform }}
|
||||
@@ -221,39 +214,100 @@ 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: |
|
||||
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:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.4
|
||||
targets: ${{ matrix.target_base || matrix.target }}
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.target }}
|
||||
- name: Cargo build
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: >
|
||||
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
|
||||
path: |
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||
target/${{ matrix.target_base || matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
target/${{ matrix.target_base || matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
name: Release
|
||||
build-wasm:
|
||||
name: Build objdiff-wasm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rust-src
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
- name: Install dependencies
|
||||
run: npm -C objdiff-wasm ci
|
||||
- name: Build
|
||||
run: npm -C objdiff-wasm run build
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wasm
|
||||
path: objdiff-wasm/dist/
|
||||
if-no-files-found: error
|
||||
|
||||
check-version:
|
||||
name: Check package versions
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ check, build-cli, build-gui ]
|
||||
needs: [ build-cli, build-gui, build-wasm ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Check git tag against package versions
|
||||
shell: bash
|
||||
run: |
|
||||
set -eou pipefail
|
||||
tag='${{github.ref}}'
|
||||
tag="${tag#refs/tags/}"
|
||||
version=$(grep '^version' Cargo.toml | head -1 | awk -F' = ' '{print $2}' | tr -d '"')
|
||||
version="v$version"
|
||||
if [ "$tag" != "$version" ]; then
|
||||
echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)"
|
||||
exit 1
|
||||
fi
|
||||
version="v$(jq -r .version objdiff-wasm/package.json)"
|
||||
if [ "$tag" != "$version" ]; then
|
||||
echo "::error::Git tag doesn't match the npm version! ($tag != $version)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release-github:
|
||||
name: Release (GitHub)
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ check-version ]
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: objdiff-*
|
||||
path: artifacts
|
||||
- name: Rename artifacts
|
||||
working-directory: artifacts
|
||||
@@ -278,6 +332,58 @@ jobs:
|
||||
done
|
||||
ls -R ../out
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: out/*
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
|
||||
release-cargo:
|
||||
name: Release (Cargo)
|
||||
if: 'false' # TODO re-enable when all dependencies are published
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ check-version ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: cargo publish -p objdiff-core
|
||||
|
||||
release-npm:
|
||||
name: Release (npm)
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ check-version ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: wasm
|
||||
path: objdiff-wasm/dist
|
||||
- name: Publish
|
||||
working-directory: objdiff-wasm
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version=$(jq -r '.version' package.json)
|
||||
tag="latest"
|
||||
# Check for prerelease by looking for a dash
|
||||
case "$version" in
|
||||
*-*)
|
||||
tag=$(echo "$version" | sed -e 's/^[^-]*-//' -e 's/\..*$//')
|
||||
;;
|
||||
esac
|
||||
echo "Publishing version $version with tag '$tag'..."
|
||||
npm publish --provenance --access public --tag "$tag"
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,10 +3,6 @@ target/
|
||||
**/*.rs.bk
|
||||
generated/
|
||||
|
||||
# cargo-mobile
|
||||
.cargo/
|
||||
/gen
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -22,4 +18,4 @@ android.keystore
|
||||
*.frag
|
||||
*.vert
|
||||
*.metal
|
||||
.vscode/launch.json
|
||||
.vscode/
|
||||
|
||||
36
.pre-commit-config.yaml
Normal file
36
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
args: [--markdown-linebreak-ext=md]
|
||||
- id: end-of-file-fixer
|
||||
- id: fix-byte-order-marker
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: cargo-fmt
|
||||
name: cargo fmt
|
||||
description: Run cargo fmt on all project files.
|
||||
language: system
|
||||
entry: cargo
|
||||
args: ["+nightly", "fmt", "--all"]
|
||||
pass_filenames: false
|
||||
- id: cargo clippy
|
||||
name: cargo clippy
|
||||
description: Run cargo clippy on all project files.
|
||||
language: system
|
||||
entry: cargo
|
||||
args: ["+nightly", "clippy", "--all-targets", "--all-features", "--workspace"]
|
||||
pass_filenames: false
|
||||
- id: cargo-deny
|
||||
name: cargo deny
|
||||
description: Run cargo deny on all project files.
|
||||
language: system
|
||||
entry: cargo
|
||||
args: ["deny", "check"]
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
44
AGENTS.md
Normal file
44
AGENTS.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
- `objdiff-core`: core library with diffing logic and shared components
|
||||
- `objdiff-cli`: CLI/TUI with `ratatui`
|
||||
- `objdiff-gui`: GUI with `eframe`/`egui`
|
||||
- `objdiff-wasm`: WebAssembly bindings
|
||||
|
||||
objdiff has three main frontends: GUI, CLI/TUI, and [web](https://github.com/encounter/objdiff-web) (utilizing `objdiff-wasm`).
|
||||
|
||||
`objdiff-gui` is the most fully-featured and is the primary target for new features.
|
||||
|
||||
`objdiff-cli` has an interactive TUI `diff` mode (powered by `ratatui`) and a non-interactive `report` mode to generate detailed progress reports (e.g. for <https://decomp.dev>).
|
||||
|
||||
`objdiff-wasm` is compiled into a [WebAssembly Component](https://component-model.bytecodealliance.org/). The [API](objdiff-wasm/wit/objdiff.wit) is defined using [WIT](https://component-model.bytecodealliance.org/design/wit.html). The web interface is separate, but mirrors the GUI in functionality.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
Run `cargo build` for a debug build and `cargo build --release` for an optimized build.
|
||||
The CLI can be exercised with `cargo run --release --bin objdiff-cli -- --help`.
|
||||
|
||||
The WASM build is not included in the workspace by default due to differences in toolchain and target; build it with `npm -C objdiff-wasm run build`. (nightly required)
|
||||
In general, do NOT use `--workspace`, as it will include the WASM crate and likely fail. Formatting and linting commands are exempt.
|
||||
|
||||
Pre-commit tasks:
|
||||
|
||||
- `cargo test` to run the test suite
|
||||
- `cargo +nightly fmt --all` to format code (nightly required)
|
||||
- `cargo +nightly clippy --all-targets --all-features --workspace -- -D warnings` to lint code (nightly required)
|
||||
- `cargo deny check` (`cargo install --locked cargo-deny` if needed) if dependencies change
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Favor focused unit tests adjacent to the code, and integration scenarios in `objdiff-core/tests`.
|
||||
Integration tests utilize snapshots with the `insta` crate; run `curl -LsSf https://insta.rs/install.sh | sh` if needed.
|
||||
To generate updated snapshots, use `cargo insta test`, review diffs manually, then accept changes with `cargo insta accept` (or simply `mv` to accept specific snapshots).
|
||||
|
||||
## Configuration Tips
|
||||
|
||||
- `config.schema.json`: JSON schema for the user-facing project config file (`objdiff.json`)
|
||||
- `objdiff-core/config-schema.json`: Internal options schema to generate `DiffObjConfig` (see `objdiff-core/config_gen.rs`)
|
||||
|
||||
The project configuration (`objdiff.json`) is intended to be generated by the projects' build system. It includes paths to the target (expected) and base (current) object files, along with build commands and file watch patterns. See `README.md` for a summary of key options.
|
||||
5129
Cargo.lock
generated
5129
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
24
Cargo.toml
24
Cargo.toml
@@ -3,10 +3,30 @@ members = [
|
||||
"objdiff-cli",
|
||||
"objdiff-core",
|
||||
"objdiff-gui",
|
||||
"objdiff-wasm",
|
||||
]
|
||||
resolver = "2"
|
||||
default-members = [
|
||||
"objdiff-cli",
|
||||
"objdiff-core",
|
||||
"objdiff-gui",
|
||||
# Exclude objdiff-wasm by default
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.4.1"
|
||||
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 = "thin"
|
||||
lto = "fat"
|
||||
strip = "debuginfo"
|
||||
codegen-units = 1
|
||||
|
||||
[profile.release-min]
|
||||
inherits = "release-lto"
|
||||
opt-level = "z"
|
||||
|
||||
171
README.md
171
README.md
@@ -6,53 +6,75 @@
|
||||
A local diffing tool for decompilation projects. Inspired by [decomp.me](https://decomp.me) and [asm-differ](https://github.com/simonlindholm/asm-differ).
|
||||
|
||||
Features:
|
||||
- Compare entire object files: functions and data.
|
||||
- Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC)
|
||||
- Automatic rebuild on source file changes.
|
||||
- Project integration via [configuration file](#configuration).
|
||||
- Search and filter all of a project's objects and quickly switch.
|
||||
- Click to highlight all instances of values and registers.
|
||||
|
||||
- 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:
|
||||
- PowerPC 750CL (GameCube, Wii)
|
||||
- MIPS (N64, PS1, PS2, PSP)
|
||||
- x86 (COFF only at the moment)
|
||||
|
||||
- ARM (GBA, DS, 3DS)
|
||||
- ARM64 (Switch)
|
||||
- MIPS (N64, PS1, PS2, PSP)
|
||||
- PowerPC (GameCube, Wii, PS3, Xbox 360)
|
||||
- SuperH (Saturn, Dreamcast)
|
||||
- x86, x86_64 (PC)
|
||||
|
||||
See [Usage](#usage) for more information.
|
||||
|
||||
## Downloads
|
||||
|
||||
To build from source, see [Building](#building).
|
||||
|
||||
### GUI
|
||||
|
||||
- [Windows (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-windows-x86_64.exe)
|
||||
- [Linux (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-linux-x86_64)
|
||||
- [macOS (arm64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-macos-arm64)
|
||||
- [macOS (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-macos-x86_64)
|
||||
|
||||
For Linux and macOS, run `chmod +x objdiff-*` to make the binary executable.
|
||||
|
||||
### CLI
|
||||
|
||||
CLI binaries are available on the [releases page](https://github.com/encounter/objdiff/releases).
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||
## 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
|
||||
@@ -67,22 +89,31 @@ 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",
|
||||
"*.json"
|
||||
],
|
||||
"ignore_patterns": [
|
||||
"build/**/*"
|
||||
],
|
||||
"units": [
|
||||
{
|
||||
"name": "main/MetroTRK/mslsupp",
|
||||
@@ -96,61 +127,77 @@ 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.
|
||||
|
||||
`units` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
|
||||
#### File Watching
|
||||
|
||||
> `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".
|
||||
**`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.
|
||||
|
||||
**`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
|
||||
# or, for wgpu backend (recommended on macOS)
|
||||
$ cargo run --release --features wgpu
|
||||
git clone https://github.com/encounter/objdiff.git
|
||||
cd objdiff
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
Or install directly with cargo:
|
||||
|
||||
```shell
|
||||
cargo install --locked --git https://github.com/encounter/objdiff.git objdiff-gui objdiff-cli
|
||||
```
|
||||
|
||||
Binaries will be installed to `~/.cargo/bin` as `objdiff` and `objdiff-cli`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Install `pre-commit` to run linting and formatting automatically:
|
||||
|
||||
```shell
|
||||
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
|
||||
|
||||
Licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
|
||||
|
||||
at your option.
|
||||
|
||||
|
||||
@@ -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,39 +41,55 @@
|
||||
},
|
||||
"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",
|
||||
"*.json"
|
||||
]
|
||||
},
|
||||
"ignore_patterns": {
|
||||
"type": "array",
|
||||
"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"
|
||||
},
|
||||
"default": [
|
||||
"build/**/*"
|
||||
]
|
||||
},
|
||||
"objects": {
|
||||
"type": "array",
|
||||
"description": "Use units instead.",
|
||||
@@ -95,6 +111,25 @@
|
||||
"items": {
|
||||
"$ref": "#/$defs/progress_category"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"description": "Diff configuration options that should be applied automatically when the project is loaded.",
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"demangler": "gnu_legacy"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
@@ -103,7 +138,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",
|
||||
@@ -112,11 +147,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",
|
||||
@@ -133,6 +168,27 @@
|
||||
},
|
||||
"metadata": {
|
||||
"ref": "#/$defs/metadata"
|
||||
},
|
||||
"symbol_mappings": {
|
||||
"type": "object",
|
||||
"description": "Manual symbol mappings from target to base.",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"description": "Diff configuration options that should be applied when this unit is active.",
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -168,6 +224,10 @@
|
||||
"type": "boolean",
|
||||
"description": "If true, objdiff will run the build command with the context file as an argument to generate it.",
|
||||
"default": false
|
||||
},
|
||||
"preset_id": {
|
||||
"type": "number",
|
||||
"description": "The decomp.me preset ID to use for the scratch.\nCompiler and flags in the config will take precedence over the preset, but the preset is useful for organizational purposes."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -180,7 +240,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",
|
||||
@@ -200,7 +260,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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
182
deny.toml
182
deny.toml
@@ -9,6 +9,11 @@
|
||||
# The values provided in this template are the default values that will be used
|
||||
# when any section or field is not specified in your own configuration
|
||||
|
||||
# Root options
|
||||
|
||||
# The graph table configures how the dependency graph is constructed and thus
|
||||
# which crates the checks are performed against
|
||||
[graph]
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
@@ -20,51 +25,69 @@
|
||||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
#{ triple = "x86_64-unknown-linux-musl" },
|
||||
#"x86_64-unknown-linux-musl",
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||
]
|
||||
# When creating the dependency graph used as the source of truth when checks are
|
||||
# executed, this field can be used to prune crates from the graph, removing them
|
||||
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||
# they are connected to another crate in the graph that hasn't been pruned,
|
||||
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||
#exclude = []
|
||||
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||
# is recommended to pass `--all-features` on the cmd line instead
|
||||
all-features = false
|
||||
# If true, metadata will be collected with `--no-default-features`. The same
|
||||
# caveat with `all-features` applies
|
||||
no-default-features = false
|
||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
# The output table provides options for how/if diagnostics are outputted
|
||||
[output]
|
||||
# When outputting inclusion graphs in diagnostics that include features, this
|
||||
# option can be used to specify the depth at which feature edges will be added.
|
||||
# This option is included since the graphs can be quite large and the addition
|
||||
# of features from the crate(s) to all of the graph roots can be far too verbose.
|
||||
# This option can be overridden via `--feature-depth` on the cmd line
|
||||
feature-depth = 1
|
||||
|
||||
# This section is considered when running `cargo deny check advisories`
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory database is cloned/fetched into
|
||||
db-path = "~/.cargo/advisory-db"
|
||||
# The path where the advisory databases are cloned/fetched into
|
||||
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||
# The url(s) of the advisory databases to use
|
||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# The lint level for security vulnerabilities
|
||||
vulnerability = "deny"
|
||||
# The lint level for unmaintained crates
|
||||
unmaintained = "warn"
|
||||
# The lint level for crates that have been yanked from their source registry
|
||||
yanked = "warn"
|
||||
# The lint level for crates with security notices. Note that as of
|
||||
# 2019-12-17 there are no security notice advisories in
|
||||
# https://github.com/rustsec/advisory-db
|
||||
notice = "warn"
|
||||
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = []
|
||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||
# lower than the range specified will be ignored. Note that ignored advisories
|
||||
# will still output a note when they are encountered.
|
||||
# * None - CVSS Score 0.0
|
||||
# * Low - CVSS Score 0.1 - 3.9
|
||||
# * Medium - CVSS Score 4.0 - 6.9
|
||||
# * High - CVSS Score 7.0 - 8.9
|
||||
# * Critical - CVSS Score 9.0 - 10.0
|
||||
#severity-threshold =
|
||||
ignore = [
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate is an indirect dependency" },
|
||||
{ id = "RUSTSEC-2025-0052", reason = "Unmaintained async-std crate is an indirect dependency" },
|
||||
{ id = "RUSTSEC-2025-0119", reason = "Unmaintained number_prefix 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.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
# See Git Authentication for more information about setting up git authentication.
|
||||
#git-fetch-with-cli = true
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# The lint level for crates which do not have a detectable license
|
||||
unlicensed = "deny"
|
||||
# List of explictly allowed licenses
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
@@ -77,34 +100,13 @@ allow = [
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"MPL-2.0",
|
||||
"Unicode-DFS-2016",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
"0BSD",
|
||||
"OFL-1.1",
|
||||
"LicenseRef-UFL-1.0",
|
||||
"OpenSSL",
|
||||
"GPL-3.0",
|
||||
"Ubuntu-font-1.0",
|
||||
"CDLA-Permissive-2.0",
|
||||
]
|
||||
# List of explictly disallowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
deny = [
|
||||
#"Nokia",
|
||||
]
|
||||
# Lint level for licenses considered copyleft
|
||||
copyleft = "warn"
|
||||
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
|
||||
# * both - The license will be approved if it is both OSI-approved *AND* FSF
|
||||
# * either - The license will be approved if it is either OSI-approved *OR* FSF
|
||||
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
|
||||
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
|
||||
# * neither - This predicate is ignored and the default lint level is used
|
||||
allow-osi-fsf-free = "neither"
|
||||
# Lint level used when no other predicates are matched
|
||||
# 1. License isn't in the allow or deny lists
|
||||
# 2. License isn't copyleft
|
||||
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
|
||||
default = "deny"
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
@@ -115,17 +117,15 @@ confidence-threshold = 0.8
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], name = "adler32", version = "*" },
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
[[licenses.clarify]]
|
||||
# The name of the crate the clarification applies to
|
||||
name = "ring"
|
||||
# The optional version constraint for the crate
|
||||
version = "*"
|
||||
# The package spec the clarification applies to
|
||||
crate = "ring"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
@@ -140,7 +140,9 @@ license-files = [
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries
|
||||
# published to private registries.
|
||||
# To see how to mark a crate as unpublished (to the official registry),
|
||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
@@ -163,30 +165,63 @@ wildcards = "allow"
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# The default lint level for `default` features for crates that are members of
|
||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||
# `default` on a crate-by-crate basis if desired.
|
||||
workspace-default-features = "allow"
|
||||
# The default lint level for `default` features for external crates that are not
|
||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||
# on a crate-by-crate basis if desired.
|
||||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
]
|
||||
|
||||
# List of features to allow/deny
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#crate = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
#allow = [
|
||||
# "rustls",
|
||||
# "__rustls",
|
||||
# "__tls",
|
||||
# "hyper-rustls",
|
||||
# "rustls",
|
||||
# "rustls-pemfile",
|
||||
# "rustls-tls-webpki-roots",
|
||||
# "tokio-rustls",
|
||||
# "webpki-roots",
|
||||
#]
|
||||
# If true, the allowed features must exactly match the enabled feature set. If
|
||||
# this is set there is no point setting `deny`
|
||||
#exact = true
|
||||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
@@ -206,9 +241,12 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# 1 or more github.com organizations to allow git sources for
|
||||
github = ["encounter"]
|
||||
# 1 or more gitlab.com organizations to allow git sources for
|
||||
#gitlab = [""]
|
||||
# 1 or more bitbucket.org organizations to allow git sources for
|
||||
#bitbucket = [""]
|
||||
# github.com organizations to allow git sources for
|
||||
github = [
|
||||
"encounter",
|
||||
"gimli-rs", # gimli
|
||||
]
|
||||
# gitlab.com organizations to allow git sources for
|
||||
gitlab = []
|
||||
# bitbucket.org organizations to allow git sources for
|
||||
bitbucket = []
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
[package]
|
||||
name = "objdiff-cli"
|
||||
version = "2.0.0-beta.6"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/objdiff"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "../README.md"
|
||||
description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
publish = false
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
argp = "0.3.0"
|
||||
crossterm = "0.27.0"
|
||||
enable-ansi-support = "0.2.1"
|
||||
memmap2 = "0.9.4"
|
||||
anyhow = "1.0"
|
||||
argp = "0.4"
|
||||
crossterm = "0.29"
|
||||
enable-ansi-support = "0.3"
|
||||
memmap2 = "0.9"
|
||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||
prost = "0.13.1"
|
||||
ratatui = "0.26.2"
|
||||
rayon = "1.10.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
supports-color = "3.0.0"
|
||||
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
prost = "0.14"
|
||||
ratatui = "0.29"
|
||||
rayon = "1.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
supports-color = "3.0"
|
||||
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
typed-path = "0.12"
|
||||
|
||||
[target.'cfg(target_env = "musl")'.dependencies]
|
||||
mimalloc = "0.1"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
fn main() {
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.expect("Failed to execute git");
|
||||
let rev = String::from_utf8(output.stdout).expect("Failed to parse git output");
|
||||
println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}");
|
||||
println!("cargo:rustc-rerun-if-changed=.git/HEAD");
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
//! For now, this only adds a --version/-V option which causes early-exit.
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use argp::{parser::ParseGlobalOptions, EarlyExit, FromArgs, TopLevelCommand};
|
||||
use argp::{EarlyExit, FromArgs, TopLevelCommand, parser::ParseGlobalOptions};
|
||||
|
||||
struct ArgsOrVersion<T>(T)
|
||||
where T: FromArgs;
|
||||
@@ -31,10 +31,9 @@ where T: FromArgs
|
||||
Ok(v) => {
|
||||
if v.version {
|
||||
println!(
|
||||
"{} {} {}",
|
||||
"{} {}",
|
||||
command_name.first().unwrap_or(&""),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
env!("GIT_COMMIT_SHA"),
|
||||
);
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,33 @@
|
||||
pub mod diff;
|
||||
pub mod report;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use objdiff_core::diff::{ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig};
|
||||
|
||||
pub fn apply_config_args(diff_config: &mut DiffObjConfig, args: &[String]) -> Result<()> {
|
||||
for config in args {
|
||||
let (key, value) = config.split_once('=').context("--config expects \"key=value\"")?;
|
||||
let property_id = ConfigPropertyId::from_str(key)
|
||||
.map_err(|()| anyhow!("Invalid configuration property: {}", key))?;
|
||||
diff_config.set_property_value_str(property_id, value).map_err(|()| {
|
||||
let mut options = String::new();
|
||||
match property_id.kind() {
|
||||
ConfigPropertyKind::Boolean => {
|
||||
options = "true, false".to_string();
|
||||
}
|
||||
ConfigPropertyKind::Choice(variants) => {
|
||||
for (i, variant) in variants.iter().enumerate() {
|
||||
if i > 0 {
|
||||
options.push_str(", ");
|
||||
}
|
||||
options.push_str(variant.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow!("Invalid value for {}. Expected one of: {}", property_id.name(), options)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
time::Instant,
|
||||
};
|
||||
use std::{collections::HashSet, fs::File, io::Read, time::Instant};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use argp::FromArgs;
|
||||
use objdiff_core::{
|
||||
bindings::report::{
|
||||
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report,
|
||||
ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
|
||||
REPORT_VERSION,
|
||||
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, REPORT_VERSION,
|
||||
Report, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
|
||||
},
|
||||
config::ProjectObject,
|
||||
diff, obj,
|
||||
obj::{ObjSectionKind, ObjSymbolFlags},
|
||||
config::{ProjectObject, ProjectOptions, apply_project_options, path::platform_path},
|
||||
diff,
|
||||
obj::{self, SectionKind, SymbolFlag, SymbolKind},
|
||||
};
|
||||
use prost::Message;
|
||||
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
use tracing::{info, warn};
|
||||
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
|
||||
|
||||
use crate::util::output::{write_output, OutputFormat};
|
||||
use crate::{
|
||||
cmd::{apply_config_args, diff::ObjectConfig},
|
||||
util::output::{OutputFormat, write_output},
|
||||
};
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Generate a progress report for a project.
|
||||
@@ -43,33 +40,36 @@ pub enum SubCommand {
|
||||
/// Generate a progress report for a project.
|
||||
#[argp(subcommand, name = "generate")]
|
||||
pub struct GenerateArgs {
|
||||
#[argp(option, short = 'p')]
|
||||
#[argp(option, short = 'p', from_str_fn(platform_path))]
|
||||
/// Project directory
|
||||
project: Option<PathBuf>,
|
||||
#[argp(option, short = 'o')]
|
||||
project: Option<Utf8PlatformPathBuf>,
|
||||
#[argp(option, short = 'o', from_str_fn(platform_path))]
|
||||
/// Output file
|
||||
output: Option<PathBuf>,
|
||||
output: Option<Utf8PlatformPathBuf>,
|
||||
#[argp(switch, short = 'd')]
|
||||
/// Deduplicate global and weak symbols (runs single-threaded)
|
||||
deduplicate: bool,
|
||||
#[argp(option, short = 'f')]
|
||||
/// Output format (json, json-pretty, proto) (default: json)
|
||||
format: Option<String>,
|
||||
#[argp(option, short = 'c')]
|
||||
/// Configuration property (key=value)
|
||||
config: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// List any changes from a previous report.
|
||||
#[argp(subcommand, name = "changes")]
|
||||
pub struct ChangesArgs {
|
||||
#[argp(positional)]
|
||||
#[argp(positional, from_str_fn(platform_path))]
|
||||
/// Previous report file
|
||||
previous: PathBuf,
|
||||
#[argp(positional)]
|
||||
previous: Utf8PlatformPathBuf,
|
||||
#[argp(positional, from_str_fn(platform_path))]
|
||||
/// Current report file
|
||||
current: PathBuf,
|
||||
#[argp(option, short = 'o')]
|
||||
current: Utf8PlatformPathBuf,
|
||||
#[argp(option, short = 'o', from_str_fn(platform_path))]
|
||||
/// Output file
|
||||
output: Option<PathBuf>,
|
||||
output: Option<Utf8PlatformPathBuf>,
|
||||
#[argp(option, short = 'f')]
|
||||
/// Output format (json, json-pretty, proto) (default: json)
|
||||
format: Option<String>,
|
||||
@@ -83,18 +83,46 @@ pub fn run(args: Args) -> Result<()> {
|
||||
}
|
||||
|
||||
fn generate(args: GenerateArgs) -> Result<()> {
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
|
||||
info!("Loading project {}", project_dir.display());
|
||||
let base_diff_config = diff::DiffObjConfig {
|
||||
function_reloc_diffs: diff::FunctionRelocDiffs::None,
|
||||
combine_data_sections: true,
|
||||
combine_text_sections: true,
|
||||
ppc_calculate_pool_relocations: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut project = match objdiff_core::config::try_project_config(project_dir) {
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let project_dir = args.project.as_deref().unwrap_or_else(|| Utf8PlatformPath::new("."));
|
||||
info!("Loading project {}", project_dir);
|
||||
|
||||
let project = match objdiff_core::config::try_project_config(project_dir.as_ref()) {
|
||||
Some((Ok(config), _)) => config,
|
||||
Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
|
||||
None => bail!("No project configuration found"),
|
||||
};
|
||||
let target_obj_dir =
|
||||
project.target_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
|
||||
let base_obj_dir =
|
||||
project.base_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
|
||||
let project_units = project.units.as_deref().unwrap_or_default();
|
||||
let objects = project_units
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, o)| {
|
||||
(
|
||||
ObjectConfig::new(
|
||||
o,
|
||||
project_dir,
|
||||
target_obj_dir.as_deref(),
|
||||
base_obj_dir.as_deref(),
|
||||
),
|
||||
idx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
info!(
|
||||
"Generating report for {} units (using {} threads)",
|
||||
project.objects.len(),
|
||||
objects.len(),
|
||||
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
||||
);
|
||||
|
||||
@@ -103,36 +131,36 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
let mut existing_functions: HashSet<String> = HashSet::new();
|
||||
if args.deduplicate {
|
||||
// If deduplicating, we need to run single-threaded
|
||||
for object in &mut project.objects {
|
||||
if let Some(unit) = report_object(
|
||||
object,
|
||||
project_dir,
|
||||
project.target_dir.as_deref(),
|
||||
project.base_dir.as_deref(),
|
||||
Some(&mut existing_functions),
|
||||
)? {
|
||||
for (object, unit_idx) in &objects {
|
||||
let diff_config = build_unit_diff_config(
|
||||
&base_diff_config,
|
||||
project.options.as_ref(),
|
||||
project_units.get(*unit_idx).and_then(ProjectObject::options),
|
||||
&args.config,
|
||||
)?;
|
||||
if let Some(unit) = report_object(object, &diff_config, Some(&mut existing_functions))?
|
||||
{
|
||||
units.push(unit);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let vec = project
|
||||
.objects
|
||||
.par_iter_mut()
|
||||
.map(|object| {
|
||||
report_object(
|
||||
object,
|
||||
project_dir,
|
||||
project.target_dir.as_deref(),
|
||||
project.base_dir.as_deref(),
|
||||
None,
|
||||
)
|
||||
let vec = objects
|
||||
.par_iter()
|
||||
.map(|(object, unit_idx)| {
|
||||
let diff_config = build_unit_diff_config(
|
||||
&base_diff_config,
|
||||
project.options.as_ref(),
|
||||
project_units.get(*unit_idx).and_then(ProjectObject::options),
|
||||
&args.config,
|
||||
)?;
|
||||
report_object(object, &diff_config, None)
|
||||
})
|
||||
.collect::<Result<Vec<Option<ReportUnit>>>>()?;
|
||||
units = vec.into_iter().flatten().collect();
|
||||
}
|
||||
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
|
||||
let mut categories = Vec::new();
|
||||
for category in &project.progress_categories {
|
||||
for category in project.progress_categories() {
|
||||
categories.push(ReportCategory {
|
||||
id: category.id.clone(),
|
||||
name: category.name.clone(),
|
||||
@@ -148,72 +176,87 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_unit_diff_config(
|
||||
base: &diff::DiffObjConfig,
|
||||
project_options: Option<&ProjectOptions>,
|
||||
unit_options: Option<&ProjectOptions>,
|
||||
cli_args: &[String],
|
||||
) -> Result<diff::DiffObjConfig> {
|
||||
let mut diff_config = base.clone();
|
||||
if let Some(options) = project_options {
|
||||
apply_project_options(&mut diff_config, options)?;
|
||||
}
|
||||
if let Some(options) = unit_options {
|
||||
apply_project_options(&mut diff_config, options)?;
|
||||
}
|
||||
// CLI args override project and unit options
|
||||
apply_config_args(&mut diff_config, cli_args)?;
|
||||
Ok(diff_config)
|
||||
}
|
||||
|
||||
fn report_object(
|
||||
object: &mut ProjectObject,
|
||||
project_dir: &Path,
|
||||
target_dir: Option<&Path>,
|
||||
base_dir: Option<&Path>,
|
||||
object: &ObjectConfig,
|
||||
diff_config: &diff::DiffObjConfig,
|
||||
mut existing_functions: Option<&mut HashSet<String>>,
|
||||
) -> Result<Option<ReportUnit>> {
|
||||
object.resolve_paths(project_dir, target_dir, base_dir);
|
||||
match (&object.target_path, &object.base_path) {
|
||||
(None, Some(_)) if !object.complete().unwrap_or(false) => {
|
||||
warn!("Skipping object without target: {}", object.name());
|
||||
(None, Some(_)) if !object.complete.unwrap_or(false) => {
|
||||
warn!("Skipping object without target: {}", object.name);
|
||||
return Ok(None);
|
||||
}
|
||||
(None, None) => {
|
||||
warn!("Skipping object without target or base: {}", object.name());
|
||||
warn!("Skipping object without target or base: {}", object.name);
|
||||
return Ok(None);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
|
||||
let mapping_config = diff::MappingConfig::default();
|
||||
let target = object
|
||||
.target_path
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
|
||||
obj::read::read(p.as_ref(), diff_config, diff::DiffSide::Target)
|
||||
.with_context(|| format!("Failed to open {p}"))
|
||||
})
|
||||
.transpose()?;
|
||||
let base = object
|
||||
.base_path
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
|
||||
obj::read::read(p.as_ref(), diff_config, diff::DiffSide::Base)
|
||||
.with_context(|| format!("Failed to open {p}"))
|
||||
})
|
||||
.transpose()?;
|
||||
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
|
||||
let result =
|
||||
diff::diff_objs(target.as_ref(), base.as_ref(), None, diff_config, &mapping_config)?;
|
||||
|
||||
let metadata = ReportUnitMetadata {
|
||||
complete: object.complete(),
|
||||
complete: object.metadata.complete,
|
||||
module_name: target
|
||||
.as_ref()
|
||||
.and_then(|o| o.split_meta.as_ref())
|
||||
.and_then(|m| m.module_name.clone()),
|
||||
module_id: target.as_ref().and_then(|o| o.split_meta.as_ref()).and_then(|m| m.module_id),
|
||||
source_path: object.metadata.as_ref().and_then(|m| m.source_path.clone()),
|
||||
progress_categories: object
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(|m| m.progress_categories.clone())
|
||||
.unwrap_or_default(),
|
||||
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
|
||||
source_path: object.metadata.source_path.as_ref().map(|p| p.to_string()),
|
||||
progress_categories: object.metadata.progress_categories.clone().unwrap_or_default(),
|
||||
auto_generated: object.metadata.auto_generated,
|
||||
};
|
||||
let mut measures = Measures::default();
|
||||
let mut measures = Measures { total_units: 1, ..Default::default() };
|
||||
let mut sections = vec![];
|
||||
let mut functions = vec![];
|
||||
|
||||
let obj = target.as_ref().or(base.as_ref()).unwrap();
|
||||
let obj_diff = result.left.as_ref().or(result.right.as_ref()).unwrap();
|
||||
for (section, section_diff) in obj.sections.iter().zip(&obj_diff.sections) {
|
||||
for ((section_idx, section), section_diff) in
|
||||
obj.sections.iter().enumerate().zip(&obj_diff.sections)
|
||||
{
|
||||
if section.kind == SectionKind::Unknown {
|
||||
continue;
|
||||
}
|
||||
let section_match_percent = section_diff.match_percent.unwrap_or_else(|| {
|
||||
// Support cases where we don't have a target object,
|
||||
// assume complete means 100% match
|
||||
if object.complete().unwrap_or(false) {
|
||||
100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
if object.complete.unwrap_or(false) { 100.0 } else { 0.0 }
|
||||
});
|
||||
sections.push(ReportItem {
|
||||
name: section.name.clone(),
|
||||
@@ -223,39 +266,40 @@ fn report_object(
|
||||
demangled_name: None,
|
||||
virtual_address: section.virtual_address,
|
||||
}),
|
||||
address: None,
|
||||
});
|
||||
|
||||
match section.kind {
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => {
|
||||
SectionKind::Data | SectionKind::Bss => {
|
||||
measures.total_data += section.size;
|
||||
if section_match_percent == 100.0 {
|
||||
measures.matched_data += section.size;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
ObjSectionKind::Code => (),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for (symbol, symbol_diff) in section.symbols.iter().zip(§ion_diff.symbols) {
|
||||
if symbol.size == 0 {
|
||||
for (symbol, symbol_diff) in obj.symbols.iter().zip(&obj_diff.symbols) {
|
||||
if symbol.section != Some(section_idx)
|
||||
|| symbol.size == 0
|
||||
|| symbol.flags.contains(SymbolFlag::Hidden)
|
||||
|| symbol.flags.contains(SymbolFlag::Ignored)
|
||||
|| symbol.kind == SymbolKind::Section
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(existing_functions) = &mut existing_functions {
|
||||
if (symbol.flags.0.contains(ObjSymbolFlags::Global)
|
||||
|| symbol.flags.0.contains(ObjSymbolFlags::Weak))
|
||||
&& !existing_functions.insert(symbol.name.clone())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(existing_functions) = &mut existing_functions
|
||||
&& (symbol.flags.contains(SymbolFlag::Global)
|
||||
|| symbol.flags.contains(SymbolFlag::Weak))
|
||||
&& !existing_functions.insert(symbol.name.clone())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let match_percent = symbol_diff.match_percent.unwrap_or_else(|| {
|
||||
// Support cases where we don't have a target object,
|
||||
// assume complete means 100% match
|
||||
if object.complete().unwrap_or(false) {
|
||||
100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
if object.complete.unwrap_or(false) { 100.0 } else { 0.0 }
|
||||
});
|
||||
measures.fuzzy_match_percent += match_percent * symbol.size as f32;
|
||||
measures.total_code += symbol.size;
|
||||
@@ -270,6 +314,7 @@ fn report_object(
|
||||
demangled_name: symbol.demangled_name.clone(),
|
||||
virtual_address: symbol.virtual_address,
|
||||
}),
|
||||
address: symbol.address.checked_sub(section.address),
|
||||
});
|
||||
if match_percent == 100.0 {
|
||||
measures.matched_functions += 1;
|
||||
@@ -277,14 +322,25 @@ fn report_object(
|
||||
measures.total_functions += 1;
|
||||
}
|
||||
}
|
||||
sections.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let reverse_fn_order = object.metadata.reverse_fn_order.unwrap_or(false);
|
||||
functions.sort_by(|a, b| {
|
||||
if reverse_fn_order {
|
||||
b.address.unwrap_or(0).cmp(&a.address.unwrap_or(0))
|
||||
} else {
|
||||
a.address.unwrap_or(u64::MAX).cmp(&b.address.unwrap_or(u64::MAX))
|
||||
}
|
||||
.then_with(|| a.size.cmp(&b.size))
|
||||
});
|
||||
if metadata.complete.unwrap_or(false) {
|
||||
measures.complete_code = measures.total_code;
|
||||
measures.complete_data = measures.total_data;
|
||||
measures.complete_units = 1;
|
||||
}
|
||||
measures.calc_fuzzy_match_percent();
|
||||
measures.calc_matched_percent();
|
||||
Ok(Some(ReportUnit {
|
||||
name: object.name().to_string(),
|
||||
name: object.name.clone(),
|
||||
measures: Some(measures),
|
||||
sections,
|
||||
functions,
|
||||
@@ -294,7 +350,7 @@ fn report_object(
|
||||
|
||||
fn changes(args: ChangesArgs) -> Result<()> {
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") {
|
||||
let (previous, current) = if args.previous == "-" && args.current == "-" {
|
||||
// Special case for comparing two reports from stdin
|
||||
let mut data = vec![];
|
||||
std::io::stdin().read_to_end(&mut data)?;
|
||||
@@ -409,15 +465,14 @@ fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_report(path: &Path) -> Result<Report> {
|
||||
if path == Path::new("-") {
|
||||
fn read_report(path: &Utf8PlatformPath) -> Result<Report> {
|
||||
if path == Utf8PlatformPath::new("-") {
|
||||
let mut data = vec![];
|
||||
std::io::stdin().read_to_end(&mut data)?;
|
||||
return Report::parse(&data).with_context(|| "Failed to load report from stdin");
|
||||
}
|
||||
let file = File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
|
||||
let mmap = unsafe { memmap2::Mmap::map(&file) }
|
||||
.with_context(|| format!("Failed to map {}", path.display()))?;
|
||||
Report::parse(mmap.as_ref())
|
||||
.with_context(|| format!("Failed to load report {}", path.display()))
|
||||
let file = File::open(path).with_context(|| format!("Failed to open {path}"))?;
|
||||
let mmap =
|
||||
unsafe { memmap2::Mmap::map(&file) }.with_context(|| format!("Failed to map {path}"))?;
|
||||
Report::parse(mmap.as_ref()).with_context(|| format!("Failed to load report {path}"))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
mod argp_version;
|
||||
mod cmd;
|
||||
mod util;
|
||||
mod views;
|
||||
|
||||
// musl's allocator is very slow, so use mimalloc when targeting musl.
|
||||
// Otherwise, use the system allocator to avoid extra code size.
|
||||
#[cfg(target_env = "musl")]
|
||||
#[global_allocator]
|
||||
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
|
||||
|
||||
@@ -8,7 +17,7 @@ use anyhow::{Error, Result};
|
||||
use argp::{FromArgValue, FromArgs};
|
||||
use enable_ansi_support::enable_ansi_support;
|
||||
use supports_color::Stream;
|
||||
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
|
||||
use tracing_subscriber::{EnvFilter, filter::LevelFilter};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
enum LogLevel {
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||
@@ -34,9 +34,12 @@ impl OutputFormat {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
|
||||
where T: serde::Serialize + prost::Message {
|
||||
match output {
|
||||
pub fn write_output<T, P>(input: &T, output: Option<P>, format: OutputFormat) -> Result<()>
|
||||
where
|
||||
T: serde::Serialize + prost::Message,
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
match output.as_ref().map(|p| p.as_ref()) {
|
||||
Some(output) if output != Path::new("-") => {
|
||||
info!("Writing to {}", output.display());
|
||||
let file = File::options()
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{io::stdout, panic};
|
||||
use crossterm::{
|
||||
cursor::Show,
|
||||
event::DisableMouseCapture,
|
||||
terminal::{disable_raw_mode, LeaveAlternateScreen},
|
||||
terminal::{LeaveAlternateScreen, disable_raw_mode},
|
||||
};
|
||||
|
||||
pub fn crossterm_panic_handler() {
|
||||
|
||||
652
objdiff-cli/src/views/function_diff.rs
Normal file
652
objdiff-cli/src/views/function_diff.rs
Normal file
@@ -0,0 +1,652 @@
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};
|
||||
use objdiff_core::{
|
||||
build::BuildStatus,
|
||||
diff::{
|
||||
DiffObjConfig, FunctionRelocDiffs, InstructionDiffKind, ObjectDiff, SymbolDiff,
|
||||
display::{DiffText, DiffTextColor, HighlightKind, display_row},
|
||||
},
|
||||
obj::Object,
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
};
|
||||
|
||||
use super::{EventControlFlow, EventResult, UiView};
|
||||
use crate::cmd::diff::AppState;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Default)]
|
||||
pub struct FunctionDiffUi {
|
||||
pub symbol_name: String,
|
||||
pub left_highlight: HighlightKind,
|
||||
pub right_highlight: HighlightKind,
|
||||
pub scroll_x: usize,
|
||||
pub scroll_state_x: ScrollbarState,
|
||||
pub scroll_y: usize,
|
||||
pub scroll_state_y: ScrollbarState,
|
||||
pub per_page: usize,
|
||||
pub num_rows: usize,
|
||||
pub left_sym: Option<usize>,
|
||||
pub right_sym: Option<usize>,
|
||||
pub prev_sym: Option<usize>,
|
||||
pub open_options: bool,
|
||||
pub three_way: bool,
|
||||
}
|
||||
|
||||
impl UiView for FunctionDiffUi {
|
||||
fn draw(&mut self, state: &AppState, f: &mut Frame, result: &mut EventResult) {
|
||||
let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(f.area());
|
||||
let header_chunks = Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(chunks[0]);
|
||||
let content_chunks = if self.three_way {
|
||||
Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(chunks[1])
|
||||
} else {
|
||||
Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(chunks[1])
|
||||
};
|
||||
|
||||
self.per_page = chunks[1].height.saturating_sub(2) as usize;
|
||||
let max_scroll_y = self.num_rows.saturating_sub(self.per_page);
|
||||
if self.scroll_y > max_scroll_y {
|
||||
self.scroll_y = max_scroll_y;
|
||||
}
|
||||
self.scroll_state_y =
|
||||
self.scroll_state_y.content_length(max_scroll_y).position(self.scroll_y);
|
||||
|
||||
let mut line_l = Line::default();
|
||||
line_l
|
||||
.spans
|
||||
.push(Span::styled(self.symbol_name.clone(), Style::new().fg(Color::White).bold()));
|
||||
f.render_widget(line_l, header_chunks[0]);
|
||||
|
||||
let mut line_r = Line::default();
|
||||
if let Some(percent) = get_symbol(state.right_obj.as_ref(), self.right_sym)
|
||||
.and_then(|(_, _, d)| d.match_percent)
|
||||
{
|
||||
line_r.spans.push(Span::styled(
|
||||
format!("{percent:.2}% "),
|
||||
Style::new().fg(match_percent_color(percent)),
|
||||
));
|
||||
}
|
||||
let reload_time = state
|
||||
.reload_time
|
||||
.as_ref()
|
||||
.and_then(|t| t.format(&state.time_format).ok())
|
||||
.unwrap_or_else(|| "N/A".to_string());
|
||||
line_r.spans.push(Span::styled(
|
||||
format!("Last reload: {reload_time}"),
|
||||
Style::new().fg(Color::White),
|
||||
));
|
||||
line_r.spans.push(Span::styled(
|
||||
format!(" ({} jobs)", state.jobs.jobs.len()),
|
||||
Style::new().fg(Color::LightYellow),
|
||||
));
|
||||
f.render_widget(line_r, header_chunks[2]);
|
||||
|
||||
let mut left_text = None;
|
||||
let mut left_highlight = None;
|
||||
let mut max_width = 0;
|
||||
if let Some((obj, symbol_idx, symbol_diff)) =
|
||||
get_symbol(state.left_obj.as_ref(), self.left_sym)
|
||||
{
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[0].inner(Margin::new(0, 1));
|
||||
left_highlight = self.print_sym(
|
||||
&mut text,
|
||||
obj,
|
||||
symbol_idx,
|
||||
symbol_diff,
|
||||
&state.diff_obj_config,
|
||||
rect,
|
||||
&self.left_highlight,
|
||||
result,
|
||||
false,
|
||||
);
|
||||
max_width = max_width.max(text.width());
|
||||
left_text = Some(text);
|
||||
} else if let Some(status) = &state.left_status {
|
||||
let mut text = Text::default();
|
||||
self.print_build_status(&mut text, status);
|
||||
max_width = max_width.max(text.width());
|
||||
left_text = Some(text);
|
||||
}
|
||||
|
||||
let mut right_text = None;
|
||||
let mut right_highlight = None;
|
||||
let mut margin_text = None;
|
||||
if let Some((obj, symbol_idx, symbol_diff)) =
|
||||
get_symbol(state.right_obj.as_ref(), self.right_sym)
|
||||
{
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[2].inner(Margin::new(0, 1));
|
||||
right_highlight = self.print_sym(
|
||||
&mut text,
|
||||
obj,
|
||||
symbol_idx,
|
||||
symbol_diff,
|
||||
&state.diff_obj_config,
|
||||
rect,
|
||||
&self.right_highlight,
|
||||
result,
|
||||
false,
|
||||
);
|
||||
max_width = max_width.max(text.width());
|
||||
right_text = Some(text);
|
||||
|
||||
// Render margin
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[1].inner(Margin::new(1, 1));
|
||||
self.print_margin(&mut text, symbol_diff, rect);
|
||||
margin_text = Some(text);
|
||||
} else if let Some(status) = &state.right_status {
|
||||
let mut text = Text::default();
|
||||
self.print_build_status(&mut text, status);
|
||||
max_width = max_width.max(text.width());
|
||||
right_text = Some(text);
|
||||
}
|
||||
|
||||
let mut prev_text = None;
|
||||
let mut prev_margin_text = None;
|
||||
if self.three_way
|
||||
&& let Some((obj, symbol_idx, symbol_diff)) =
|
||||
get_symbol(state.prev_obj.as_ref(), self.prev_sym)
|
||||
{
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[4].inner(Margin::new(0, 1));
|
||||
self.print_sym(
|
||||
&mut text,
|
||||
obj,
|
||||
symbol_idx,
|
||||
symbol_diff,
|
||||
&state.diff_obj_config,
|
||||
rect,
|
||||
&self.right_highlight,
|
||||
result,
|
||||
true,
|
||||
);
|
||||
max_width = max_width.max(text.width());
|
||||
prev_text = Some(text);
|
||||
|
||||
// Render margin
|
||||
let mut text = Text::default();
|
||||
let rect = content_chunks[3].inner(Margin::new(1, 1));
|
||||
self.print_margin(&mut text, symbol_diff, rect);
|
||||
prev_margin_text = Some(text);
|
||||
}
|
||||
|
||||
let max_scroll_x =
|
||||
max_width.saturating_sub(content_chunks[0].width.min(content_chunks[2].width) as usize);
|
||||
if self.scroll_x > max_scroll_x {
|
||||
self.scroll_x = max_scroll_x;
|
||||
}
|
||||
self.scroll_state_x =
|
||||
self.scroll_state_x.content_length(max_scroll_x).position(self.scroll_x);
|
||||
|
||||
if let Some(text) = left_text {
|
||||
// Render left column
|
||||
f.render_widget(
|
||||
Paragraph::new(text)
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::new().fg(Color::Gray))
|
||||
.title_style(Style::new().bold())
|
||||
.title("TARGET"),
|
||||
)
|
||||
.scroll((0, self.scroll_x as u16)),
|
||||
content_chunks[0],
|
||||
);
|
||||
}
|
||||
if let Some(text) = margin_text {
|
||||
f.render_widget(text, content_chunks[1].inner(Margin::new(1, 1)));
|
||||
}
|
||||
if let Some(text) = right_text {
|
||||
f.render_widget(
|
||||
Paragraph::new(text)
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::new().fg(Color::Gray))
|
||||
.title_style(Style::new().bold())
|
||||
.title("CURRENT"),
|
||||
)
|
||||
.scroll((0, self.scroll_x as u16)),
|
||||
content_chunks[2],
|
||||
);
|
||||
}
|
||||
|
||||
if self.three_way {
|
||||
if let Some(text) = prev_margin_text {
|
||||
f.render_widget(text, content_chunks[3].inner(Margin::new(1, 1)));
|
||||
}
|
||||
let block = Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::new().fg(Color::Gray))
|
||||
.title_style(Style::new().bold())
|
||||
.title("SAVED");
|
||||
if let Some(text) = prev_text {
|
||||
f.render_widget(
|
||||
Paragraph::new(text).block(block.clone()).scroll((0, self.scroll_x as u16)),
|
||||
content_chunks[4],
|
||||
);
|
||||
} else {
|
||||
f.render_widget(block, content_chunks[4]);
|
||||
}
|
||||
}
|
||||
|
||||
// Render scrollbars
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight).begin_symbol(None).end_symbol(None),
|
||||
chunks[1].inner(Margin::new(0, 1)),
|
||||
&mut self.scroll_state_y,
|
||||
);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"),
|
||||
content_chunks[0],
|
||||
&mut self.scroll_state_x,
|
||||
);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"),
|
||||
content_chunks[2],
|
||||
&mut self.scroll_state_x,
|
||||
);
|
||||
if self.three_way {
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"),
|
||||
content_chunks[4],
|
||||
&mut self.scroll_state_x,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(new_highlight) = left_highlight {
|
||||
if new_highlight == self.left_highlight {
|
||||
if self.left_highlight != self.right_highlight {
|
||||
self.right_highlight = self.left_highlight.clone();
|
||||
} else {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
}
|
||||
} else {
|
||||
self.left_highlight = new_highlight;
|
||||
}
|
||||
result.redraw = true;
|
||||
} else if let Some(new_highlight) = right_highlight {
|
||||
if new_highlight == self.right_highlight {
|
||||
if self.left_highlight != self.right_highlight {
|
||||
self.left_highlight = self.right_highlight.clone();
|
||||
} else {
|
||||
self.left_highlight = HighlightKind::None;
|
||||
self.right_highlight = HighlightKind::None;
|
||||
}
|
||||
} else {
|
||||
self.right_highlight = new_highlight;
|
||||
}
|
||||
result.redraw = true;
|
||||
}
|
||||
|
||||
if self.open_options {
|
||||
self.draw_options(f, result);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, state: &mut AppState, event: Event) -> EventControlFlow {
|
||||
let mut result = EventResult::default();
|
||||
match event {
|
||||
Event::Key(event)
|
||||
if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) =>
|
||||
{
|
||||
match event.code {
|
||||
// Quit
|
||||
KeyCode::Esc | KeyCode::Char('q') => return EventControlFlow::Break,
|
||||
// Page up
|
||||
KeyCode::PageUp => {
|
||||
self.page_up(false);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Page up (shift + space)
|
||||
KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => {
|
||||
self.page_up(false);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Page down
|
||||
KeyCode::Char(' ') | KeyCode::PageDown => {
|
||||
self.page_down(false);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Page down (ctrl + f)
|
||||
KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.page_down(false);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Page up (ctrl + b)
|
||||
KeyCode::Char('b') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.page_up(false);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Half page down (ctrl + d)
|
||||
KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.page_down(true);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Half page up (ctrl + u)
|
||||
KeyCode::Char('u') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.page_up(true);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Scroll down
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
self.scroll_y += 1;
|
||||
result.redraw = true;
|
||||
}
|
||||
// Scroll up
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
self.scroll_y = self.scroll_y.saturating_sub(1);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Scroll to start
|
||||
KeyCode::Char('g') => {
|
||||
self.scroll_y = 0;
|
||||
result.redraw = true;
|
||||
}
|
||||
// Scroll to end
|
||||
KeyCode::Char('G') => {
|
||||
self.scroll_y = self.num_rows;
|
||||
result.redraw = true;
|
||||
}
|
||||
// Reload
|
||||
KeyCode::Char('r') => {
|
||||
return EventControlFlow::Reload;
|
||||
}
|
||||
// Scroll right
|
||||
KeyCode::Right | KeyCode::Char('l') => {
|
||||
self.scroll_x += 1;
|
||||
result.redraw = true;
|
||||
}
|
||||
// Scroll left
|
||||
KeyCode::Left | KeyCode::Char('h') => {
|
||||
self.scroll_x = self.scroll_x.saturating_sub(1);
|
||||
result.redraw = true;
|
||||
}
|
||||
// Cycle through function relocation diff mode
|
||||
KeyCode::Char('x') => {
|
||||
state.diff_obj_config.function_reloc_diffs =
|
||||
match state.diff_obj_config.function_reloc_diffs {
|
||||
FunctionRelocDiffs::None => FunctionRelocDiffs::NameAddress,
|
||||
FunctionRelocDiffs::NameAddress => FunctionRelocDiffs::DataValue,
|
||||
FunctionRelocDiffs::DataValue => FunctionRelocDiffs::All,
|
||||
FunctionRelocDiffs::All => FunctionRelocDiffs::None,
|
||||
};
|
||||
return EventControlFlow::Reload;
|
||||
}
|
||||
// Toggle three-way diff
|
||||
KeyCode::Char('3') => {
|
||||
self.three_way = !self.three_way;
|
||||
result.redraw = true;
|
||||
}
|
||||
// Toggle options
|
||||
KeyCode::Char('o') => {
|
||||
self.open_options = !self.open_options;
|
||||
result.redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Event::Mouse(event) => match event.kind {
|
||||
MouseEventKind::ScrollDown => {
|
||||
self.scroll_y += 3;
|
||||
result.redraw = true;
|
||||
}
|
||||
MouseEventKind::ScrollUp => {
|
||||
self.scroll_y = self.scroll_y.saturating_sub(3);
|
||||
result.redraw = true;
|
||||
}
|
||||
MouseEventKind::ScrollRight => {
|
||||
self.scroll_x += 3;
|
||||
result.redraw = true;
|
||||
}
|
||||
MouseEventKind::ScrollLeft => {
|
||||
self.scroll_x = self.scroll_x.saturating_sub(3);
|
||||
result.redraw = true;
|
||||
}
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
result.click_xy = Some((event.column, event.row));
|
||||
result.redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Resize(_, _) => {
|
||||
result.redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
EventControlFlow::Continue(result)
|
||||
}
|
||||
|
||||
fn reload(&mut self, state: &AppState) -> Result<()> {
|
||||
let left_sym =
|
||||
state.left_obj.as_ref().and_then(|(o, _)| o.symbol_by_name(&self.symbol_name));
|
||||
let right_sym =
|
||||
state.right_obj.as_ref().and_then(|(o, _)| o.symbol_by_name(&self.symbol_name));
|
||||
let prev_sym =
|
||||
state.prev_obj.as_ref().and_then(|(o, _)| o.symbol_by_name(&self.symbol_name));
|
||||
self.num_rows = match (
|
||||
get_symbol(state.left_obj.as_ref(), left_sym),
|
||||
get_symbol(state.right_obj.as_ref(), right_sym),
|
||||
) {
|
||||
(Some((_l, _ls, ld)), Some((_r, _rs, rd))) => {
|
||||
ld.instruction_rows.len().max(rd.instruction_rows.len())
|
||||
}
|
||||
(Some((_l, _ls, ld)), None) => ld.instruction_rows.len(),
|
||||
(None, Some((_r, _rs, rd))) => rd.instruction_rows.len(),
|
||||
(None, None) => 0,
|
||||
};
|
||||
self.left_sym = left_sym;
|
||||
self.right_sym = right_sym;
|
||||
self.prev_sym = prev_sym;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionDiffUi {
|
||||
pub fn draw_options(&mut self, f: &mut Frame, _result: &mut EventResult) {
|
||||
let percent_x = 50;
|
||||
let percent_y = 50;
|
||||
let popup_rect = Layout::vertical([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
])
|
||||
.split(f.area())[1];
|
||||
let popup_rect = Layout::horizontal([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
])
|
||||
.split(popup_rect)[1];
|
||||
|
||||
let popup = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Options")
|
||||
.title_style(Style::default().fg(Color::White).bg(Color::Black));
|
||||
f.render_widget(Clear, popup_rect);
|
||||
f.render_widget(popup, popup_rect);
|
||||
}
|
||||
|
||||
fn page_up(&mut self, half: bool) {
|
||||
self.scroll_y = self.scroll_y.saturating_sub(self.per_page / if half { 2 } else { 1 });
|
||||
}
|
||||
|
||||
fn page_down(&mut self, half: bool) {
|
||||
self.scroll_y += self.per_page / if half { 2 } else { 1 };
|
||||
}
|
||||
|
||||
fn print_sym(
|
||||
&self,
|
||||
out: &mut Text<'static>,
|
||||
obj: &Object,
|
||||
symbol_index: usize,
|
||||
symbol_diff: &SymbolDiff,
|
||||
diff_config: &DiffObjConfig,
|
||||
rect: Rect,
|
||||
highlight: &HighlightKind,
|
||||
result: &EventResult,
|
||||
only_changed: bool,
|
||||
) -> Option<HighlightKind> {
|
||||
let mut new_highlight = None;
|
||||
for (y, ins_row) in symbol_diff
|
||||
.instruction_rows
|
||||
.iter()
|
||||
.skip(self.scroll_y)
|
||||
.take(rect.height as usize)
|
||||
.enumerate()
|
||||
{
|
||||
if only_changed && ins_row.kind == InstructionDiffKind::None {
|
||||
out.lines.push(Line::default());
|
||||
continue;
|
||||
}
|
||||
let mut sx = rect.x;
|
||||
let sy = rect.y + y as u16;
|
||||
let mut line = Line::default();
|
||||
display_row(obj, symbol_index, ins_row, diff_config, |segment| {
|
||||
let highlight_kind = HighlightKind::from(&segment.text);
|
||||
let label_text = match segment.text {
|
||||
DiffText::Basic(text) => text.to_string(),
|
||||
DiffText::Line(num) => format!("{num} "),
|
||||
DiffText::Address(addr) => format!("{addr:x}:"),
|
||||
DiffText::Opcode(mnemonic, _op) => format!("{mnemonic} "),
|
||||
DiffText::Argument(arg) => arg.to_string(),
|
||||
DiffText::BranchDest(addr) => format!("{addr:x}"),
|
||||
DiffText::Symbol(sym) => {
|
||||
sym.demangled_name.as_ref().unwrap_or(&sym.name).clone()
|
||||
}
|
||||
DiffText::Addend(addend) => match addend.cmp(&0i64) {
|
||||
Ordering::Greater => format!("+{addend:#x}"),
|
||||
Ordering::Less => format!("-{:#x}", -addend),
|
||||
_ => String::new(),
|
||||
},
|
||||
DiffText::Spacing(n) => {
|
||||
line.spans.push(Span::raw(" ".repeat(n as usize)));
|
||||
sx += n as u16;
|
||||
return Ok(());
|
||||
}
|
||||
DiffText::Eol => return Ok(()),
|
||||
};
|
||||
|
||||
let len = label_text.len();
|
||||
let highlighted =
|
||||
highlight_kind != HighlightKind::None && *highlight == highlight_kind;
|
||||
if let Some((cx, cy)) = result.click_xy
|
||||
&& cx >= sx
|
||||
&& cx < sx + len as u16
|
||||
&& cy == sy
|
||||
{
|
||||
new_highlight = Some(highlight_kind);
|
||||
}
|
||||
let mut style = Style::new().fg(match segment.color {
|
||||
DiffTextColor::Normal => Color::Gray,
|
||||
DiffTextColor::Dim => Color::DarkGray,
|
||||
DiffTextColor::Bright => Color::White,
|
||||
DiffTextColor::DataFlow => Color::LightCyan,
|
||||
DiffTextColor::Replace => Color::Cyan,
|
||||
DiffTextColor::Delete => Color::Red,
|
||||
DiffTextColor::Insert => Color::Green,
|
||||
DiffTextColor::Rotating(i) => COLOR_ROTATION[i as usize % COLOR_ROTATION.len()],
|
||||
});
|
||||
if highlighted {
|
||||
style = style.bg(Color::DarkGray);
|
||||
}
|
||||
line.spans.push(Span::styled(label_text, style));
|
||||
sx += len as u16;
|
||||
if segment.pad_to as usize > len {
|
||||
let pad = (segment.pad_to as usize - len) as u16;
|
||||
line.spans.push(Span::raw(" ".repeat(pad as usize)));
|
||||
sx += pad;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
out.lines.push(line);
|
||||
}
|
||||
new_highlight
|
||||
}
|
||||
|
||||
fn print_margin(&self, out: &mut Text, symbol: &SymbolDiff, rect: Rect) {
|
||||
for ins_row in symbol.instruction_rows.iter().skip(self.scroll_y).take(rect.height as usize)
|
||||
{
|
||||
if ins_row.kind != InstructionDiffKind::None {
|
||||
out.lines.push(Line::raw(match ins_row.kind {
|
||||
InstructionDiffKind::Delete => "<",
|
||||
InstructionDiffKind::Insert => ">",
|
||||
_ => "|",
|
||||
}));
|
||||
} else {
|
||||
out.lines.push(Line::raw(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_build_status<'a>(&self, out: &mut Text<'a>, status: &'a BuildStatus) {
|
||||
if !status.cmdline.is_empty() {
|
||||
out.lines.push(Line::styled(status.cmdline.clone(), Style::new().fg(Color::LightBlue)));
|
||||
}
|
||||
for line in status.stdout.lines() {
|
||||
out.lines.push(Line::styled(line, Style::new().fg(Color::White)));
|
||||
}
|
||||
for line in status.stderr.lines() {
|
||||
out.lines.push(Line::styled(line, Style::new().fg(Color::Red)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const COLOR_ROTATION: [Color; 7] = [
|
||||
Color::Magenta,
|
||||
Color::Cyan,
|
||||
Color::Green,
|
||||
Color::Red,
|
||||
Color::Yellow,
|
||||
Color::Blue,
|
||||
Color::Green,
|
||||
];
|
||||
|
||||
pub fn match_percent_color(match_percent: f32) -> Color {
|
||||
if match_percent == 100.0 {
|
||||
Color::Green
|
||||
} else if match_percent >= 50.0 {
|
||||
Color::LightBlue
|
||||
} else {
|
||||
Color::LightRed
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_symbol(
|
||||
obj: Option<&(Object, ObjectDiff)>,
|
||||
sym: Option<usize>,
|
||||
) -> Option<(&Object, usize, &SymbolDiff)> {
|
||||
let (obj, diff) = obj?;
|
||||
let sym = sym?;
|
||||
Some((obj, sym, &diff.symbols[sym]))
|
||||
}
|
||||
25
objdiff-cli/src/views/mod.rs
Normal file
25
objdiff-cli/src/views/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::event::Event;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::cmd::diff::AppState;
|
||||
|
||||
pub mod function_diff;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EventResult {
|
||||
pub redraw: bool,
|
||||
pub click_xy: Option<(u16, u16)>,
|
||||
}
|
||||
|
||||
pub enum EventControlFlow {
|
||||
Break,
|
||||
Continue(EventResult),
|
||||
Reload,
|
||||
}
|
||||
|
||||
pub trait UiView {
|
||||
fn draw(&mut self, state: &AppState, f: &mut Frame, result: &mut EventResult);
|
||||
fn handle_event(&mut self, state: &mut AppState, event: Event) -> EventControlFlow;
|
||||
fn reload(&mut self, state: &AppState) -> Result<()>;
|
||||
}
|
||||
@@ -1,76 +1,212 @@
|
||||
[package]
|
||||
name = "objdiff-core"
|
||||
version = "2.0.0-beta.6"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/encounter/objdiff"
|
||||
readme = "../README.md"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
documentation = "https://docs.rs/objdiff-core"
|
||||
|
||||
[features]
|
||||
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
|
||||
any-arch = [] # Implicit, used to check if any arch is enabled
|
||||
config = ["globset", "semver", "serde_json", "serde_yaml"]
|
||||
dwarf = ["gimli"]
|
||||
mips = ["any-arch", "rabbitizer"]
|
||||
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
|
||||
x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"]
|
||||
arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
|
||||
bindings = ["serde_json", "prost", "pbjson"]
|
||||
wasm = ["bindings", "console_error_panic_hook", "console_log"]
|
||||
default = ["std"]
|
||||
all = [
|
||||
# Features
|
||||
"bindings",
|
||||
"build",
|
||||
"config",
|
||||
"dwarf",
|
||||
"serde",
|
||||
# Architectures
|
||||
"arm",
|
||||
"arm64",
|
||||
"mips",
|
||||
"ppc",
|
||||
"x86",
|
||||
"superh"
|
||||
]
|
||||
# Implicit, used to check if any arch is enabled
|
||||
any-arch = [
|
||||
"dep:flagset",
|
||||
"dep:heck",
|
||||
"dep:log",
|
||||
"dep:num-traits",
|
||||
"dep:prettyplease",
|
||||
"dep:proc-macro2",
|
||||
"dep:quote",
|
||||
"dep:regex",
|
||||
"dep:similar",
|
||||
"dep:syn",
|
||||
"dep:encoding_rs",
|
||||
"demangler",
|
||||
]
|
||||
bindings = [
|
||||
"dep:prost",
|
||||
"dep:prost-build",
|
||||
]
|
||||
build = [
|
||||
"dep:notify",
|
||||
"dep:notify-debouncer-full",
|
||||
"dep:reqwest",
|
||||
"dep:self_update",
|
||||
"dep:shell-escape",
|
||||
"dep:tempfile",
|
||||
"dep:time",
|
||||
"dep:winapi",
|
||||
]
|
||||
config = [
|
||||
"dep:globset",
|
||||
"dep:semver",
|
||||
"dep:typed-path",
|
||||
]
|
||||
dwarf = [
|
||||
"dep:gimli",
|
||||
"dep:typed-arena",
|
||||
]
|
||||
serde = [
|
||||
"dep:pbjson",
|
||||
"dep:pbjson-build",
|
||||
"dep:serde",
|
||||
"dep:serde_json",
|
||||
]
|
||||
std = [
|
||||
"anyhow/std",
|
||||
"flagset?/std",
|
||||
"log?/std",
|
||||
"num-traits?/std",
|
||||
"object/std",
|
||||
"prost?/std",
|
||||
"serde?/std",
|
||||
"similar?/std",
|
||||
"typed-arena?/std",
|
||||
"typed-path?/std",
|
||||
"dep:filetime",
|
||||
"dep:memmap2",
|
||||
]
|
||||
mips = [
|
||||
"any-arch",
|
||||
"dep:rabbitizer",
|
||||
]
|
||||
ppc = [
|
||||
"any-arch",
|
||||
"dep:cwextab",
|
||||
"dep:powerpc",
|
||||
"dep:rlwinmdec",
|
||||
]
|
||||
x86 = [
|
||||
"any-arch",
|
||||
"dep:iced-x86",
|
||||
]
|
||||
arm = [
|
||||
"any-arch",
|
||||
"dep:arm-attr",
|
||||
"dep:unarm",
|
||||
]
|
||||
arm64 = [
|
||||
"any-arch",
|
||||
"dep:yaxpeax-arch",
|
||||
"dep:yaxpeax-arm",
|
||||
]
|
||||
superh = [
|
||||
"any-arch",
|
||||
]
|
||||
demangler = [
|
||||
"dep:cpp_demangle",
|
||||
"dep:cwdemangle",
|
||||
"dep:gnuv2_demangle",
|
||||
"dep:msvc-demangler",
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["all"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
byteorder = "1.5.0"
|
||||
filetime = "0.2.23"
|
||||
flagset = "0.4.5"
|
||||
log = "0.4.21"
|
||||
memmap2 = "0.9.4"
|
||||
num-traits = "0.2.18"
|
||||
object = { version = "0.36.0", features = ["read_core", "std", "elf", "pe"], default-features = false }
|
||||
pbjson = { version = "0.7.0", optional = true }
|
||||
prost = { version = "0.13.1", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
similar = { version = "2.5.0", default-features = false }
|
||||
strum = { version = "0.26.2", features = ["derive"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
tsify-next = { version = "0.5.4", default-features = false, features = ["js"] }
|
||||
console_log = { version = "1.0.0", optional = true }
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
anyhow = { version = "1.0", default-features = false }
|
||||
filetime = { version = "0.2", optional = true }
|
||||
flagset = { version = "0.4", default-features = false, optional = true }
|
||||
itertools = { version = "0.14", default-features = false, features = ["use_alloc"] }
|
||||
log = { version = "0.4", default-features = false, optional = true }
|
||||
memmap2 = { version = "0.9", optional = true }
|
||||
num-traits = { version = "0.2", default-features = false, optional = true }
|
||||
object = { version = "0.37", default-features = false, features = ["read_core", "elf", "coff"] }
|
||||
pbjson = { version = "0.8", default-features = false, optional = true }
|
||||
prost = { version = "0.14", default-features = false, features = ["derive"], optional = true }
|
||||
regex = { version = "1.12", default-features = false, features = [], optional = true }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
|
||||
similar = { git = "https://github.com/encounter/similar.git", branch = "no_std", default-features = false, features = ["hashbrown"], optional = true }
|
||||
typed-path = { version = "0.12", default-features = false, optional = true }
|
||||
|
||||
# config
|
||||
globset = { version = "0.4.14", features = ["serde1"], optional = true }
|
||||
semver = { version = "1.0.22", optional = true }
|
||||
serde_json = { version = "1.0.116", optional = true }
|
||||
serde_yaml = { version = "0.9.34", optional = true }
|
||||
globset = { version = "0.4", default-features = false, optional = true }
|
||||
semver = { version = "1.0", default-features = false, optional = true }
|
||||
serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true }
|
||||
|
||||
# dwarf
|
||||
gimli = { version = "0.29.0", default-features = false, features = ["read-all"], optional = true }
|
||||
gimli = { git = "https://github.com/gimli-rs/gimli", rev = "7335f00e7c39fd501511584fefb0ba974117c950", default-features = false, features = ["read"], optional = true }
|
||||
typed-arena = { version = "2.0", default-features = false, optional = true }
|
||||
|
||||
# ppc
|
||||
cwdemangle = { version = "1.0.0", optional = true }
|
||||
cwextab = { version = "0.2.3", optional = true }
|
||||
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "6cbd7d888c7082c2c860f66cbb9848d633f753ed", optional = true }
|
||||
cwextab = { version = "1.1", optional = true }
|
||||
powerpc = { version = "0.4", optional = true }
|
||||
rlwinmdec = { version = "1.1", optional = true }
|
||||
|
||||
# mips
|
||||
rabbitizer = { version = "1.11.0", optional = true }
|
||||
rabbitizer = { version = "2.0.0-alpha.7", default-features = false, features = ["all_extensions"], optional = true }
|
||||
|
||||
# x86
|
||||
cpp_demangle = { version = "0.4.3", optional = true }
|
||||
iced-x86 = { version = "1.21.0", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
|
||||
msvc-demangler = { version = "0.10.0", optional = true }
|
||||
iced-x86 = { version = "1.21", default-features = false, features = ["decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums", "no_std"], optional = true }
|
||||
|
||||
# arm
|
||||
unarm = { version = "1.5.0", optional = true }
|
||||
arm-attr = { version = "0.1.1", optional = true }
|
||||
unarm = { version = "2.1", optional = true }
|
||||
arm-attr = { version = "0.2", optional = true }
|
||||
|
||||
# arm64
|
||||
yaxpeax-arch = { version = "0.3", default-features = false, optional = true }
|
||||
yaxpeax-arm = { version = "0.3", default-features = false, optional = true }
|
||||
|
||||
# build
|
||||
notify = { version = "8.2.0", optional = true }
|
||||
notify-debouncer-full = { version = "0.6.0", optional = true }
|
||||
shell-escape = { version = "0.1", optional = true }
|
||||
tempfile = { version = "3.23", optional = true }
|
||||
time = { version = "0.3", optional = true }
|
||||
encoding_rs = { version = "0.8.35", optional = true }
|
||||
|
||||
# demangler
|
||||
cpp_demangle = { version = "0.5", optional = true, default-features = false, features = ["alloc"] }
|
||||
cwdemangle = { version = "1.0", optional = true }
|
||||
gnuv2_demangle = { version = "0.4", optional = true }
|
||||
msvc-demangler = { version = "0.11", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3", optional = true, features = ["winbase"] }
|
||||
|
||||
# For Linux static binaries, use rustls
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"], optional = true }
|
||||
self_update = { version = "0.42", default-features = false, features = ["rustls"], optional = true }
|
||||
|
||||
# For all other platforms, use native TLS
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"], optional = true }
|
||||
self_update = { version = "0.42", optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.13.1"
|
||||
pbjson-build = "0.7.0"
|
||||
heck = { version = "0.5", optional = true }
|
||||
pbjson-build = { version = "0.8", optional = true }
|
||||
prettyplease = { version = "0.2", optional = true }
|
||||
proc-macro2 = { version = "1.0", optional = true }
|
||||
prost-build = { version = "0.14", optional = true }
|
||||
quote = { version = "1.0", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0" }
|
||||
syn = { version = "2.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Enable all features for tests
|
||||
objdiff-core = { path = ".", features = ["all"] }
|
||||
insta = "1.43"
|
||||
|
||||
16
objdiff-core/README.md
Normal file
16
objdiff-core/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# objdiff-core
|
||||
|
||||
objdiff-core contains the core functionality of [objdiff](https://github.com/encounter/objdiff), a tool for comparing object files in decompilation projects. See the main repository for more information.
|
||||
|
||||
## Crate feature flags
|
||||
|
||||
- **`all`**: Enables all main features.
|
||||
- **`bindings`**: Enables serialization and deserialization of objdiff data structures.
|
||||
- **`config`**: Enables objdiff configuration file support.
|
||||
- **`dwarf`**: Enables extraction of line number information from DWARF debug sections.
|
||||
- **`arm64`**: Enables the ARM64 backend powered by [yaxpeax-arm](https://github.com/iximeow/yaxpeax-arm).
|
||||
- **`arm`**: Enables the ARM backend powered by [unarm](https://github.com/AetiasHax/unarm).
|
||||
- **`mips`**: Enables the MIPS backend powered by [rabbitizer](https://github.com/Decompollaborate/rabbitizer).
|
||||
- **`ppc`**: Enables the PowerPC backend powered by [powerpc](https://github.com/encounter/powerpc-rs).
|
||||
- **`superh`**: Enables the SuperH backend powered by an included disassembler.
|
||||
- **`x86`**: Enables the x86 backend powered by [iced-x86](https://crates.io/crates/iced-x86).
|
||||
@@ -1,6 +1,17 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
#[cfg(feature = "any-arch")]
|
||||
mod config_gen;
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(feature = "bindings")]
|
||||
compile_protos();
|
||||
#[cfg(feature = "any-arch")]
|
||||
config_gen::generate_diff_config();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "bindings")]
|
||||
fn compile_protos() {
|
||||
use std::path::{Path, PathBuf};
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
|
||||
let descriptor_path = root.join("proto_descriptor.bin");
|
||||
println!("cargo:rerun-if-changed={}", descriptor_path.display());
|
||||
@@ -8,7 +19,15 @@ fn main() {
|
||||
.map(|m| m.modified().unwrap())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
|
||||
let mut run_protoc = false;
|
||||
let proto_files = vec![root.join("diff.proto"), root.join("report.proto")];
|
||||
let proto_files = root
|
||||
.read_dir()
|
||||
.unwrap()
|
||||
.filter_map(|e| {
|
||||
let e = e.unwrap();
|
||||
let path = e.path();
|
||||
(path.extension() == Some(std::ffi::OsStr::new("proto"))).then_some(path)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for proto_file in &proto_files {
|
||||
println!("cargo:rerun-if-changed={}", proto_file.display());
|
||||
let mtime = match std::fs::metadata(proto_file) {
|
||||
@@ -44,11 +63,14 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
|
||||
pbjson_build::Builder::new()
|
||||
.register_descriptors(&descriptor_set)
|
||||
.expect("Failed to register descriptors")
|
||||
.preserve_proto_field_names()
|
||||
.build(&[".objdiff"])
|
||||
.expect("Failed to build pbjson");
|
||||
#[cfg(feature = "serde")]
|
||||
{
|
||||
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
|
||||
pbjson_build::Builder::new()
|
||||
.register_descriptors(&descriptor_set)
|
||||
.expect("Failed to register descriptors")
|
||||
.preserve_proto_field_names()
|
||||
.build(&[".objdiff"])
|
||||
.expect("Failed to build pbjson");
|
||||
}
|
||||
}
|
||||
|
||||
379
objdiff-core/config-schema.json
Normal file
379
objdiff-core/config-schema.json
Normal file
@@ -0,0 +1,379 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"id": "functionRelocDiffs",
|
||||
"type": "choice",
|
||||
"default": "name_address",
|
||||
"name": "Function relocation diffs",
|
||||
"description": "How relocation targets will be diffed in the function view.",
|
||||
"items": [
|
||||
{
|
||||
"value": "none",
|
||||
"name": "None"
|
||||
},
|
||||
{
|
||||
"value": "name_address",
|
||||
"name": "Name or address"
|
||||
},
|
||||
{
|
||||
"value": "data_value",
|
||||
"name": "Data value"
|
||||
},
|
||||
{
|
||||
"value": "all",
|
||||
"name": "Name or address, data value"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "demangler",
|
||||
"type": "choice",
|
||||
"default": "auto",
|
||||
"name": "Demangler",
|
||||
"description": "Which demangler should be used to demangle each symbol.",
|
||||
"items": [
|
||||
{
|
||||
"value": "auto",
|
||||
"name": "Auto",
|
||||
"description": "Try to automatically guess the mangling format."
|
||||
},
|
||||
{
|
||||
"value": "none",
|
||||
"name": "None",
|
||||
"description": "Disable demangling."
|
||||
},
|
||||
{
|
||||
"value": "codewarrior",
|
||||
"name": "CodeWarrior"
|
||||
},
|
||||
{
|
||||
"value": "itanium",
|
||||
"name": "Itanium"
|
||||
},
|
||||
{
|
||||
"value": "msvc",
|
||||
"name": "MSVC"
|
||||
},
|
||||
{
|
||||
"value": "gnu_legacy",
|
||||
"name": "GNU g++ (Legacy)",
|
||||
"description": "Use the old GNU mangling ABI. Used up to g++ 2.9.x"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "analyzeDataFlow",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "(Experimental) Perform data flow analysis",
|
||||
"description": "Use data flow analysis to display known information about register contents where possible"
|
||||
},
|
||||
{
|
||||
"id": "showDataFlow",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"name": "Show data flow",
|
||||
"description": "Show data flow analysis results in place of register name where present"
|
||||
},
|
||||
{
|
||||
"id": "spaceBetweenArgs",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"name": "Space between args",
|
||||
"description": "Adds a space between arguments in the diff output."
|
||||
},
|
||||
{
|
||||
"id": "showSymbolSizes",
|
||||
"type": "choice",
|
||||
"default": "off",
|
||||
"name": "Show symbol sizes",
|
||||
"description": "Shows symbol sizes in the symbol view.",
|
||||
"items": [
|
||||
{
|
||||
"value": "off",
|
||||
"name": "Off"
|
||||
},
|
||||
{
|
||||
"value": "hex",
|
||||
"name": "Hex"
|
||||
},
|
||||
{
|
||||
"value": "decimal",
|
||||
"name": "Decimal"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "combineDataSections",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Combine data sections",
|
||||
"description": "Combines data sections with equal names."
|
||||
},
|
||||
{
|
||||
"id": "combineTextSections",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Combine text sections",
|
||||
"description": "Combines all text sections into one."
|
||||
},
|
||||
{
|
||||
"id": "arm.archVersion",
|
||||
"type": "choice",
|
||||
"default": "auto",
|
||||
"name": "Architecture version",
|
||||
"description": "ARM architecture version to use for disassembly.",
|
||||
"items": [
|
||||
{
|
||||
"value": "auto",
|
||||
"name": "Auto"
|
||||
},
|
||||
{
|
||||
"value": "v4",
|
||||
"name": "ARMv4"
|
||||
},
|
||||
{
|
||||
"value": "v4t",
|
||||
"name": "ARMv4T (GBA)"
|
||||
},
|
||||
{
|
||||
"value": "v5t",
|
||||
"name": "ARMv5T"
|
||||
},
|
||||
{
|
||||
"value": "v5te",
|
||||
"name": "ARMv5TE (DS)"
|
||||
},
|
||||
{
|
||||
"value": "v5tej",
|
||||
"name": "ARMv5TEJ"
|
||||
},
|
||||
{
|
||||
"value": "v6",
|
||||
"name": "ARMv6"
|
||||
},
|
||||
{
|
||||
"value": "v6k",
|
||||
"name": "ARMv6K (3DS)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "arm.vfpV2",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"name": "VFPv2 instructions",
|
||||
"description": "Adds floating-point instructions from VFPv2."
|
||||
},
|
||||
{
|
||||
"id": "arm.unifiedSyntax",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Unified syntax",
|
||||
"description": "Disassemble as unified assembly language (UAL)."
|
||||
},
|
||||
{
|
||||
"id": "arm.avRegisters",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Use A/V registers",
|
||||
"description": "Display R0-R3 as A1-A4 and R4-R11 as V1-V8."
|
||||
},
|
||||
{
|
||||
"id": "arm.r9Usage",
|
||||
"type": "choice",
|
||||
"default": "generalPurpose",
|
||||
"name": "Display R9 as",
|
||||
"items": [
|
||||
{
|
||||
"value": "generalPurpose",
|
||||
"name": "R9 or V6",
|
||||
"description": "Use R9 as a general-purpose register."
|
||||
},
|
||||
{
|
||||
"value": "sb",
|
||||
"name": "SB (static base)",
|
||||
"description": "Used for position-independent data (PID)."
|
||||
},
|
||||
{
|
||||
"value": "tr",
|
||||
"name": "TR (TLS register)",
|
||||
"description": "Used for thread-local storage."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "arm.slUsage",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Display R10 as SL",
|
||||
"description": "Used for explicit stack limits."
|
||||
},
|
||||
{
|
||||
"id": "arm.fpUsage",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Display R11 as FP",
|
||||
"description": "Used for frame pointers."
|
||||
},
|
||||
{
|
||||
"id": "arm.ipUsage",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Display R12 as IP",
|
||||
"description": "Used for interworking and long branches."
|
||||
},
|
||||
{
|
||||
"id": "mips.abi",
|
||||
"type": "choice",
|
||||
"default": "auto",
|
||||
"name": "ABI",
|
||||
"description": "MIPS ABI to use for disassembly.",
|
||||
"items": [
|
||||
{
|
||||
"value": "auto",
|
||||
"name": "Auto"
|
||||
},
|
||||
{
|
||||
"value": "o32",
|
||||
"name": "O32"
|
||||
},
|
||||
{
|
||||
"value": "o64",
|
||||
"name": "O64"
|
||||
},
|
||||
{
|
||||
"value": "n32",
|
||||
"name": "N32"
|
||||
},
|
||||
{
|
||||
"value": "n64",
|
||||
"name": "N64"
|
||||
},
|
||||
{
|
||||
"value": "eabi32",
|
||||
"name": "eabi32"
|
||||
},
|
||||
{
|
||||
"value": "eabi64",
|
||||
"name": "eabi64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mips.instrCategory",
|
||||
"type": "choice",
|
||||
"default": "auto",
|
||||
"name": "Instruction category",
|
||||
"description": "MIPS instruction category to use for disassembly.",
|
||||
"items": [
|
||||
{
|
||||
"value": "auto",
|
||||
"name": "Auto"
|
||||
},
|
||||
{
|
||||
"value": "cpu",
|
||||
"name": "CPU"
|
||||
},
|
||||
{
|
||||
"value": "rsp",
|
||||
"name": "RSP (N64)"
|
||||
},
|
||||
{
|
||||
"value": "r3000gte",
|
||||
"name": "R3000 GTE (PS1)"
|
||||
},
|
||||
{
|
||||
"value": "r4000allegrex",
|
||||
"name": "R4000 ALLEGREX (PSP)"
|
||||
},
|
||||
{
|
||||
"value": "r5900",
|
||||
"name": "R5900 EE (PS2)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mips.registerPrefix",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Register '$' prefix",
|
||||
"description": "Display MIPS register names with a '$' prefix."
|
||||
},
|
||||
{
|
||||
"id": "ppc.calculatePoolRelocations",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"name": "Calculate pooled data references",
|
||||
"description": "Display pooled data references in functions as fake relocations."
|
||||
},
|
||||
{
|
||||
"id": "x86.formatter",
|
||||
"type": "choice",
|
||||
"default": "intel",
|
||||
"name": "Format",
|
||||
"description": "x86 disassembly syntax.",
|
||||
"items": [
|
||||
{
|
||||
"value": "intel",
|
||||
"name": "Intel"
|
||||
},
|
||||
{
|
||||
"value": "gas",
|
||||
"name": "AT&T"
|
||||
},
|
||||
{
|
||||
"value": "nasm",
|
||||
"name": "NASM"
|
||||
},
|
||||
{
|
||||
"value": "masm",
|
||||
"name": "MASM"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "general",
|
||||
"name": "General",
|
||||
"properties": [
|
||||
"functionRelocDiffs",
|
||||
"demangler",
|
||||
"showSymbolSizes",
|
||||
"spaceBetweenArgs",
|
||||
"combineDataSections",
|
||||
"combineTextSections"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "arm",
|
||||
"name": "ARM",
|
||||
"properties": [
|
||||
"arm.archVersion",
|
||||
"arm.vfpV2",
|
||||
"arm.unifiedSyntax",
|
||||
"arm.avRegisters",
|
||||
"arm.r9Usage",
|
||||
"arm.slUsage",
|
||||
"arm.fpUsage",
|
||||
"arm.ipUsage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mips",
|
||||
"name": "MIPS",
|
||||
"properties": ["mips.abi", "mips.instrCategory", "mips.registerPrefix"]
|
||||
},
|
||||
{
|
||||
"id": "ppc",
|
||||
"name": "PowerPC",
|
||||
"properties": ["ppc.calculatePoolRelocations", "analyzeDataFlow"]
|
||||
},
|
||||
{
|
||||
"id": "x86",
|
||||
"name": "x86",
|
||||
"properties": ["x86.formatter"]
|
||||
}
|
||||
]
|
||||
}
|
||||
500
objdiff-core/config_gen.rs
Normal file
500
objdiff-core/config_gen.rs
Normal file
@@ -0,0 +1,500 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigSchema {
|
||||
pub properties: Vec<ConfigProperty>,
|
||||
pub groups: Vec<ConfigGroup>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ConfigProperty {
|
||||
#[serde(rename = "boolean")]
|
||||
Boolean(ConfigPropertyBoolean),
|
||||
#[serde(rename = "choice")]
|
||||
Choice(ConfigPropertyChoice),
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigPropertyBase {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigPropertyBoolean {
|
||||
#[serde(flatten)]
|
||||
pub base: ConfigPropertyBase,
|
||||
pub default: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigPropertyChoice {
|
||||
#[serde(flatten)]
|
||||
pub base: ConfigPropertyBase,
|
||||
pub default: String,
|
||||
pub items: Vec<ConfigPropertyChoiceItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigPropertyChoiceItem {
|
||||
pub value: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ConfigGroup {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub properties: Vec<String>,
|
||||
}
|
||||
|
||||
fn build_doc(name: &str, description: Option<&str>) -> TokenStream {
|
||||
let mut doc = format!(" {name}");
|
||||
let mut out = quote! { #[doc = #doc] };
|
||||
if let Some(description) = description {
|
||||
doc = format!(" {description}");
|
||||
out.extend(quote! { #[doc = ""] });
|
||||
out.extend(quote! { #[doc = #doc] });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn generate_diff_config() {
|
||||
let schema_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("config-schema.json");
|
||||
println!("cargo:rerun-if-changed={}", schema_path.display());
|
||||
let schema_file = File::open(schema_path).expect("Failed to open config schema file");
|
||||
let schema: ConfigSchema =
|
||||
serde_json::from_reader(schema_file).expect("Failed to parse config schema");
|
||||
|
||||
let mut enums = TokenStream::new();
|
||||
for property in &schema.properties {
|
||||
let ConfigProperty::Choice(choice) = property else {
|
||||
continue;
|
||||
};
|
||||
let enum_ident = format_ident!("{}", choice.base.id.to_upper_camel_case());
|
||||
let mut variants = TokenStream::new();
|
||||
let mut full_variants = TokenStream::new();
|
||||
let mut variant_info = TokenStream::new();
|
||||
let mut variant_to_str = TokenStream::new();
|
||||
let mut variant_to_name = TokenStream::new();
|
||||
let mut variant_to_description = TokenStream::new();
|
||||
let mut variant_from_str = TokenStream::new();
|
||||
for item in &choice.items {
|
||||
let variant_name = item.value.to_upper_camel_case();
|
||||
let variant_ident = format_ident!("{}", variant_name);
|
||||
let is_default = item.value == choice.default;
|
||||
variants.extend(build_doc(&item.name, item.description.as_deref()));
|
||||
if is_default {
|
||||
variants.extend(quote! { #[default] });
|
||||
}
|
||||
let value = &item.value;
|
||||
variants.extend(quote! {
|
||||
#[cfg_attr(feature = "serde", serde(rename = #value, alias = #variant_name))]
|
||||
#variant_ident,
|
||||
});
|
||||
full_variants.extend(quote! { #enum_ident::#variant_ident, });
|
||||
variant_to_str.extend(quote! { #enum_ident::#variant_ident => #value, });
|
||||
let name = &item.name;
|
||||
variant_to_name.extend(quote! { #enum_ident::#variant_ident => #name, });
|
||||
if let Some(description) = &item.description {
|
||||
variant_to_description.extend(quote! {
|
||||
#enum_ident::#variant_ident => Some(#description),
|
||||
});
|
||||
} else {
|
||||
variant_to_description.extend(quote! {
|
||||
#enum_ident::#variant_ident => None,
|
||||
});
|
||||
}
|
||||
let description = if let Some(description) = &item.description {
|
||||
quote! { Some(#description) }
|
||||
} else {
|
||||
quote! { None }
|
||||
};
|
||||
variant_info.extend(quote! {
|
||||
ConfigEnumVariantInfo {
|
||||
value: #value,
|
||||
name: #name,
|
||||
description: #description,
|
||||
is_default: #is_default,
|
||||
},
|
||||
});
|
||||
variant_from_str.extend(quote! {
|
||||
if s.eq_ignore_ascii_case(#value) { return Ok(#enum_ident::#variant_ident); }
|
||||
});
|
||||
}
|
||||
enums.extend(quote! {
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum #enum_ident {
|
||||
#variants
|
||||
}
|
||||
impl ConfigEnum for #enum_ident {
|
||||
#[inline]
|
||||
fn variants() -> &'static [Self] {
|
||||
static VARIANTS: &[#enum_ident] = &[#full_variants];
|
||||
VARIANTS
|
||||
}
|
||||
#[inline]
|
||||
fn variant_info() -> &'static [ConfigEnumVariantInfo] {
|
||||
static VARIANT_INFO: &[ConfigEnumVariantInfo] = &[
|
||||
#variant_info
|
||||
];
|
||||
VARIANT_INFO
|
||||
}
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
#variant_to_str
|
||||
}
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
#variant_to_name
|
||||
}
|
||||
}
|
||||
fn description(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
#variant_to_description
|
||||
}
|
||||
}
|
||||
}
|
||||
impl core::str::FromStr for #enum_ident {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
#variant_from_str
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut groups = TokenStream::new();
|
||||
let mut group_idents = Vec::new();
|
||||
for group in &schema.groups {
|
||||
let ident = format_ident!("CONFIG_GROUP_{}", group.id.to_shouty_snake_case());
|
||||
let id = &group.id;
|
||||
let name = &group.name;
|
||||
let description = if let Some(description) = &group.description {
|
||||
quote! { Some(#description) }
|
||||
} else {
|
||||
quote! { None }
|
||||
};
|
||||
let properties =
|
||||
group.properties.iter().map(|p| format_ident!("{}", p.to_upper_camel_case()));
|
||||
groups.extend(quote! {
|
||||
ConfigPropertyGroup {
|
||||
id: #id,
|
||||
name: #name,
|
||||
description: #description,
|
||||
properties: &[#(ConfigPropertyId::#properties,)*],
|
||||
},
|
||||
});
|
||||
group_idents.push(ident);
|
||||
}
|
||||
|
||||
let mut property_idents = Vec::new();
|
||||
let mut property_variants = TokenStream::new();
|
||||
let mut variant_info = TokenStream::new();
|
||||
let mut config_property_id_to_str = TokenStream::new();
|
||||
let mut config_property_id_to_name = TokenStream::new();
|
||||
let mut config_property_id_to_description = TokenStream::new();
|
||||
let mut config_property_id_to_kind = TokenStream::new();
|
||||
let mut property_fields = TokenStream::new();
|
||||
let mut default_fields = TokenStream::new();
|
||||
let mut get_property_value_variants = TokenStream::new();
|
||||
let mut set_property_value_variants = TokenStream::new();
|
||||
let mut set_property_value_str_variants = TokenStream::new();
|
||||
let mut config_property_id_from_str = TokenStream::new();
|
||||
for property in &schema.properties {
|
||||
let base = match property {
|
||||
ConfigProperty::Boolean(b) => &b.base,
|
||||
ConfigProperty::Choice(c) => &c.base,
|
||||
};
|
||||
let id = &base.id;
|
||||
let enum_ident = format_ident!("{}", id.to_upper_camel_case());
|
||||
property_idents.push(enum_ident.clone());
|
||||
config_property_id_to_str.extend(quote! { Self::#enum_ident => #id, });
|
||||
let name = &base.name;
|
||||
config_property_id_to_name.extend(quote! { Self::#enum_ident => #name, });
|
||||
if let Some(description) = &base.description {
|
||||
config_property_id_to_description.extend(quote! {
|
||||
Self::#enum_ident => Some(#description),
|
||||
});
|
||||
} else {
|
||||
config_property_id_to_description.extend(quote! {
|
||||
Self::#enum_ident => None,
|
||||
});
|
||||
}
|
||||
let doc = build_doc(name, base.description.as_deref());
|
||||
property_variants.extend(quote! { #doc #enum_ident, });
|
||||
property_fields.extend(doc);
|
||||
let field_ident = format_ident!("{}", id.to_snake_case());
|
||||
match property {
|
||||
ConfigProperty::Boolean(b) => {
|
||||
let default = b.default;
|
||||
if default {
|
||||
property_fields.extend(quote! {
|
||||
#[cfg_attr(feature = "serde", serde(default = "default_true"))]
|
||||
});
|
||||
}
|
||||
property_fields.extend(quote! {
|
||||
pub #field_ident: bool,
|
||||
});
|
||||
default_fields.extend(quote! {
|
||||
#field_ident: #default,
|
||||
});
|
||||
}
|
||||
ConfigProperty::Choice(_) => {
|
||||
property_fields.extend(quote! {
|
||||
pub #field_ident: #enum_ident,
|
||||
});
|
||||
default_fields.extend(quote! {
|
||||
#field_ident: #enum_ident::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
let property_value = match property {
|
||||
ConfigProperty::Boolean(_) => {
|
||||
quote! { ConfigPropertyValue::Boolean(self.#field_ident) }
|
||||
}
|
||||
ConfigProperty::Choice(_) => {
|
||||
quote! { ConfigPropertyValue::Choice(self.#field_ident.as_str()) }
|
||||
}
|
||||
};
|
||||
get_property_value_variants.extend(quote! {
|
||||
ConfigPropertyId::#enum_ident => #property_value,
|
||||
});
|
||||
match property {
|
||||
ConfigProperty::Boolean(_) => {
|
||||
set_property_value_variants.extend(quote! {
|
||||
ConfigPropertyId::#enum_ident => {
|
||||
if let ConfigPropertyValue::Boolean(value) = value {
|
||||
self.#field_ident = value;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
},
|
||||
});
|
||||
set_property_value_str_variants.extend(quote! {
|
||||
ConfigPropertyId::#enum_ident => {
|
||||
if let Ok(value) = value.parse() {
|
||||
self.#field_ident = value;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
ConfigProperty::Choice(_) => {
|
||||
set_property_value_variants.extend(quote! {
|
||||
ConfigPropertyId::#enum_ident => {
|
||||
if let ConfigPropertyValue::Choice(value) = value {
|
||||
if let Ok(value) = value.parse() {
|
||||
self.#field_ident = value;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
},
|
||||
});
|
||||
set_property_value_str_variants.extend(quote! {
|
||||
ConfigPropertyId::#enum_ident => {
|
||||
if let Ok(value) = value.parse() {
|
||||
self.#field_ident = value;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
let description = if let Some(description) = &base.description {
|
||||
quote! { Some(#description) }
|
||||
} else {
|
||||
quote! { None }
|
||||
};
|
||||
variant_info.extend(quote! {
|
||||
ConfigEnumVariantInfo {
|
||||
value: #id,
|
||||
name: #name,
|
||||
description: #description,
|
||||
is_default: false,
|
||||
},
|
||||
});
|
||||
match property {
|
||||
ConfigProperty::Boolean(_) => {
|
||||
config_property_id_to_kind.extend(quote! {
|
||||
Self::#enum_ident => ConfigPropertyKind::Boolean,
|
||||
});
|
||||
}
|
||||
ConfigProperty::Choice(_) => {
|
||||
config_property_id_to_kind.extend(quote! {
|
||||
Self::#enum_ident => ConfigPropertyKind::Choice(#enum_ident::variant_info()),
|
||||
});
|
||||
}
|
||||
}
|
||||
let snake_id = id.to_snake_case();
|
||||
config_property_id_from_str.extend(quote! {
|
||||
if s.eq_ignore_ascii_case(#id) || s.eq_ignore_ascii_case(#snake_id) {
|
||||
return Ok(Self::#enum_ident);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let tokens = quote! {
|
||||
pub trait ConfigEnum: Sized {
|
||||
fn variants() -> &'static [Self];
|
||||
fn variant_info() -> &'static [ConfigEnumVariantInfo];
|
||||
fn as_str(&self) -> &'static str;
|
||||
fn name(&self) -> &'static str;
|
||||
fn description(&self) -> Option<&'static str>;
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConfigEnumVariantInfo {
|
||||
pub value: &'static str,
|
||||
pub name: &'static str,
|
||||
pub description: Option<&'static str>,
|
||||
pub is_default: bool,
|
||||
}
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum ConfigPropertyId {
|
||||
#property_variants
|
||||
}
|
||||
impl ConfigEnum for ConfigPropertyId {
|
||||
#[inline]
|
||||
fn variants() -> &'static [Self] {
|
||||
static VARIANTS: &[ConfigPropertyId] = &[#(ConfigPropertyId::#property_idents,)*];
|
||||
VARIANTS
|
||||
}
|
||||
#[inline]
|
||||
fn variant_info() -> &'static [ConfigEnumVariantInfo] {
|
||||
static VARIANT_INFO: &[ConfigEnumVariantInfo] = &[
|
||||
#variant_info
|
||||
];
|
||||
VARIANT_INFO
|
||||
}
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
#config_property_id_to_str
|
||||
}
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
#config_property_id_to_name
|
||||
}
|
||||
}
|
||||
fn description(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
#config_property_id_to_description
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ConfigPropertyId {
|
||||
pub fn kind(&self) -> ConfigPropertyKind {
|
||||
match self {
|
||||
#config_property_id_to_kind
|
||||
}
|
||||
}
|
||||
}
|
||||
impl core::str::FromStr for ConfigPropertyId {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
#config_property_id_from_str
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConfigPropertyGroup {
|
||||
pub id: &'static str,
|
||||
pub name: &'static str,
|
||||
pub description: Option<&'static str>,
|
||||
pub properties: &'static [ConfigPropertyId],
|
||||
}
|
||||
pub static CONFIG_GROUPS: &[ConfigPropertyGroup] = &[#groups];
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum ConfigPropertyValue {
|
||||
Boolean(bool),
|
||||
Choice(&'static str),
|
||||
}
|
||||
impl ConfigPropertyValue {
|
||||
#[cfg(feature = "serde")]
|
||||
pub fn to_json(&self) -> serde_json::Value {
|
||||
match self {
|
||||
ConfigPropertyValue::Boolean(value) => serde_json::Value::Bool(*value),
|
||||
ConfigPropertyValue::Choice(value) => serde_json::Value::String(value.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl core::fmt::Display for ConfigPropertyValue {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match *self {
|
||||
ConfigPropertyValue::Boolean(value) => write!(f, "{value}"),
|
||||
ConfigPropertyValue::Choice(value) => f.write_str(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ConfigPropertyKind {
|
||||
Boolean,
|
||||
Choice(&'static [ConfigEnumVariantInfo]),
|
||||
}
|
||||
#enums
|
||||
#[cfg(feature = "serde")]
|
||||
#[inline(always)]
|
||||
fn default_true() -> bool { true }
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(default))]
|
||||
pub struct DiffObjConfig {
|
||||
#property_fields
|
||||
}
|
||||
impl Default for DiffObjConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#default_fields
|
||||
}
|
||||
}
|
||||
}
|
||||
impl DiffObjConfig {
|
||||
pub fn get_property_value(&self, id: ConfigPropertyId) -> ConfigPropertyValue {
|
||||
match id {
|
||||
#get_property_value_variants
|
||||
}
|
||||
}
|
||||
#[allow(clippy::result_unit_err)]
|
||||
pub fn set_property_value(&mut self, id: ConfigPropertyId, value: ConfigPropertyValue) -> Result<(), ()> {
|
||||
match id {
|
||||
#set_property_value_variants
|
||||
}
|
||||
}
|
||||
#[allow(clippy::result_unit_err)]
|
||||
pub fn set_property_value_str(&mut self, id: ConfigPropertyId, value: &str) -> Result<(), ()> {
|
||||
match id {
|
||||
#set_property_value_str_variants
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let file = syn::parse2(tokens).unwrap();
|
||||
let formatted = prettyplease::unparse(&file);
|
||||
std::fs::write(
|
||||
PathBuf::from(std::env::var_os("OUT_DIR").unwrap()).join("config.gen.rs"),
|
||||
formatted,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
59
objdiff-core/protos/changes.proto
Normal file
59
objdiff-core/protos/changes.proto
Normal file
@@ -0,0 +1,59 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "report.proto";
|
||||
|
||||
package objdiff.report;
|
||||
|
||||
// A pair of reports to compare and generate changes
|
||||
message ChangesInput {
|
||||
// The previous report
|
||||
Report from = 1;
|
||||
// The current report
|
||||
Report to = 2;
|
||||
}
|
||||
|
||||
// Changes between two reports
|
||||
message Changes {
|
||||
// The progress info for the previous report
|
||||
Measures from = 1;
|
||||
// The progress info for the current report
|
||||
Measures to = 2;
|
||||
// Units that changed
|
||||
repeated ChangeUnit units = 3;
|
||||
}
|
||||
|
||||
// A changed unit
|
||||
message ChangeUnit {
|
||||
// The name of the unit
|
||||
string name = 1;
|
||||
// The previous progress info (omitted if new)
|
||||
optional Measures from = 2;
|
||||
// The current progress info (omitted if removed)
|
||||
optional Measures to = 3;
|
||||
// Sections that changed
|
||||
repeated ChangeItem sections = 4;
|
||||
// Functions that changed
|
||||
repeated ChangeItem functions = 5;
|
||||
// Extra metadata for this unit
|
||||
optional ReportUnitMetadata metadata = 6;
|
||||
}
|
||||
|
||||
// A changed section or function
|
||||
message ChangeItem {
|
||||
// The name of the item
|
||||
string name = 1;
|
||||
// The previous progress info (omitted if new)
|
||||
optional ChangeItemInfo from = 2;
|
||||
// The current progress info (omitted if removed)
|
||||
optional ChangeItemInfo to = 3;
|
||||
// Extra metadata for this item
|
||||
optional ReportItemMetadata metadata = 4;
|
||||
}
|
||||
|
||||
// Progress info for a section or function
|
||||
message ChangeItemInfo {
|
||||
// The overall match percent for this item
|
||||
float fuzzy_match_percent = 1;
|
||||
// The size of the item in bytes
|
||||
uint64 size = 2;
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package objdiff.diff;
|
||||
|
||||
// A symbol
|
||||
message Symbol {
|
||||
// Name of the symbol
|
||||
string name = 1;
|
||||
// Demangled name of the symbol
|
||||
optional string demangled_name = 2;
|
||||
// Symbol address
|
||||
uint64 address = 3;
|
||||
// Symbol size
|
||||
uint64 size = 4;
|
||||
// Bitmask of SymbolFlag
|
||||
uint32 flags = 5;
|
||||
}
|
||||
|
||||
// Symbol visibility flags
|
||||
enum SymbolFlag {
|
||||
SYMBOL_NONE = 0;
|
||||
SYMBOL_GLOBAL = 1;
|
||||
SYMBOL_LOCAL = 2;
|
||||
SYMBOL_WEAK = 3;
|
||||
SYMBOL_COMMON = 4;
|
||||
SYMBOL_HIDDEN = 5;
|
||||
}
|
||||
|
||||
// A single parsed instruction
|
||||
message Instruction {
|
||||
// Instruction address
|
||||
uint64 address = 1;
|
||||
// Instruction size
|
||||
uint32 size = 2;
|
||||
// Instruction opcode
|
||||
uint32 opcode = 3;
|
||||
// Instruction mnemonic
|
||||
string mnemonic = 4;
|
||||
// Instruction formatted string
|
||||
string formatted = 5;
|
||||
// Original (unsimplified) instruction string
|
||||
optional string original = 6;
|
||||
// Instruction arguments
|
||||
repeated Argument arguments = 7;
|
||||
// Instruction relocation
|
||||
optional Relocation relocation = 8;
|
||||
// Instruction branch destination
|
||||
optional uint64 branch_dest = 9;
|
||||
// Instruction line number
|
||||
optional uint32 line_number = 10;
|
||||
}
|
||||
|
||||
// An instruction argument
|
||||
message Argument {
|
||||
oneof value {
|
||||
// Plain text
|
||||
string plain_text = 1;
|
||||
// Value
|
||||
ArgumentValue argument = 2;
|
||||
// Relocation
|
||||
ArgumentRelocation relocation = 3;
|
||||
// Branch destination
|
||||
uint64 branch_dest = 4;
|
||||
}
|
||||
}
|
||||
|
||||
// An instruction argument value
|
||||
message ArgumentValue {
|
||||
oneof value {
|
||||
// Signed integer
|
||||
int64 signed = 1;
|
||||
// Unsigned integer
|
||||
uint64 unsigned = 2;
|
||||
// Opaque value
|
||||
string opaque = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Marker type for relocation arguments
|
||||
message ArgumentRelocation {
|
||||
}
|
||||
|
||||
message Relocation {
|
||||
uint32 type = 1;
|
||||
string type_name = 2;
|
||||
RelocationTarget target = 3;
|
||||
}
|
||||
|
||||
message RelocationTarget {
|
||||
Symbol symbol = 1;
|
||||
int64 addend = 2;
|
||||
}
|
||||
|
||||
message InstructionDiff {
|
||||
DiffKind diff_kind = 1;
|
||||
optional Instruction instruction = 2;
|
||||
optional InstructionBranchFrom branch_from = 3;
|
||||
optional InstructionBranchTo branch_to = 4;
|
||||
repeated ArgumentDiff arg_diff = 5;
|
||||
}
|
||||
|
||||
message ArgumentDiff {
|
||||
optional uint32 diff_index = 1;
|
||||
}
|
||||
|
||||
enum DiffKind {
|
||||
DIFF_NONE = 0;
|
||||
DIFF_REPLACE = 1;
|
||||
DIFF_DELETE = 2;
|
||||
DIFF_INSERT = 3;
|
||||
DIFF_OP_MISMATCH = 4;
|
||||
DIFF_ARG_MISMATCH = 5;
|
||||
}
|
||||
|
||||
message InstructionBranchFrom {
|
||||
repeated uint32 instruction_index = 1;
|
||||
uint32 branch_index = 2;
|
||||
}
|
||||
|
||||
message InstructionBranchTo {
|
||||
uint32 instruction_index = 1;
|
||||
uint32 branch_index = 2;
|
||||
}
|
||||
|
||||
message FunctionDiff {
|
||||
Symbol symbol = 1;
|
||||
repeated InstructionDiff instructions = 2;
|
||||
optional float match_percent = 3;
|
||||
}
|
||||
|
||||
message DataDiff {
|
||||
DiffKind kind = 1;
|
||||
bytes data = 2;
|
||||
// May be larger than data
|
||||
uint64 size = 3;
|
||||
}
|
||||
|
||||
message SectionDiff {
|
||||
string name = 1;
|
||||
SectionKind kind = 2;
|
||||
uint64 size = 3;
|
||||
uint64 address = 4;
|
||||
repeated FunctionDiff functions = 5;
|
||||
repeated DataDiff data = 6;
|
||||
optional float match_percent = 7;
|
||||
}
|
||||
|
||||
enum SectionKind {
|
||||
SECTION_UNKNOWN = 0;
|
||||
SECTION_TEXT = 1;
|
||||
SECTION_DATA = 2;
|
||||
SECTION_BSS = 3;
|
||||
SECTION_COMMON = 4;
|
||||
}
|
||||
|
||||
message ObjectDiff {
|
||||
repeated SectionDiff sections = 1;
|
||||
}
|
||||
|
||||
message DiffResult {
|
||||
optional ObjectDiff left = 1;
|
||||
optional ObjectDiff right = 2;
|
||||
}
|
||||
Binary file not shown.
@@ -2,6 +2,18 @@ syntax = "proto3";
|
||||
|
||||
package objdiff.report;
|
||||
|
||||
// Project progress report
|
||||
message Report {
|
||||
// Overall progress info
|
||||
Measures measures = 1;
|
||||
// Units within this report
|
||||
repeated ReportUnit units = 2;
|
||||
// Report version
|
||||
uint32 version = 3;
|
||||
// Progress categories
|
||||
repeated ReportCategory categories = 4;
|
||||
}
|
||||
|
||||
// Progress info for a report or unit
|
||||
message Measures {
|
||||
// Overall match percent, including partially matched functions and data
|
||||
@@ -32,18 +44,10 @@ message Measures {
|
||||
uint64 complete_data = 13;
|
||||
// Completed (or "linked") data percent
|
||||
float complete_data_percent = 14;
|
||||
}
|
||||
|
||||
// Project progress report
|
||||
message Report {
|
||||
// Overall progress info
|
||||
Measures measures = 1;
|
||||
// Units within this report
|
||||
repeated ReportUnit units = 2;
|
||||
// Report version
|
||||
uint32 version = 3;
|
||||
// Progress categories
|
||||
repeated ReportCategory categories = 4;
|
||||
// Total number of units
|
||||
uint32 total_units = 15;
|
||||
// Completed (or "linked") units
|
||||
uint32 complete_units = 16;
|
||||
}
|
||||
|
||||
message ReportCategory {
|
||||
@@ -95,6 +99,8 @@ message ReportItem {
|
||||
float fuzzy_match_percent = 3;
|
||||
// Extra metadata for this item
|
||||
optional ReportItemMetadata metadata = 4;
|
||||
// Address of the item (section-relative offset)
|
||||
optional uint64 address = 5;
|
||||
}
|
||||
|
||||
// Extra metadata for an item
|
||||
@@ -104,57 +110,3 @@ message ReportItemMetadata {
|
||||
// The virtual address of the function or section
|
||||
optional uint64 virtual_address = 2;
|
||||
}
|
||||
|
||||
// A pair of reports to compare and generate changes
|
||||
message ChangesInput {
|
||||
// The previous report
|
||||
Report from = 1;
|
||||
// The current report
|
||||
Report to = 2;
|
||||
}
|
||||
|
||||
// Changes between two reports
|
||||
message Changes {
|
||||
// The progress info for the previous report
|
||||
Measures from = 1;
|
||||
// The progress info for the current report
|
||||
Measures to = 2;
|
||||
// Units that changed
|
||||
repeated ChangeUnit units = 3;
|
||||
}
|
||||
|
||||
// A changed unit
|
||||
message ChangeUnit {
|
||||
// The name of the unit
|
||||
string name = 1;
|
||||
// The previous progress info (omitted if new)
|
||||
optional Measures from = 2;
|
||||
// The current progress info (omitted if removed)
|
||||
optional Measures to = 3;
|
||||
// Sections that changed
|
||||
repeated ChangeItem sections = 4;
|
||||
// Functions that changed
|
||||
repeated ChangeItem functions = 5;
|
||||
// Extra metadata for this unit
|
||||
optional ReportUnitMetadata metadata = 6;
|
||||
}
|
||||
|
||||
// A changed section or function
|
||||
message ChangeItem {
|
||||
// The name of the item
|
||||
string name = 1;
|
||||
// The previous progress info (omitted if new)
|
||||
optional ChangeItemInfo from = 2;
|
||||
// The current progress info (omitted if removed)
|
||||
optional ChangeItemInfo to = 3;
|
||||
// Extra metadata for this item
|
||||
optional ReportItemMetadata metadata = 4;
|
||||
}
|
||||
|
||||
// Progress info for a section or function
|
||||
message ChangeItemInfo {
|
||||
// The overall match percent for this item
|
||||
float fuzzy_match_percent = 1;
|
||||
// The size of the item in bytes
|
||||
uint64 size = 2;
|
||||
}
|
||||
|
||||
@@ -1,40 +1,35 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, HashMap},
|
||||
};
|
||||
use alloc::{borrow::Cow, collections::BTreeMap, vec::Vec};
|
||||
use core::fmt::Write;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use arm_attr::{enums::CpuArch, tag::Tag, BuildAttrs};
|
||||
use object::{
|
||||
elf::{self, SHT_ARM_ATTRIBUTES},
|
||||
Endian, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, SectionIndex,
|
||||
SectionKind, Symbol, SymbolKind,
|
||||
};
|
||||
use unarm::{
|
||||
args::{Argument, OffsetImm, OffsetReg, Register},
|
||||
parse::{ArmVersion, ParseMode, Parser},
|
||||
DisplayOptions, ParseFlags, ParsedIns, RegNames,
|
||||
};
|
||||
use anyhow::{Result, bail};
|
||||
use arm_attr::{BuildAttrs, enums::CpuArch, tag::Tag};
|
||||
use object::{Endian as _, Object as _, ObjectSection as _, ObjectSymbol as _, elf};
|
||||
use unarm::FormatValue as _;
|
||||
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig},
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
||||
arch::{Arch, OPCODE_DATA, OPCODE_INVALID, RelocationOverride, RelocationOverrideTarget},
|
||||
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, display::InstructionPart},
|
||||
obj::{
|
||||
InstructionRef, Relocation, RelocationFlags, ResolvedInstructionRef, Section, SectionKind,
|
||||
Symbol, SymbolFlag, SymbolFlagSet, SymbolKind,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct ObjArchArm {
|
||||
#[derive(Debug)]
|
||||
pub struct ArchArm {
|
||||
/// Maps section index, to list of disasm modes (arm, thumb or data) sorted by address
|
||||
disasm_modes: HashMap<SectionIndex, Vec<DisasmMode>>,
|
||||
detected_version: Option<ArmVersion>,
|
||||
disasm_modes: BTreeMap<usize, Vec<DisasmMode>>,
|
||||
detected_version: Option<unarm::Version>,
|
||||
endianness: object::Endianness,
|
||||
}
|
||||
|
||||
impl ObjArchArm {
|
||||
pub fn new(file: &File) -> Result<Self> {
|
||||
impl ArchArm {
|
||||
pub fn new(file: &object::File) -> Result<Self> {
|
||||
let endianness = file.endianness();
|
||||
match file {
|
||||
File::Elf32(_) => {
|
||||
let disasm_modes = Self::elf_get_mapping_symbols(file);
|
||||
object::File::Elf32(_) => {
|
||||
// The disasm_modes mapping is populated later in the post_init step so that we have access to merged sections.
|
||||
let disasm_modes = BTreeMap::new();
|
||||
let detected_version = Self::elf_detect_arm_version(file)?;
|
||||
Ok(Self { disasm_modes, detected_version, endianness })
|
||||
}
|
||||
@@ -42,10 +37,11 @@ impl ObjArchArm {
|
||||
}
|
||||
}
|
||||
|
||||
fn elf_detect_arm_version(file: &File) -> Result<Option<ArmVersion>> {
|
||||
fn elf_detect_arm_version(file: &object::File) -> Result<Option<unarm::Version>> {
|
||||
// Check ARM attributes
|
||||
if let Some(arm_attrs) = file.sections().find(|s| {
|
||||
s.kind() == SectionKind::Elf(SHT_ARM_ATTRIBUTES) && s.name() == Ok(".ARM.attributes")
|
||||
s.kind() == object::SectionKind::Elf(elf::SHT_ARM_ATTRIBUTES)
|
||||
&& s.name() == Ok(".ARM.attributes")
|
||||
}) {
|
||||
let attr_data = arm_attrs.uncompressed_data()?;
|
||||
let build_attrs = BuildAttrs::new(&attr_data, match file.endianness() {
|
||||
@@ -59,16 +55,15 @@ impl ObjArchArm {
|
||||
}
|
||||
// Only checking first CpuArch tag. Others may exist, but that's very unlikely.
|
||||
let cpu_arch = subsection.into_public_tag_iter()?.find_map(|(_, tag)| {
|
||||
if let Tag::CpuArch(cpu_arch) = tag {
|
||||
Some(cpu_arch)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if let Tag::CpuArch(cpu_arch) = tag { Some(cpu_arch) } else { None }
|
||||
});
|
||||
match cpu_arch {
|
||||
Some(CpuArch::V4T) => return Ok(Some(ArmVersion::V4T)),
|
||||
Some(CpuArch::V5TE) => return Ok(Some(ArmVersion::V5Te)),
|
||||
Some(CpuArch::V6K) => return Ok(Some(ArmVersion::V6K)),
|
||||
Some(CpuArch::V4) => return Ok(Some(unarm::Version::V4)),
|
||||
Some(CpuArch::V4T) => return Ok(Some(unarm::Version::V4T)),
|
||||
Some(CpuArch::V5TE) => return Ok(Some(unarm::Version::V5Te)),
|
||||
Some(CpuArch::V5TEJ) => return Ok(Some(unarm::Version::V5Tej)),
|
||||
Some(CpuArch::V6) => return Ok(Some(unarm::Version::V6)),
|
||||
Some(CpuArch::V6K) => return Ok(Some(unarm::Version::V6K)),
|
||||
Some(arch) => bail!("ARM arch {} not supported", arch),
|
||||
None => {}
|
||||
};
|
||||
@@ -78,366 +73,532 @@ impl ObjArchArm {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn elf_get_mapping_symbols(file: &File) -> HashMap<SectionIndex, Vec<DisasmMode>> {
|
||||
file.sections()
|
||||
.filter(|s| s.kind() == SectionKind::Text)
|
||||
.map(|s| {
|
||||
let index = s.index();
|
||||
let mut mapping_symbols: Vec<_> = file
|
||||
.symbols()
|
||||
.filter(|s| s.section_index().map(|i| i == index).unwrap_or(false))
|
||||
.filter_map(|s| DisasmMode::from_symbol(&s))
|
||||
fn get_mapping_symbols(
|
||||
sections: &[Section],
|
||||
symbols: &[Symbol],
|
||||
) -> BTreeMap<usize, Vec<DisasmMode>> {
|
||||
sections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, section)| section.kind == SectionKind::Code)
|
||||
.map(|(index, _)| {
|
||||
let mut mapping_symbols: Vec<_> = symbols
|
||||
.iter()
|
||||
.filter(|s| s.section.map(|i| i == index).unwrap_or(false))
|
||||
.filter_map(DisasmMode::from_symbol)
|
||||
.collect();
|
||||
mapping_symbols.sort_unstable_by_key(|x| x.address);
|
||||
(s.index(), mapping_symbols)
|
||||
(index, mapping_symbols)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjArch for ObjArchArm {
|
||||
fn symbol_address(&self, symbol: &Symbol) -> u64 {
|
||||
let address = symbol.address();
|
||||
if symbol.kind() == SymbolKind::Text {
|
||||
address & !1
|
||||
} else {
|
||||
address
|
||||
fn unarm_options(&self, diff_config: &DiffObjConfig) -> unarm::Options {
|
||||
let mut extensions = unarm::Extensions::none();
|
||||
if diff_config.arm_vfp_v2 {
|
||||
extensions = extensions.with(unarm::Extension::VfpV2);
|
||||
}
|
||||
unarm::Options {
|
||||
version: match diff_config.arm_arch_version {
|
||||
ArmArchVersion::Auto => self.detected_version.unwrap_or(unarm::Version::V5Te),
|
||||
ArmArchVersion::V4 => unarm::Version::V4,
|
||||
ArmArchVersion::V4t => unarm::Version::V4T,
|
||||
ArmArchVersion::V5t => unarm::Version::V5T,
|
||||
ArmArchVersion::V5te => unarm::Version::V5Te,
|
||||
ArmArchVersion::V5tej => unarm::Version::V5Tej,
|
||||
ArmArchVersion::V6 => unarm::Version::V6,
|
||||
ArmArchVersion::V6k => unarm::Version::V6K,
|
||||
},
|
||||
extensions,
|
||||
av: diff_config.arm_av_registers,
|
||||
r9_use: match diff_config.arm_r9_usage {
|
||||
ArmR9Usage::GeneralPurpose => unarm::R9Use::R9,
|
||||
ArmR9Usage::Sb => unarm::R9Use::Sb,
|
||||
ArmR9Usage::Tr => unarm::R9Use::Tr,
|
||||
},
|
||||
sl: diff_config.arm_sl_usage,
|
||||
fp: diff_config.arm_fp_usage,
|
||||
ip: diff_config.arm_ip_usage,
|
||||
ual: diff_config.arm_unified_syntax,
|
||||
}
|
||||
}
|
||||
|
||||
fn process_code(
|
||||
fn parse_ins_ref(
|
||||
&self,
|
||||
ins_ref: InstructionRef,
|
||||
code: &[u8],
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<unarm::Ins> {
|
||||
let code = match (self.endianness, ins_ref.size) {
|
||||
(object::Endianness::Little, 2) => u16::from_le_bytes([code[0], code[1]]) as u32,
|
||||
(object::Endianness::Little, 4) => {
|
||||
u32::from_le_bytes([code[0], code[1], code[2], code[3]])
|
||||
}
|
||||
(object::Endianness::Big, 2) => u16::from_be_bytes([code[0], code[1]]) as u32,
|
||||
(object::Endianness::Big, 4) => {
|
||||
u32::from_be_bytes([code[0], code[1], code[2], code[3]])
|
||||
}
|
||||
_ => bail!("Invalid instruction size {}", ins_ref.size),
|
||||
};
|
||||
|
||||
let thumb = ins_ref.opcode & (1 << 15) == 0;
|
||||
let discriminant = ins_ref.opcode & !(1 << 15);
|
||||
let pc = ins_ref.address as u32;
|
||||
let options = self.unarm_options(diff_config);
|
||||
|
||||
let ins = if ins_ref.opcode == OPCODE_DATA {
|
||||
match ins_ref.size {
|
||||
4 => unarm::Ins::Word(code),
|
||||
2 => unarm::Ins::HalfWord(code as u16),
|
||||
_ => bail!("Invalid data size {}", ins_ref.size),
|
||||
}
|
||||
} else if thumb {
|
||||
unarm::parse_thumb_with_discriminant(code, discriminant, pc, &options)
|
||||
} else {
|
||||
unarm::parse_arm_with_discriminant(code, discriminant, pc, &options)
|
||||
};
|
||||
Ok(ins)
|
||||
}
|
||||
}
|
||||
|
||||
impl Arch for ArchArm {
|
||||
fn post_init(&mut self, sections: &[Section], symbols: &[Symbol]) {
|
||||
self.disasm_modes = Self::get_mapping_symbols(sections, symbols);
|
||||
}
|
||||
|
||||
fn scan_instructions_internal(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
_relocations: &[Relocation],
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<Vec<InstructionRef>> {
|
||||
let start_addr = address as u32;
|
||||
let end_addr = start_addr + code.len() as u32;
|
||||
|
||||
// Mapping symbols decide what kind of data comes after it. $a for ARM code, $t for Thumb code and $d for data.
|
||||
let fallback_mappings = [DisasmMode { address: start_addr, mapping: ParseMode::Arm }];
|
||||
let fallback_mappings =
|
||||
[DisasmMode { address: start_addr, mapping: unarm::ParseMode::Arm }];
|
||||
let mapping_symbols = self
|
||||
.disasm_modes
|
||||
.get(&SectionIndex(section_index))
|
||||
.get(§ion_index)
|
||||
.map(|x| x.as_slice())
|
||||
.unwrap_or(&fallback_mappings);
|
||||
let first_mapping_idx =
|
||||
match mapping_symbols.binary_search_by_key(&start_addr, |x| x.address) {
|
||||
Ok(idx) => idx,
|
||||
Err(idx) => idx - 1,
|
||||
};
|
||||
let first_mapping = mapping_symbols[first_mapping_idx].mapping;
|
||||
let first_mapping_idx = mapping_symbols
|
||||
.binary_search_by_key(&start_addr, |x| x.address)
|
||||
.unwrap_or_else(|idx| idx.saturating_sub(1));
|
||||
let mut mode = mapping_symbols[first_mapping_idx].mapping;
|
||||
|
||||
let mut mappings_iter =
|
||||
mapping_symbols.iter().skip(first_mapping_idx + 1).take_while(|x| x.address < end_addr);
|
||||
let mut mappings_iter = mapping_symbols
|
||||
.iter()
|
||||
.copied()
|
||||
.skip(first_mapping_idx + 1)
|
||||
.take_while(|x| x.address < end_addr);
|
||||
let mut next_mapping = mappings_iter.next();
|
||||
|
||||
let ins_count = code.len() / first_mapping.instruction_size(start_addr);
|
||||
let mut ops = Vec::<u16>::with_capacity(ins_count);
|
||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||
let min_ins_size = if mode == unarm::ParseMode::Thumb { 2 } else { 4 };
|
||||
let ins_count = code.len() / min_ins_size;
|
||||
let mut ops = Vec::<InstructionRef>::with_capacity(ins_count);
|
||||
|
||||
let version = match config.arm_arch_version {
|
||||
ArmArchVersion::Auto => self.detected_version.unwrap_or(ArmVersion::V5Te),
|
||||
ArmArchVersion::V4T => ArmVersion::V4T,
|
||||
ArmArchVersion::V5TE => ArmVersion::V5Te,
|
||||
ArmArchVersion::V6K => ArmVersion::V6K,
|
||||
};
|
||||
let endian = match self.endianness {
|
||||
object::Endianness::Little => unarm::Endian::Little,
|
||||
object::Endianness::Big => unarm::Endian::Big,
|
||||
};
|
||||
let options = self.unarm_options(diff_config);
|
||||
|
||||
let parse_flags = ParseFlags { ual: config.arm_unified_syntax, version };
|
||||
|
||||
let mut parser = Parser::new(first_mapping, start_addr, endian, parse_flags, code);
|
||||
|
||||
let display_options = DisplayOptions {
|
||||
reg_names: RegNames {
|
||||
av_registers: config.arm_av_registers,
|
||||
r9_use: match config.arm_r9_usage {
|
||||
ArmR9Usage::GeneralPurpose => unarm::R9Use::GeneralPurpose,
|
||||
ArmR9Usage::Sb => unarm::R9Use::Pid,
|
||||
ArmR9Usage::Tr => unarm::R9Use::Tls,
|
||||
},
|
||||
explicit_stack_limit: config.arm_sl_usage,
|
||||
frame_pointer: config.arm_fp_usage,
|
||||
ip: config.arm_ip_usage,
|
||||
},
|
||||
};
|
||||
|
||||
while let Some((address, ins, parsed_ins)) = parser.next() {
|
||||
if let Some(next) = next_mapping {
|
||||
let next_address = parser.address;
|
||||
if next_address >= next.address {
|
||||
// Change mapping
|
||||
parser.mode = next.mapping;
|
||||
next_mapping = mappings_iter.next();
|
||||
}
|
||||
let mut address = start_addr;
|
||||
while address < end_addr {
|
||||
while let Some(next) = next_mapping.filter(|x| address >= x.address) {
|
||||
// Change mapping
|
||||
mode = next.mapping;
|
||||
next_mapping = mappings_iter.next();
|
||||
}
|
||||
let line = line_info.range(..=address as u64).last().map(|(_, &b)| b);
|
||||
|
||||
let reloc = relocations.iter().find(|r| (r.address as u32 & !1) == address).cloned();
|
||||
let data = &code[(address - start_addr) as usize..];
|
||||
if data.len() < min_ins_size {
|
||||
// Push the remainder as data
|
||||
ops.push(InstructionRef {
|
||||
address: address as u64,
|
||||
size: data.len() as u8,
|
||||
opcode: OPCODE_DATA,
|
||||
branch_dest: None,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
let mut reloc_arg = None;
|
||||
if let Some(reloc) = &reloc {
|
||||
match reloc.flags {
|
||||
// Calls
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_THM_XPC22 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
|
||||
reloc_arg = parsed_ins
|
||||
.args
|
||||
.iter()
|
||||
.rposition(|a| matches!(a, Argument::BranchDest(_)));
|
||||
}
|
||||
// Data
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_ABS32 } => {
|
||||
reloc_arg =
|
||||
parsed_ins.args.iter().rposition(|a| matches!(a, Argument::UImm(_)));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
};
|
||||
|
||||
let (args, branch_dest) = if reloc.is_some() && parser.mode == ParseMode::Data {
|
||||
(vec![ObjInsArg::Reloc], None)
|
||||
// Check how many bytes we can/should read
|
||||
let num_code_bytes = if mode == unarm::ParseMode::Data {
|
||||
// 32-bit .word value should be aligned on a 4-byte boundary, otherwise use .hword
|
||||
if address & 3 == 0 { 4 } else { 2 }
|
||||
} else if data.len() >= 4 {
|
||||
// Read 4 bytes even for Thumb, as the parser will determine if it's a 2 or 4 byte instruction
|
||||
4
|
||||
} else if mode != unarm::ParseMode::Arm {
|
||||
2
|
||||
} else {
|
||||
push_args(&parsed_ins, config, reloc_arg, address, display_options)?
|
||||
// Invalid instruction size
|
||||
ops.push(InstructionRef {
|
||||
address: address as u64,
|
||||
size: min_ins_size as u8,
|
||||
opcode: OPCODE_INVALID,
|
||||
branch_dest: None,
|
||||
});
|
||||
address += min_ins_size as u32;
|
||||
continue;
|
||||
};
|
||||
|
||||
ops.push(ins.opcode_id());
|
||||
insts.push(ObjIns {
|
||||
let code = match num_code_bytes {
|
||||
4 => match self.endianness {
|
||||
object::Endianness::Little => {
|
||||
u32::from_le_bytes([data[0], data[1], data[2], data[3]])
|
||||
}
|
||||
object::Endianness::Big => {
|
||||
if mode != unarm::ParseMode::Thumb {
|
||||
u32::from_be_bytes([data[0], data[1], data[2], data[3]])
|
||||
} else {
|
||||
// For 4-byte Thumb instructions, read two 16-bit halfwords in big endian
|
||||
u32::from_be_bytes([data[2], data[3], data[0], data[1]])
|
||||
}
|
||||
}
|
||||
},
|
||||
2 => match self.endianness {
|
||||
object::Endianness::Little => u16::from_le_bytes([data[0], data[1]]) as u32,
|
||||
object::Endianness::Big => u16::from_be_bytes([data[0], data[1]]) as u32,
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let (opcode, ins, ins_size) = match mode {
|
||||
unarm::ParseMode::Arm => {
|
||||
let ins = unarm::parse_arm(code, address, &options);
|
||||
let opcode = ins.discriminant() | (1 << 15);
|
||||
(opcode, ins, 4)
|
||||
}
|
||||
unarm::ParseMode::Thumb => {
|
||||
let (ins, size) = unarm::parse_thumb(code, address, &options);
|
||||
let opcode = ins.discriminant();
|
||||
(opcode, ins, size)
|
||||
}
|
||||
unarm::ParseMode::Data => (
|
||||
OPCODE_DATA,
|
||||
if num_code_bytes == 4 {
|
||||
unarm::Ins::Word(code)
|
||||
} else {
|
||||
unarm::Ins::HalfWord(code as u16)
|
||||
},
|
||||
num_code_bytes,
|
||||
),
|
||||
};
|
||||
|
||||
let branch_dest = match ins {
|
||||
unarm::Ins::B { target, .. }
|
||||
| unarm::Ins::Bl { target, .. }
|
||||
| unarm::Ins::Blx { target: unarm::BlxTarget::Direct(target), .. } => {
|
||||
Some(target.addr)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
ops.push(InstructionRef {
|
||||
address: address as u64,
|
||||
size: (parser.address - address) as u8,
|
||||
op: ins.opcode_id(),
|
||||
mnemonic: parsed_ins.mnemonic.to_string(),
|
||||
args,
|
||||
reloc,
|
||||
branch_dest,
|
||||
line,
|
||||
formatted: parsed_ins.display(display_options).to_string(),
|
||||
orig: None,
|
||||
size: ins_size as u8,
|
||||
opcode,
|
||||
branch_dest: branch_dest.map(|x| x as u64),
|
||||
});
|
||||
address += ins_size;
|
||||
}
|
||||
|
||||
Ok(ProcessCodeResult { ops, insts })
|
||||
Ok(ops)
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
fn display_instruction(
|
||||
&self,
|
||||
_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
resolved: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let ins = self.parse_ins_ref(resolved.ins_ref, resolved.code, diff_config)?;
|
||||
|
||||
let options = self.unarm_options(diff_config);
|
||||
let mut string_fmt = unarm::StringFormatter::new(&options);
|
||||
ins.write_opcode(&mut string_fmt)?;
|
||||
let opcode = string_fmt.into_string();
|
||||
cb(InstructionPart::opcode(opcode, resolved.ins_ref.opcode))?;
|
||||
|
||||
let mut args_formatter =
|
||||
ArgsFormatter { options: &options, cb, resolved: &resolved, skip_leading_space: true };
|
||||
ins.write_params(&mut args_formatter)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn relocation_override(
|
||||
&self,
|
||||
_file: &object::File<'_>,
|
||||
section: &object::Section,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> anyhow::Result<i64> {
|
||||
let address = address as usize;
|
||||
Ok(match reloc.flags() {
|
||||
// ARM calls
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
|
||||
let data = section.data[address..address + 4].try_into()?;
|
||||
let addend = self.endianness.read_i32_bytes(data);
|
||||
let imm24 = addend & 0xffffff;
|
||||
(imm24 << 2) << 8 >> 8
|
||||
relocation: &object::Relocation,
|
||||
) -> Result<Option<RelocationOverride>> {
|
||||
match relocation.flags() {
|
||||
// Handle ELF implicit relocations
|
||||
object::RelocationFlags::Elf { r_type } => {
|
||||
if relocation.has_implicit_addend() {
|
||||
let section_data = section.data()?;
|
||||
let address = address as usize;
|
||||
let addend = match r_type {
|
||||
// ARM calls
|
||||
elf::R_ARM_PC24 | elf::R_ARM_XPC25 | elf::R_ARM_CALL => {
|
||||
let data = section_data[address..address + 4].try_into()?;
|
||||
let addend = self.endianness.read_i32_bytes(data);
|
||||
let imm24 = addend & 0xffffff;
|
||||
(imm24 << 2) << 8 >> 8
|
||||
}
|
||||
|
||||
// Thumb calls
|
||||
elf::R_ARM_THM_PC22 | elf::R_ARM_THM_XPC22 => {
|
||||
let data = section_data[address..address + 2].try_into()?;
|
||||
let high = self.endianness.read_i16_bytes(data) as i32;
|
||||
let data = section_data[address + 2..address + 4].try_into()?;
|
||||
let low = self.endianness.read_i16_bytes(data) as i32;
|
||||
|
||||
let imm22 = ((high & 0x7ff) << 11) | (low & 0x7ff);
|
||||
(imm22 << 1) << 9 >> 9
|
||||
}
|
||||
|
||||
// Data
|
||||
elf::R_ARM_ABS32 => {
|
||||
let data = section_data[address..address + 4].try_into()?;
|
||||
self.endianness.read_i32_bytes(data)
|
||||
}
|
||||
|
||||
flags => bail!("Unsupported ARM implicit relocation {flags:?}"),
|
||||
};
|
||||
Ok(Some(RelocationOverride {
|
||||
target: RelocationOverrideTarget::Keep,
|
||||
addend: addend as i64,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
// Thumb calls
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_XPC22 } => {
|
||||
let data = section.data[address..address + 2].try_into()?;
|
||||
let high = self.endianness.read_i16_bytes(data) as i32;
|
||||
let data = section.data[address + 2..address + 4].try_into()?;
|
||||
let low = self.endianness.read_i16_bytes(data) as i32;
|
||||
|
||||
let imm22 = ((high & 0x7ff) << 11) | (low & 0x7ff);
|
||||
(imm22 << 1) << 9 >> 9
|
||||
}
|
||||
|
||||
// Data
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_ABS32 } => {
|
||||
let data = section.data[address..address + 4].try_into()?;
|
||||
self.endianness.read_i32_bytes(data)
|
||||
}
|
||||
|
||||
flags => bail!("Unsupported ARM implicit relocation {flags:?}"),
|
||||
} as i64)
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_ARM_NONE => Some("R_ARM_NONE"),
|
||||
elf::R_ARM_ABS32 => Some("R_ARM_ABS32"),
|
||||
elf::R_ARM_REL32 => Some("R_ARM_REL32"),
|
||||
elf::R_ARM_ABS16 => Some("R_ARM_ABS16"),
|
||||
elf::R_ARM_ABS8 => Some("R_ARM_ABS8"),
|
||||
elf::R_ARM_THM_PC22 => Some("R_ARM_THM_PC22"),
|
||||
elf::R_ARM_THM_XPC22 => Some("R_ARM_THM_XPC22"),
|
||||
elf::R_ARM_PC24 => Some("R_ARM_PC24"),
|
||||
elf::R_ARM_XPC25 => Some("R_ARM_XPC25"),
|
||||
elf::R_ARM_CALL => Some("R_ARM_CALL"),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
||||
Cow::Owned(format!("<{flags:?}>"))
|
||||
fn data_reloc_size(&self, flags: RelocationFlags) -> usize {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_ARM_NONE => 0,
|
||||
elf::R_ARM_ABS32 => 4,
|
||||
elf::R_ARM_REL32 => 4,
|
||||
elf::R_ARM_ABS16 => 2,
|
||||
elf::R_ARM_ABS8 => 1,
|
||||
elf::R_ARM_THM_PC22 => 4,
|
||||
elf::R_ARM_THM_XPC22 => 4,
|
||||
elf::R_ARM_PC24 => 4,
|
||||
elf::R_ARM_XPC25 => 4,
|
||||
elf::R_ARM_CALL => 4,
|
||||
_ => 1,
|
||||
},
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn symbol_address(&self, address: u64, kind: SymbolKind) -> u64 {
|
||||
if kind == SymbolKind::Function { address & !1 } else { address }
|
||||
}
|
||||
|
||||
fn extra_symbol_flags(&self, symbol: &object::Symbol) -> SymbolFlagSet {
|
||||
let mut flags = SymbolFlagSet::default();
|
||||
if DisasmMode::from_object_symbol(symbol).is_some() {
|
||||
flags |= SymbolFlag::Hidden;
|
||||
}
|
||||
flags
|
||||
}
|
||||
|
||||
fn infer_function_size(
|
||||
&self,
|
||||
symbol: &Symbol,
|
||||
section: &Section,
|
||||
mut next_address: u64,
|
||||
) -> Result<u64> {
|
||||
// TODO: This should probably check the disasm mode and trim accordingly,
|
||||
// but self.disasm_modes isn't populated until post_init, so it needs a refactor.
|
||||
|
||||
// Trim any trailing 2-byte zeroes from the end (padding)
|
||||
while next_address >= symbol.address + 2
|
||||
&& let Some(data) = section.data_range(next_address - 2, 2)
|
||||
&& data == [0u8; 2]
|
||||
&& section.relocation_at(next_address - 2, 2).is_none()
|
||||
{
|
||||
next_address -= 2;
|
||||
}
|
||||
Ok(next_address.saturating_sub(symbol.address))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct DisasmMode {
|
||||
address: u32,
|
||||
mapping: ParseMode,
|
||||
mapping: unarm::ParseMode,
|
||||
}
|
||||
|
||||
impl DisasmMode {
|
||||
fn from_symbol<'a>(sym: &Symbol<'a, '_, &'a [u8]>) -> Option<Self> {
|
||||
if let Ok(name) = sym.name() {
|
||||
ParseMode::from_mapping_symbol(name)
|
||||
.map(|mapping| DisasmMode { address: sym.address() as u32, mapping })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
fn from_object_symbol<'a>(sym: &object::Symbol<'a, '_, &'a [u8]>) -> Option<Self> {
|
||||
sym.name()
|
||||
.ok()
|
||||
.and_then(unarm::ParseMode::from_mapping_symbol)
|
||||
.map(|mapping| DisasmMode { address: sym.address() as u32, mapping })
|
||||
}
|
||||
|
||||
fn from_symbol(sym: &Symbol) -> Option<Self> {
|
||||
unarm::ParseMode::from_mapping_symbol(&sym.name)
|
||||
.map(|mapping| DisasmMode { address: sym.address as u32, mapping })
|
||||
}
|
||||
}
|
||||
|
||||
fn push_args(
|
||||
parsed_ins: &ParsedIns,
|
||||
config: &DiffObjConfig,
|
||||
reloc_arg: Option<usize>,
|
||||
cur_addr: u32,
|
||||
display_options: DisplayOptions,
|
||||
) -> Result<(Vec<ObjInsArg>, Option<u64>)> {
|
||||
let mut args = vec![];
|
||||
let mut branch_dest = None;
|
||||
let mut writeback = false;
|
||||
let mut deref = false;
|
||||
for (i, arg) in parsed_ins.args_iter().enumerate() {
|
||||
// Emit punctuation before separator
|
||||
if deref {
|
||||
match arg {
|
||||
Argument::OffsetImm(OffsetImm { post_indexed: true, value: _ })
|
||||
| Argument::OffsetReg(OffsetReg { add: _, post_indexed: true, reg: _ })
|
||||
| Argument::CoOption(_) => {
|
||||
deref = false;
|
||||
args.push(ObjInsArg::PlainText("]".into()));
|
||||
if writeback {
|
||||
writeback = false;
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
pub struct ArgsFormatter<'a> {
|
||||
options: &'a unarm::Options,
|
||||
cb: &'a mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
resolved: &'a ResolvedInstructionRef<'a>,
|
||||
skip_leading_space: bool,
|
||||
}
|
||||
|
||||
impl ArgsFormatter<'_> {
|
||||
fn write(&mut self, part: InstructionPart) -> core::fmt::Result {
|
||||
(self.cb)(part).map_err(|_| core::fmt::Error)
|
||||
}
|
||||
|
||||
fn write_opaque<F>(&mut self, value: F) -> core::fmt::Result
|
||||
where F: unarm::FormatValue {
|
||||
let mut string_fmt = unarm::StringFormatter::new(self.options);
|
||||
value.write(&mut string_fmt)?;
|
||||
self.write(InstructionPart::opaque(string_fmt.into_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for ArgsFormatter<'_> {
|
||||
fn write_str(&mut self, s: &str) -> core::fmt::Result { self.write(InstructionPart::basic(s)) }
|
||||
}
|
||||
|
||||
impl unarm::FormatIns for ArgsFormatter<'_> {
|
||||
fn options(&self) -> &unarm::Options { self.options }
|
||||
|
||||
fn write_ins(&mut self, ins: &unarm::Ins) -> core::fmt::Result {
|
||||
let mut string_fmt = unarm::StringFormatter::new(self.options);
|
||||
ins.write_opcode(&mut string_fmt)?;
|
||||
let opcode = string_fmt.into_string();
|
||||
self.write(InstructionPart::Opcode(Cow::Owned(opcode), self.resolved.ins_ref.opcode))?;
|
||||
ins.write_params(self)
|
||||
}
|
||||
|
||||
fn write_space(&mut self) -> core::fmt::Result {
|
||||
if self.skip_leading_space {
|
||||
self.skip_leading_space = false;
|
||||
Ok(())
|
||||
} else {
|
||||
self.write_str(" ")
|
||||
}
|
||||
}
|
||||
|
||||
fn write_separator(&mut self) -> core::fmt::Result { self.write(InstructionPart::separator()) }
|
||||
|
||||
fn write_uimm(&mut self, uimm: u32) -> core::fmt::Result {
|
||||
if let Some(resolved) = self.resolved.relocation
|
||||
&& let RelocationFlags::Elf(elf::R_ARM_ABS32) = resolved.relocation.flags
|
||||
{
|
||||
return self.write(InstructionPart::reloc());
|
||||
}
|
||||
self.write(InstructionPart::unsigned(uimm))
|
||||
}
|
||||
|
||||
fn write_simm(&mut self, simm: i32) -> core::fmt::Result {
|
||||
self.write(InstructionPart::signed(simm))
|
||||
}
|
||||
|
||||
fn write_branch_target(&mut self, branch_target: unarm::BranchTarget) -> core::fmt::Result {
|
||||
if let Some(resolved) = self.resolved.relocation {
|
||||
match resolved.relocation.flags {
|
||||
RelocationFlags::Elf(elf::R_ARM_THM_XPC22)
|
||||
| RelocationFlags::Elf(elf::R_ARM_THM_PC22)
|
||||
| RelocationFlags::Elf(elf::R_ARM_PC24)
|
||||
| RelocationFlags::Elf(elf::R_ARM_XPC25)
|
||||
| RelocationFlags::Elf(elf::R_ARM_CALL) => {
|
||||
return self.write(InstructionPart::reloc());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
args.push(ObjInsArg::PlainText(config.separator().into()));
|
||||
}
|
||||
|
||||
if reloc_arg == Some(i) {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
} else {
|
||||
match arg {
|
||||
Argument::None => {}
|
||||
Argument::Reg(reg) => {
|
||||
if reg.deref {
|
||||
deref = true;
|
||||
args.push(ObjInsArg::PlainText("[".into()));
|
||||
}
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
reg.reg.display(display_options.reg_names).to_string().into(),
|
||||
)));
|
||||
if reg.writeback {
|
||||
if reg.deref {
|
||||
writeback = true;
|
||||
} else {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
}
|
||||
Argument::RegList(reg_list) => {
|
||||
args.push(ObjInsArg::PlainText("{".into()));
|
||||
let mut first = true;
|
||||
for i in 0..16 {
|
||||
if (reg_list.regs & (1 << i)) != 0 {
|
||||
if !first {
|
||||
args.push(ObjInsArg::PlainText(config.separator().into()));
|
||||
}
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
Register::parse(i)
|
||||
.display(display_options.reg_names)
|
||||
.to_string()
|
||||
.into(),
|
||||
)));
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
args.push(ObjInsArg::PlainText("}".into()));
|
||||
if reg_list.user_mode {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("^".to_string().into())));
|
||||
}
|
||||
}
|
||||
Argument::UImm(value) | Argument::CoOpcode(value) | Argument::SatImm(value) => {
|
||||
args.push(ObjInsArg::PlainText("#".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
|
||||
}
|
||||
Argument::SImm(value)
|
||||
| Argument::OffsetImm(OffsetImm { post_indexed: _, value }) => {
|
||||
args.push(ObjInsArg::PlainText("#".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(*value as i64)));
|
||||
}
|
||||
Argument::BranchDest(value) => {
|
||||
let dest = cur_addr.wrapping_add_signed(*value) as u64;
|
||||
args.push(ObjInsArg::BranchDest(dest));
|
||||
branch_dest = Some(dest);
|
||||
}
|
||||
Argument::CoOption(value) => {
|
||||
args.push(ObjInsArg::PlainText("{".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
|
||||
args.push(ObjInsArg::PlainText("}".into()));
|
||||
}
|
||||
Argument::CoprocNum(value) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(format!("p{}", value).into())));
|
||||
}
|
||||
Argument::ShiftImm(shift) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(shift.op.to_string().into())));
|
||||
args.push(ObjInsArg::PlainText(" #".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(shift.imm as u64)));
|
||||
}
|
||||
Argument::ShiftReg(shift) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(shift.op.to_string().into())));
|
||||
args.push(ObjInsArg::PlainText(" ".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
shift.reg.display(display_options.reg_names).to_string().into(),
|
||||
)));
|
||||
}
|
||||
Argument::OffsetReg(offset) => {
|
||||
if !offset.add {
|
||||
args.push(ObjInsArg::PlainText("-".into()));
|
||||
}
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
offset.reg.display(display_options.reg_names).to_string().into(),
|
||||
)));
|
||||
}
|
||||
Argument::CpsrMode(mode) => {
|
||||
args.push(ObjInsArg::PlainText("#".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(mode.mode as u64)));
|
||||
if mode.writeback {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
Argument::CoReg(_)
|
||||
| Argument::StatusReg(_)
|
||||
| Argument::StatusMask(_)
|
||||
| Argument::Shift(_)
|
||||
| Argument::CpsrFlags(_)
|
||||
| Argument::Endian(_) => args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
arg.display(display_options, None).to_string().into(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
self.write(InstructionPart::branch_dest(branch_target.addr))
|
||||
}
|
||||
if deref {
|
||||
args.push(ObjInsArg::PlainText("]".into()));
|
||||
if writeback {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
|
||||
fn write_reg(&mut self, reg: unarm::Reg) -> core::fmt::Result { self.write_opaque(reg) }
|
||||
|
||||
fn write_status_reg(&mut self, status_reg: unarm::StatusReg) -> core::fmt::Result {
|
||||
self.write_opaque(status_reg)
|
||||
}
|
||||
|
||||
fn write_status_fields(&mut self, status_fields: unarm::StatusFields) -> core::fmt::Result {
|
||||
self.write_opaque(status_fields)
|
||||
}
|
||||
|
||||
fn write_shift_op(&mut self, shift_op: unarm::ShiftOp) -> core::fmt::Result {
|
||||
self.write_opaque(shift_op)
|
||||
}
|
||||
|
||||
fn write_coproc(&mut self, coproc: unarm::Coproc) -> core::fmt::Result {
|
||||
self.write_opaque(coproc)
|
||||
}
|
||||
|
||||
fn write_co_reg(&mut self, co_reg: unarm::CoReg) -> core::fmt::Result {
|
||||
self.write_opaque(co_reg)
|
||||
}
|
||||
|
||||
fn write_aif_flags(&mut self, aif_flags: unarm::AifFlags) -> core::fmt::Result {
|
||||
self.write_opaque(aif_flags)
|
||||
}
|
||||
|
||||
fn write_endianness(&mut self, endianness: unarm::Endianness) -> core::fmt::Result {
|
||||
self.write_opaque(endianness)
|
||||
}
|
||||
|
||||
fn write_sreg(&mut self, sreg: unarm::Sreg) -> core::fmt::Result { self.write_opaque(sreg) }
|
||||
|
||||
fn write_dreg(&mut self, dreg: unarm::Dreg) -> core::fmt::Result { self.write_opaque(dreg) }
|
||||
|
||||
fn write_fpscr(&mut self, fpscr: unarm::Fpscr) -> core::fmt::Result { self.write_opaque(fpscr) }
|
||||
|
||||
fn write_addr_ldr_str(&mut self, addr_ldr_str: unarm::AddrLdrStr) -> core::fmt::Result {
|
||||
addr_ldr_str.write(self)?;
|
||||
if let unarm::AddrLdrStr::Pre {
|
||||
rn: unarm::Reg::Pc,
|
||||
offset: unarm::LdrStrOffset::Imm(offset),
|
||||
..
|
||||
} = addr_ldr_str
|
||||
{
|
||||
let thumb = self.resolved.ins_ref.opcode & (1 << 15) == 0;
|
||||
let pc_offset = if thumb { 4 } else { 8 };
|
||||
let pc = (self.resolved.ins_ref.address as u32 & !3) + pc_offset;
|
||||
self.write(InstructionPart::basic(" (->"))?;
|
||||
self.write(InstructionPart::branch_dest(pc.wrapping_add(offset as u32)))?;
|
||||
self.write(InstructionPart::basic(")"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Ok((args, branch_dest))
|
||||
}
|
||||
|
||||
2847
objdiff-core/src/arch/arm64.rs
Normal file
2847
objdiff-core/src/arch/arm64.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,33 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap, sync::Mutex};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use object::{
|
||||
elf, Endian, Endianness, File, FileFlags, Object, ObjectSection, ObjectSymbol, Relocation,
|
||||
RelocationFlags, RelocationTarget,
|
||||
use alloc::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
string::ToString,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use object::{Endian as _, Object as _, ObjectSection as _, ObjectSymbol as _, elf};
|
||||
use rabbitizer::{
|
||||
IsaExtension, IsaVersion, Vram, abi::Abi, operands::ValuedOperand, registers_meta::Register,
|
||||
};
|
||||
use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType};
|
||||
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
diff::{DiffObjConfig, MipsAbi, MipsInstrCategory},
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
||||
arch::{Arch, RelocationOverride, RelocationOverrideTarget},
|
||||
diff::{DiffObjConfig, DiffSide, MipsAbi, MipsInstrCategory, display::InstructionPart},
|
||||
obj::{
|
||||
InstructionArg, InstructionArgValue, InstructionRef, Relocation, RelocationFlags,
|
||||
ResolvedInstructionRef, ResolvedRelocation, Section, Symbol, SymbolFlag, SymbolFlagSet,
|
||||
},
|
||||
};
|
||||
|
||||
static RABBITIZER_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn configure_rabbitizer(abi: Abi) {
|
||||
unsafe {
|
||||
config::RabbitizerConfig_Cfg.reg_names.fpr_abi_names = abi;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ObjArchMips {
|
||||
pub endianness: Endianness,
|
||||
#[derive(Debug)]
|
||||
pub struct ArchMips {
|
||||
pub endianness: object::Endianness,
|
||||
pub abi: Abi,
|
||||
pub instr_category: InstrCategory,
|
||||
pub isa_extension: Option<IsaExtension>,
|
||||
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;
|
||||
@@ -36,28 +38,31 @@ const EF_MIPS_MACH_5900: u32 = 0x00920000;
|
||||
|
||||
const R_MIPS15_S3: u32 = 119;
|
||||
|
||||
impl ObjArchMips {
|
||||
pub fn new(object: &File) -> Result<Self> {
|
||||
let mut abi = Abi::NUMERIC;
|
||||
let mut instr_category = InstrCategory::CPU;
|
||||
impl ArchMips {
|
||||
pub fn new(object: &object::File, diff_side: DiffSide) -> Result<Self> {
|
||||
let mut abi = Abi::O32;
|
||||
let mut isa_extension = None;
|
||||
match object.flags() {
|
||||
FileFlags::None => {}
|
||||
FileFlags::Elf { e_flags, .. } => {
|
||||
object::FileFlags::None => {}
|
||||
object::FileFlags::Elf { e_flags, .. } => {
|
||||
abi = match e_flags & EF_MIPS_ABI {
|
||||
elf::EF_MIPS_ABI_O32 | elf::EF_MIPS_ABI_O64 => Abi::O32,
|
||||
elf::EF_MIPS_ABI_EABI32 | elf::EF_MIPS_ABI_EABI64 => Abi::N32,
|
||||
elf::EF_MIPS_ABI_O32 => Abi::O32,
|
||||
elf::EF_MIPS_ABI_O64 if e_flags & elf::EF_MIPS_ABI2 != 0 => Abi::N64,
|
||||
elf::EF_MIPS_ABI_O64 => Abi::O64,
|
||||
elf::EF_MIPS_ABI_EABI32 => Abi::EABI32,
|
||||
elf::EF_MIPS_ABI_EABI64 => Abi::EABI64,
|
||||
_ => {
|
||||
if e_flags & elf::EF_MIPS_ABI2 != 0 {
|
||||
Abi::N32
|
||||
} else {
|
||||
Abi::NUMERIC
|
||||
Abi::O32
|
||||
}
|
||||
}
|
||||
};
|
||||
instr_category = match e_flags & EF_MIPS_MACH {
|
||||
EF_MIPS_MACH_ALLEGREX => InstrCategory::R4000ALLEGREX,
|
||||
EF_MIPS_MACH_5900 => InstrCategory::R5900,
|
||||
_ => InstrCategory::CPU,
|
||||
isa_extension = match e_flags & EF_MIPS_MACH {
|
||||
EF_MIPS_MACH_ALLEGREX => Some(IsaExtension::R4000ALLEGREX),
|
||||
EF_MIPS_MACH_5900 => Some(IsaExtension::R5900EE),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
_ => bail!("Unsupported MIPS file flags"),
|
||||
@@ -65,241 +70,428 @@ impl ObjArchMips {
|
||||
|
||||
// Parse the ri_gp_value stored in .reginfo to be able to correctly
|
||||
// calculate R_MIPS_GPREL16 relocations later. The value is stored
|
||||
// 0x14 bytes into .reginfo (on 32 bit platforms)
|
||||
// 0x14 bytes into .reginfo (on 32-bit platforms)
|
||||
let endianness = object.endianness();
|
||||
let ri_gp_value = object
|
||||
.section_by_name(".reginfo")
|
||||
.and_then(|section| section.data().ok())
|
||||
.and_then(|data| data.get(0x14..0x18))
|
||||
.and_then(|s| s.try_into().ok())
|
||||
.map(|bytes| object.endianness().read_i32_bytes(bytes))
|
||||
.map(|bytes| endianness.read_i32_bytes(bytes))
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(Self { endianness: object.endianness(), abi, instr_category, ri_gp_value })
|
||||
// Parse all relocations to pair R_MIPS_HI16 and R_MIPS_LO16. Since the instructions only
|
||||
// have 16-bit immediate fields, the 32-bit addend is split across the two relocations.
|
||||
// R_MIPS_LO16 relocations without an immediately preceding R_MIPS_HI16 use the last seen
|
||||
// R_MIPS_HI16 addend.
|
||||
// See https://refspecs.linuxfoundation.org/elf/mipsabi.pdf pages 4-17 and 4-18
|
||||
let mut paired_relocations = Vec::with_capacity(object.sections().count() + 1);
|
||||
for obj_section in object.sections() {
|
||||
let data = obj_section.data()?;
|
||||
let mut last_hi = None;
|
||||
let mut last_hi_addend = 0;
|
||||
let mut addends = BTreeMap::new();
|
||||
for (addr, reloc) in obj_section.relocations() {
|
||||
if !reloc.has_implicit_addend() {
|
||||
continue;
|
||||
}
|
||||
match reloc.flags() {
|
||||
object::RelocationFlags::Elf { r_type: elf::R_MIPS_HI16 } => {
|
||||
let code = data[addr as usize..addr as usize + 4].try_into()?;
|
||||
let addend = ((endianness.read_u32_bytes(code) & 0x0000FFFF) << 16) as i32;
|
||||
last_hi = Some(addr);
|
||||
last_hi_addend = addend;
|
||||
}
|
||||
object::RelocationFlags::Elf { r_type: elf::R_MIPS_LO16 } => {
|
||||
let code = data[addr as usize..addr as usize + 4].try_into()?;
|
||||
let addend = (endianness.read_u32_bytes(code) & 0x0000FFFF) as i16 as i32;
|
||||
let full_addend = (last_hi_addend + addend) as i64;
|
||||
if let Some(hi_addr) = last_hi.take() {
|
||||
addends.insert(hi_addr, full_addend);
|
||||
}
|
||||
addends.insert(addr, full_addend);
|
||||
}
|
||||
_ => {
|
||||
last_hi = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let section_index = obj_section.index().0;
|
||||
if section_index >= paired_relocations.len() {
|
||||
paired_relocations.resize_with(section_index + 1, BTreeMap::new);
|
||||
}
|
||||
paired_relocations[section_index] = addends;
|
||||
}
|
||||
|
||||
let mut ignored_symbols = BTreeSet::new();
|
||||
for obj_symbol in object.symbols() {
|
||||
let Ok(name) = obj_symbol.name() else { continue };
|
||||
if let Some(prefix) = name.strip_suffix(".NON_MATCHING") {
|
||||
ignored_symbols.insert(obj_symbol.index().0);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
endianness,
|
||||
abi,
|
||||
isa_extension,
|
||||
ri_gp_value,
|
||||
paired_relocations,
|
||||
ignored_symbols,
|
||||
diff_side,
|
||||
})
|
||||
}
|
||||
|
||||
fn default_instruction_flags(&self) -> rabbitizer::InstructionFlags {
|
||||
match self.isa_extension {
|
||||
Some(extension) => rabbitizer::InstructionFlags::new_extension(extension),
|
||||
None => rabbitizer::InstructionFlags::new(IsaVersion::MIPS_III),
|
||||
}
|
||||
.with_abi(self.abi)
|
||||
}
|
||||
|
||||
fn instruction_flags(&self, diff_config: &DiffObjConfig) -> rabbitizer::InstructionFlags {
|
||||
let isa_extension = match diff_config.mips_instr_category {
|
||||
MipsInstrCategory::Auto => self.isa_extension,
|
||||
MipsInstrCategory::Cpu => None,
|
||||
MipsInstrCategory::Rsp => Some(IsaExtension::RSP),
|
||||
MipsInstrCategory::R3000gte => Some(IsaExtension::R3000GTE),
|
||||
MipsInstrCategory::R4000allegrex => Some(IsaExtension::R4000ALLEGREX),
|
||||
MipsInstrCategory::R5900 => Some(IsaExtension::R5900EE),
|
||||
};
|
||||
match isa_extension {
|
||||
Some(extension) => rabbitizer::InstructionFlags::new_extension(extension),
|
||||
None => rabbitizer::InstructionFlags::new(IsaVersion::MIPS_III),
|
||||
}
|
||||
.with_abi(match diff_config.mips_abi {
|
||||
MipsAbi::Auto => self.abi,
|
||||
MipsAbi::O32 => Abi::O32,
|
||||
MipsAbi::O64 => Abi::O64,
|
||||
MipsAbi::N32 => Abi::N32,
|
||||
MipsAbi::N64 => Abi::N64,
|
||||
MipsAbi::Eabi32 => Abi::EABI32,
|
||||
MipsAbi::Eabi64 => Abi::EABI64,
|
||||
})
|
||||
}
|
||||
|
||||
fn instruction_display_flags(
|
||||
&self,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> rabbitizer::InstructionDisplayFlags {
|
||||
rabbitizer::InstructionDisplayFlags::default()
|
||||
.with_unknown_instr_comment(false)
|
||||
.with_use_dollar(diff_config.mips_register_prefix)
|
||||
}
|
||||
|
||||
fn parse_ins_ref(
|
||||
&self,
|
||||
ins_ref: InstructionRef,
|
||||
code: &[u8],
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<rabbitizer::Instruction> {
|
||||
Ok(rabbitizer::Instruction::new(
|
||||
self.endianness.read_u32_bytes(code.try_into()?),
|
||||
Vram::new(ins_ref.address as u32),
|
||||
self.instruction_flags(diff_config),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjArch for ObjArchMips {
|
||||
fn process_code(
|
||||
impl Arch for ArchMips {
|
||||
fn scan_instructions_internal(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
_section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let _guard = RABBITIZER_MUTEX.lock().map_err(|e| anyhow!("Failed to lock mutex: {e}"))?;
|
||||
configure_rabbitizer(match config.mips_abi {
|
||||
MipsAbi::Auto => self.abi,
|
||||
MipsAbi::O32 => Abi::O32,
|
||||
MipsAbi::N32 => Abi::N32,
|
||||
MipsAbi::N64 => Abi::N64,
|
||||
});
|
||||
let instr_category = match config.mips_instr_category {
|
||||
MipsInstrCategory::Auto => self.instr_category,
|
||||
MipsInstrCategory::Cpu => InstrCategory::CPU,
|
||||
MipsInstrCategory::Rsp => InstrCategory::RSP,
|
||||
MipsInstrCategory::R3000Gte => InstrCategory::R3000GTE,
|
||||
MipsInstrCategory::R4000Allegrex => InstrCategory::R4000ALLEGREX,
|
||||
MipsInstrCategory::R5900 => InstrCategory::R5900,
|
||||
};
|
||||
|
||||
let start_address = address;
|
||||
let end_address = address + code.len() as u64;
|
||||
let ins_count = code.len() / 4;
|
||||
let mut ops = Vec::<u16>::with_capacity(ins_count);
|
||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||
let mut cur_addr = start_address as u32;
|
||||
_relocations: &[Relocation],
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<Vec<InstructionRef>> {
|
||||
let instruction_flags = self.instruction_flags(diff_config);
|
||||
let mut ops = Vec::<InstructionRef>::with_capacity(code.len() / 4);
|
||||
let mut cur_addr = address as u32;
|
||||
for chunk in code.chunks_exact(4) {
|
||||
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||
let code = self.endianness.read_u32_bytes(chunk.try_into()?);
|
||||
let instruction = Instruction::new(code, cur_addr, instr_category);
|
||||
|
||||
let formatted = instruction.disassemble(None, 0);
|
||||
let op = instruction.unique_id as u16;
|
||||
ops.push(op);
|
||||
|
||||
let mnemonic = instruction.opcode_name().to_string();
|
||||
let is_branch = instruction.is_branch();
|
||||
let branch_offset = instruction.branch_offset();
|
||||
let mut branch_dest = if is_branch {
|
||||
cur_addr.checked_add_signed(branch_offset).map(|a| a as u64)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let operands = instruction.get_operands_slice();
|
||||
let mut args = Vec::with_capacity(operands.len() + 1);
|
||||
for (idx, op) in operands.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
args.push(ObjInsArg::PlainText(config.separator().into()));
|
||||
}
|
||||
|
||||
match op {
|
||||
OperandType::cpu_immediate
|
||||
| OperandType::cpu_label
|
||||
| OperandType::cpu_branch_target_label => {
|
||||
if let Some(reloc) = reloc {
|
||||
if matches!(&reloc.target_section, Some(s) if s == ".text")
|
||||
&& reloc.target.address > start_address
|
||||
&& reloc.target.address < end_address
|
||||
{
|
||||
args.push(ObjInsArg::BranchDest(reloc.target.address));
|
||||
} else {
|
||||
push_reloc(&mut args, reloc)?;
|
||||
branch_dest = None;
|
||||
}
|
||||
} else if let Some(branch_dest) = branch_dest {
|
||||
args.push(ObjInsArg::BranchDest(branch_dest));
|
||||
} else {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
op.disassemble(&instruction, None).into(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
OperandType::cpu_immediate_base => {
|
||||
if let Some(reloc) = reloc {
|
||||
push_reloc(&mut args, reloc)?;
|
||||
} else {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
OperandType::cpu_immediate.disassemble(&instruction, None).into(),
|
||||
)));
|
||||
}
|
||||
args.push(ObjInsArg::PlainText("(".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
OperandType::cpu_rs.disassemble(&instruction, None).into(),
|
||||
)));
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
}
|
||||
// OperandType::r5900_immediate15 => match reloc {
|
||||
// Some(reloc)
|
||||
// if reloc.flags == RelocationFlags::Elf { r_type: R_MIPS15_S3 } =>
|
||||
// {
|
||||
// push_reloc(&mut args, reloc)?;
|
||||
// }
|
||||
// _ => {
|
||||
// args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
// op.disassemble(&instruction, None).into(),
|
||||
// )));
|
||||
// }
|
||||
// },
|
||||
_ => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
op.disassemble(&instruction, None).into(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
|
||||
insts.push(ObjIns {
|
||||
address: cur_addr as u64,
|
||||
size: 4,
|
||||
op,
|
||||
mnemonic,
|
||||
args,
|
||||
reloc: reloc.cloned(),
|
||||
branch_dest,
|
||||
line,
|
||||
formatted,
|
||||
orig: None,
|
||||
});
|
||||
let instruction =
|
||||
rabbitizer::Instruction::new(code, Vram::new(cur_addr), instruction_flags);
|
||||
let opcode = instruction.opcode() as u16;
|
||||
let branch_dest = instruction.get_branch_vram_generic().map(|v| v.inner() as u64);
|
||||
ops.push(InstructionRef { address: cur_addr as u64, size: 4, opcode, branch_dest });
|
||||
cur_addr += 4;
|
||||
}
|
||||
Ok(ProcessCodeResult { ops, insts })
|
||||
Ok(ops)
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
fn display_instruction(
|
||||
&self,
|
||||
file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> Result<i64> {
|
||||
let data = section.data[address as usize..address as usize + 4].try_into()?;
|
||||
let addend = self.endianness.read_u32_bytes(data);
|
||||
Ok(match reloc.flags() {
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_32 } => addend as i64,
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_26 } => ((addend & 0x03FFFFFF) << 2) as i64,
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_HI16 } => {
|
||||
((addend & 0x0000FFFF) << 16) as i32 as i64
|
||||
}
|
||||
RelocationFlags::Elf {
|
||||
r_type: elf::R_MIPS_LO16 | elf::R_MIPS_GOT16 | elf::R_MIPS_CALL16,
|
||||
} => (addend & 0x0000FFFF) as i16 as i64,
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_GPREL16 | elf::R_MIPS_LITERAL } => {
|
||||
let RelocationTarget::Symbol(idx) = reloc.target() else {
|
||||
bail!("Unsupported R_MIPS_GPREL16 relocation against a non-symbol");
|
||||
};
|
||||
let sym = file.symbol_by_index(idx)?;
|
||||
resolved: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let instruction = self.parse_ins_ref(resolved.ins_ref, resolved.code, diff_config)?;
|
||||
let display_flags = self.instruction_display_flags(diff_config);
|
||||
let opcode = instruction.opcode();
|
||||
cb(InstructionPart::opcode(opcode.name(), opcode as u16))?;
|
||||
push_args(&instruction, resolved.relocation, &display_flags, cb)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// if the symbol we are relocating against is in a local section we need to add
|
||||
// the ri_gp_value from .reginfo to the addend.
|
||||
if sym.section().index().is_some() {
|
||||
((addend & 0x0000FFFF) as i16 as i64) + self.ri_gp_value as i64
|
||||
fn relocation_override(
|
||||
&self,
|
||||
file: &object::File<'_>,
|
||||
section: &object::Section,
|
||||
address: u64,
|
||||
relocation: &object::Relocation,
|
||||
) -> Result<Option<RelocationOverride>> {
|
||||
match relocation.flags() {
|
||||
// Handle ELF implicit relocations
|
||||
object::RelocationFlags::Elf { r_type } => {
|
||||
if relocation.has_implicit_addend() {
|
||||
// Check for paired R_MIPS_HI16 and R_MIPS_LO16 relocations.
|
||||
if let elf::R_MIPS_HI16 | elf::R_MIPS_LO16 = r_type
|
||||
&& let Some(addend) = self
|
||||
.paired_relocations
|
||||
.get(section.index().0)
|
||||
.and_then(|m| m.get(&address).copied())
|
||||
{
|
||||
return Ok(Some(RelocationOverride {
|
||||
target: RelocationOverrideTarget::Keep,
|
||||
addend,
|
||||
}));
|
||||
}
|
||||
|
||||
let data = section.data()?;
|
||||
let code = self
|
||||
.endianness
|
||||
.read_u32_bytes(data[address as usize..address as usize + 4].try_into()?);
|
||||
let addend = match r_type {
|
||||
elf::R_MIPS_32 => code as i64,
|
||||
elf::R_MIPS_26 => ((code & 0x03FFFFFF) << 2) as i64,
|
||||
elf::R_MIPS_HI16 => ((code & 0x0000FFFF) << 16) as i32 as i64,
|
||||
elf::R_MIPS_LO16 | elf::R_MIPS_GOT16 | elf::R_MIPS_CALL16 => {
|
||||
(code & 0x0000FFFF) as i16 as i64
|
||||
}
|
||||
elf::R_MIPS_GPREL16 | elf::R_MIPS_LITERAL => {
|
||||
let object::RelocationTarget::Symbol(idx) = relocation.target() else {
|
||||
bail!("Unsupported R_MIPS_GPREL16 relocation against a non-symbol");
|
||||
};
|
||||
let sym = file.symbol_by_index(idx)?;
|
||||
|
||||
// if the symbol we are relocating against is in a local section we need to add
|
||||
// the ri_gp_value from .reginfo to the addend.
|
||||
if sym.section().index().is_some() {
|
||||
((code & 0x0000FFFF) as i16 as i64) + self.ri_gp_value as i64
|
||||
} else {
|
||||
(code & 0x0000FFFF) as i16 as i64
|
||||
}
|
||||
}
|
||||
elf::R_MIPS_PC16 => 0, // PC-relative relocation
|
||||
R_MIPS15_S3 => ((code & 0x001FFFC0) >> 3) as i64,
|
||||
elf::R_MIPS_GPREL32 => (code as i32 as i64) + self.ri_gp_value as i64,
|
||||
flags => bail!("Unsupported MIPS implicit relocation {flags:?}"),
|
||||
};
|
||||
Ok(Some(RelocationOverride { target: RelocationOverrideTarget::Keep, addend }))
|
||||
} else {
|
||||
(addend & 0x0000FFFF) as i16 as i64
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_PC16 } => 0, // PC-relative relocation
|
||||
RelocationFlags::Elf { r_type: R_MIPS15_S3 } => ((addend & 0x001FFFC0) >> 3) as i64,
|
||||
flags => bail!("Unsupported MIPS implicit relocation {flags:?}"),
|
||||
})
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf { r_type } => match r_type {
|
||||
elf::R_MIPS_32 => Cow::Borrowed("R_MIPS_32"),
|
||||
elf::R_MIPS_26 => Cow::Borrowed("R_MIPS_26"),
|
||||
elf::R_MIPS_HI16 => Cow::Borrowed("R_MIPS_HI16"),
|
||||
elf::R_MIPS_LO16 => Cow::Borrowed("R_MIPS_LO16"),
|
||||
elf::R_MIPS_GPREL16 => Cow::Borrowed("R_MIPS_GPREL16"),
|
||||
elf::R_MIPS_LITERAL => Cow::Borrowed("R_MIPS_LITERAL"),
|
||||
elf::R_MIPS_GOT16 => Cow::Borrowed("R_MIPS_GOT16"),
|
||||
elf::R_MIPS_PC16 => Cow::Borrowed("R_MIPS_PC16"),
|
||||
elf::R_MIPS_CALL16 => Cow::Borrowed("R_MIPS_CALL16"),
|
||||
R_MIPS15_S3 => Cow::Borrowed("R_MIPS15_S3"),
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_MIPS_NONE => Some("R_MIPS_NONE"),
|
||||
elf::R_MIPS_16 => Some("R_MIPS_16"),
|
||||
elf::R_MIPS_32 => Some("R_MIPS_32"),
|
||||
elf::R_MIPS_26 => Some("R_MIPS_26"),
|
||||
elf::R_MIPS_HI16 => Some("R_MIPS_HI16"),
|
||||
elf::R_MIPS_LO16 => Some("R_MIPS_LO16"),
|
||||
elf::R_MIPS_GPREL16 => Some("R_MIPS_GPREL16"),
|
||||
elf::R_MIPS_LITERAL => Some("R_MIPS_LITERAL"),
|
||||
elf::R_MIPS_GOT16 => Some("R_MIPS_GOT16"),
|
||||
elf::R_MIPS_PC16 => Some("R_MIPS_PC16"),
|
||||
elf::R_MIPS_CALL16 => Some("R_MIPS_CALL16"),
|
||||
elf::R_MIPS_GPREL32 => Some("R_MIPS_GPREL32"),
|
||||
R_MIPS15_S3 => Some("R_MIPS15_S3"),
|
||||
_ => None,
|
||||
},
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn data_reloc_size(&self, flags: RelocationFlags) -> usize {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_MIPS_16 => 2,
|
||||
elf::R_MIPS_32 => 4,
|
||||
_ => 1,
|
||||
},
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn extra_symbol_flags(&self, symbol: &object::Symbol) -> SymbolFlagSet {
|
||||
let mut flags = SymbolFlagSet::default();
|
||||
if self.ignored_symbols.contains(&symbol.index().0) {
|
||||
flags |= SymbolFlag::Ignored;
|
||||
}
|
||||
flags
|
||||
}
|
||||
|
||||
fn infer_function_size(
|
||||
&self,
|
||||
symbol: &Symbol,
|
||||
section: &Section,
|
||||
next_address: u64,
|
||||
) -> Result<u64> {
|
||||
// Trim any trailing 4-byte zeroes from the end (nops)
|
||||
let mut new_address = next_address;
|
||||
while new_address >= symbol.address + 4
|
||||
&& let Some(data) = section.data_range(new_address - 4, 4)
|
||||
&& data == [0u8; 4]
|
||||
&& section.relocation_at(next_address - 4, 4).is_none()
|
||||
{
|
||||
new_address -= 4;
|
||||
}
|
||||
// Check if the last instruction has a delay slot, if so, include the delay slot nop
|
||||
if new_address + 4 <= next_address
|
||||
&& new_address >= symbol.address + 4
|
||||
&& let Some(data) = section.data_range(new_address - 4, 4)
|
||||
&& let instruction = rabbitizer::Instruction::new(
|
||||
self.endianness.read_u32_bytes(data.try_into().unwrap()),
|
||||
Vram::new((new_address - 4) as u32),
|
||||
self.default_instruction_flags(),
|
||||
)
|
||||
&& instruction.opcode().has_delay_slot()
|
||||
{
|
||||
new_address += 4;
|
||||
}
|
||||
Ok(new_address.saturating_sub(symbol.address))
|
||||
}
|
||||
}
|
||||
|
||||
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
|
||||
fn push_args(
|
||||
instruction: &rabbitizer::Instruction,
|
||||
relocation: Option<ResolvedRelocation>,
|
||||
display_flags: &rabbitizer::InstructionDisplayFlags,
|
||||
mut arg_cb: impl FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let operands = instruction.valued_operands_iter();
|
||||
for (idx, op) in operands.enumerate() {
|
||||
if idx > 0 {
|
||||
arg_cb(InstructionPart::separator())?;
|
||||
}
|
||||
|
||||
match op {
|
||||
ValuedOperand::core_imm_i16(imm) => {
|
||||
if let Some(resolved) = relocation {
|
||||
push_reloc(resolved.relocation, &mut arg_cb)?;
|
||||
} else {
|
||||
arg_cb(InstructionPart::signed(imm))?;
|
||||
}
|
||||
}
|
||||
ValuedOperand::core_imm_u16(imm) => {
|
||||
if let Some(resolved) = relocation {
|
||||
push_reloc(resolved.relocation, &mut arg_cb)?;
|
||||
} else {
|
||||
arg_cb(InstructionPart::unsigned(imm))?;
|
||||
}
|
||||
}
|
||||
ValuedOperand::core_label(..) | ValuedOperand::core_branch_target_label(..) => {
|
||||
if let Some(resolved) = relocation {
|
||||
push_reloc(resolved.relocation, &mut arg_cb)?;
|
||||
} else if let Some(branch_dest) = instruction
|
||||
.get_branch_offset_generic()
|
||||
.map(|o| (instruction.vram() + o).inner() as u64)
|
||||
{
|
||||
arg_cb(InstructionPart::branch_dest(branch_dest))?;
|
||||
} else {
|
||||
arg_cb(InstructionPart::opaque(
|
||||
op.display(instruction, display_flags, None::<&str>).to_string(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
ValuedOperand::core_imm_rs(imm, base) => {
|
||||
if let Some(resolved) = relocation {
|
||||
push_reloc(resolved.relocation, &mut arg_cb)?;
|
||||
} else {
|
||||
arg_cb(InstructionPart::Arg(InstructionArg::Value(
|
||||
InstructionArgValue::Signed(imm as i64),
|
||||
)))?;
|
||||
}
|
||||
arg_cb(InstructionPart::basic("("))?;
|
||||
arg_cb(InstructionPart::opaque(base.either_name(
|
||||
instruction.flags().abi(),
|
||||
display_flags.named_gpr(),
|
||||
!display_flags.use_dollar(),
|
||||
)))?;
|
||||
arg_cb(InstructionPart::basic(")"))?;
|
||||
}
|
||||
// ValuedOperand::r5900_immediate15(..) => match relocation {
|
||||
// Some(resolved)
|
||||
// if resolved.relocation.flags == RelocationFlags::Elf(R_MIPS15_S3) =>
|
||||
// {
|
||||
// push_reloc(&resolved.relocation, &mut arg_cb)?;
|
||||
// }
|
||||
// _ => {
|
||||
// arg_cb(InstructionPart::opaque(op.disassemble(&instruction, None)))?;
|
||||
// }
|
||||
// },
|
||||
_ => {
|
||||
arg_cb(InstructionPart::opaque(
|
||||
op.display(instruction, display_flags, None::<&str>).to_string(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_reloc(
|
||||
reloc: &Relocation,
|
||||
mut arg_cb: impl FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
match reloc.flags {
|
||||
RelocationFlags::Elf { r_type } => match r_type {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_MIPS_HI16 => {
|
||||
args.push(ObjInsArg::PlainText("%hi(".into()));
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
arg_cb(InstructionPart::basic("%hi("))?;
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
arg_cb(InstructionPart::basic(")"))?;
|
||||
}
|
||||
elf::R_MIPS_LO16 => {
|
||||
args.push(ObjInsArg::PlainText("%lo(".into()));
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
arg_cb(InstructionPart::basic("%lo("))?;
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
arg_cb(InstructionPart::basic(")"))?;
|
||||
}
|
||||
elf::R_MIPS_GOT16 => {
|
||||
args.push(ObjInsArg::PlainText("%got(".into()));
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
arg_cb(InstructionPart::basic("%got("))?;
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
arg_cb(InstructionPart::basic(")"))?;
|
||||
}
|
||||
elf::R_MIPS_CALL16 => {
|
||||
args.push(ObjInsArg::PlainText("%call16(".into()));
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
arg_cb(InstructionPart::basic("%call16("))?;
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
arg_cb(InstructionPart::basic(")"))?;
|
||||
}
|
||||
elf::R_MIPS_GPREL16 => {
|
||||
args.push(ObjInsArg::PlainText("%gp_rel(".into()));
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
arg_cb(InstructionPart::basic("%gp_rel("))?;
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
arg_cb(InstructionPart::basic(")"))?;
|
||||
}
|
||||
elf::R_MIPS_32
|
||||
| elf::R_MIPS_26
|
||||
| elf::R_MIPS_LITERAL
|
||||
| elf::R_MIPS_PC16
|
||||
| R_MIPS15_S3 => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
arg_cb(InstructionPart::reloc())?;
|
||||
}
|
||||
_ => bail!("Unsupported ELF MIPS relocation type {r_type}"),
|
||||
},
|
||||
|
||||
@@ -1,63 +1,491 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
use alloc::{
|
||||
borrow::Cow,
|
||||
boxed::Box,
|
||||
format,
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
};
|
||||
use core::{
|
||||
any::Any,
|
||||
ffi::CStr,
|
||||
fmt::{self, Debug},
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
|
||||
use anyhow::{Result, bail};
|
||||
use encoding_rs::SHIFT_JIS;
|
||||
use object::Endian as _;
|
||||
|
||||
use crate::{
|
||||
diff::DiffObjConfig,
|
||||
obj::{ObjIns, ObjReloc, ObjSection},
|
||||
diff::{
|
||||
DiffObjConfig, DiffSide,
|
||||
display::{ContextItem, HoverItem, InstructionPart},
|
||||
},
|
||||
obj::{
|
||||
FlowAnalysisResult, InstructionArg, InstructionRef, Object, ParsedInstruction, Relocation,
|
||||
RelocationFlags, ResolvedInstructionRef, ResolvedSymbol, Section, Symbol, SymbolFlagSet,
|
||||
SymbolKind,
|
||||
},
|
||||
util::ReallySigned,
|
||||
};
|
||||
|
||||
#[cfg(feature = "arm")]
|
||||
mod arm;
|
||||
pub mod arm;
|
||||
#[cfg(feature = "arm64")]
|
||||
pub mod arm64;
|
||||
#[cfg(feature = "mips")]
|
||||
pub mod mips;
|
||||
#[cfg(feature = "ppc")]
|
||||
pub mod ppc;
|
||||
#[cfg(feature = "superh")]
|
||||
pub mod superh;
|
||||
#[cfg(feature = "x86")]
|
||||
pub mod x86;
|
||||
|
||||
pub trait ObjArch: Send + Sync {
|
||||
fn process_code(
|
||||
pub const OPCODE_INVALID: u16 = u16::MAX;
|
||||
pub const OPCODE_DATA: u16 = u16::MAX - 1;
|
||||
|
||||
/// Represents the type of data associated with an instruction
|
||||
#[derive(PartialEq)]
|
||||
pub enum DataType {
|
||||
Int8,
|
||||
Int16,
|
||||
Int32,
|
||||
Int64,
|
||||
Float,
|
||||
Double,
|
||||
Bytes,
|
||||
String,
|
||||
}
|
||||
|
||||
impl fmt::Display for DataType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
DataType::Int8 => "Int8",
|
||||
DataType::Int16 => "Int16",
|
||||
DataType::Int32 => "Int32",
|
||||
DataType::Int64 => "Int64",
|
||||
DataType::Float => "Float",
|
||||
DataType::Double => "Double",
|
||||
DataType::Bytes => "Bytes",
|
||||
DataType::String => "String",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DataType {
|
||||
pub fn display_labels(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<String> {
|
||||
let mut strs = Vec::new();
|
||||
for (literal, label_override) in self.display_literals(endian, bytes) {
|
||||
let label = label_override.unwrap_or_else(|| self.to_string());
|
||||
strs.push(format!("{label}: {literal}"))
|
||||
}
|
||||
strs
|
||||
}
|
||||
|
||||
pub fn display_literals(
|
||||
&self,
|
||||
endian: object::Endianness,
|
||||
bytes: &[u8],
|
||||
) -> Vec<(String, Option<String>)> {
|
||||
let mut strs = Vec::new();
|
||||
if self.required_len().is_some_and(|l| bytes.len() < l) {
|
||||
log::warn!(
|
||||
"Failed to display a symbol value for a symbol whose size is too small for instruction referencing it."
|
||||
);
|
||||
return strs;
|
||||
}
|
||||
let mut bytes = bytes;
|
||||
if self.required_len().is_some_and(|l| bytes.len() > l) {
|
||||
// If the symbol's size is larger a single instance of this data type, we take just the
|
||||
// bytes necessary for one of them in order to display the first element of the array.
|
||||
bytes = &bytes[0..self.required_len().unwrap()];
|
||||
// TODO: Attempt to interpret large symbols as arrays of a smaller type and show all
|
||||
// elements of the array instead. https://github.com/encounter/objdiff/issues/124
|
||||
// However, note that the stride of an array can not always be determined just by the
|
||||
// data type guessed by the single instruction accessing it. There can also be arrays of
|
||||
// structs that contain multiple elements of different types, so if other elements after
|
||||
// the first one were to be displayed in this manner, they may be inaccurate.
|
||||
}
|
||||
|
||||
match self {
|
||||
DataType::Int8 => {
|
||||
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
|
||||
strs.push((format!("{i:#x}"), None));
|
||||
|
||||
if i < 0 {
|
||||
strs.push((format!("{:#x}", ReallySigned(i)), None));
|
||||
}
|
||||
}
|
||||
DataType::Int16 => {
|
||||
let i = endian.read_i16_bytes(bytes.try_into().unwrap());
|
||||
strs.push((format!("{i:#x}"), None));
|
||||
|
||||
if i < 0 {
|
||||
strs.push((format!("{:#x}", ReallySigned(i)), None));
|
||||
}
|
||||
}
|
||||
DataType::Int32 => {
|
||||
let i = endian.read_i32_bytes(bytes.try_into().unwrap());
|
||||
strs.push((format!("{i:#x}"), None));
|
||||
|
||||
if i < 0 {
|
||||
strs.push((format!("{:#x}", ReallySigned(i)), None));
|
||||
}
|
||||
}
|
||||
DataType::Int64 => {
|
||||
let i = endian.read_i64_bytes(bytes.try_into().unwrap());
|
||||
strs.push((format!("{i:#x}"), None));
|
||||
|
||||
if i < 0 {
|
||||
strs.push((format!("{:#x}", ReallySigned(i)), None));
|
||||
}
|
||||
}
|
||||
DataType::Float => {
|
||||
let bytes: [u8; 4] = bytes.try_into().unwrap();
|
||||
strs.push((
|
||||
format!("{:?}f", match endian {
|
||||
object::Endianness::Little => f32::from_le_bytes(bytes),
|
||||
object::Endianness::Big => f32::from_be_bytes(bytes),
|
||||
}),
|
||||
None,
|
||||
));
|
||||
}
|
||||
DataType::Double => {
|
||||
let bytes: [u8; 8] = bytes.try_into().unwrap();
|
||||
strs.push((
|
||||
format!("{:?}", match endian {
|
||||
object::Endianness::Little => f64::from_le_bytes(bytes),
|
||||
object::Endianness::Big => f64::from_be_bytes(bytes),
|
||||
}),
|
||||
None,
|
||||
));
|
||||
}
|
||||
DataType::Bytes => {
|
||||
strs.push((format!("{bytes:#?}"), None));
|
||||
}
|
||||
DataType::String => {
|
||||
if let Ok(cstr) = CStr::from_bytes_until_nul(bytes) {
|
||||
strs.push((format!("{cstr:?}"), None));
|
||||
}
|
||||
if let Some(nul_idx) = bytes.iter().position(|&c| c == b'\0') {
|
||||
let (cow, _, had_errors) = SHIFT_JIS.decode(&bytes[..nul_idx]);
|
||||
if !had_errors {
|
||||
let str = format!("{cow:?}");
|
||||
// Only add the Shift JIS string if it's different from the ASCII string.
|
||||
if !strs.iter().any(|x| x.0 == str) {
|
||||
strs.push((str, Some("Shift JIS".into())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
strs
|
||||
}
|
||||
|
||||
fn required_len(&self) -> Option<usize> {
|
||||
match self {
|
||||
DataType::Int8 => Some(1),
|
||||
DataType::Int16 => Some(2),
|
||||
DataType::Int32 => Some(4),
|
||||
DataType::Int64 => Some(8),
|
||||
DataType::Float => Some(4),
|
||||
DataType::Double => Some(8),
|
||||
DataType::Bytes => None,
|
||||
DataType::String => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn Arch {
|
||||
/// Generate a list of instructions references (offset, size, opcode) from the given code.
|
||||
///
|
||||
/// See [`scan_instructions_internal`] for more details.
|
||||
pub fn scan_instructions(
|
||||
&self,
|
||||
resolved: ResolvedSymbol,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<Vec<InstructionRef>> {
|
||||
let mut result = self.scan_instructions_internal(
|
||||
resolved.symbol.address,
|
||||
resolved.data,
|
||||
resolved.section_index,
|
||||
&resolved.section.relocations,
|
||||
diff_config,
|
||||
)?;
|
||||
|
||||
let function_start = resolved.symbol.address;
|
||||
let function_end = function_start + resolved.symbol.size;
|
||||
|
||||
// Remove any branch destinations that are outside the function range
|
||||
for ins in result.iter_mut() {
|
||||
if let Some(branch_dest) = ins.branch_dest
|
||||
&& (branch_dest < function_start || branch_dest >= function_end)
|
||||
{
|
||||
ins.branch_dest = None;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve relocation targets within the same function to branch destinations
|
||||
let mut ins_iter = result.iter_mut().peekable();
|
||||
'outer: for reloc in resolved
|
||||
.section
|
||||
.relocations
|
||||
.iter()
|
||||
.skip_while(|r| r.address < function_start)
|
||||
.take_while(|r| r.address < function_end)
|
||||
{
|
||||
let ins = loop {
|
||||
let Some(ins) = ins_iter.peek_mut() else {
|
||||
break 'outer;
|
||||
};
|
||||
if reloc.address < ins.address {
|
||||
continue 'outer;
|
||||
}
|
||||
let ins = ins_iter.next().unwrap();
|
||||
if reloc.address >= ins.address && reloc.address < ins.address + ins.size as u64 {
|
||||
break ins;
|
||||
}
|
||||
};
|
||||
// Clear existing branch destination for instructions with relocations
|
||||
ins.branch_dest = None;
|
||||
let Some(target) = resolved.obj.symbols.get(reloc.target_symbol) else {
|
||||
continue;
|
||||
};
|
||||
if target.section != Some(resolved.section_index) {
|
||||
continue;
|
||||
}
|
||||
let Some(target_address) = target.address.checked_add_signed(reloc.addend) else {
|
||||
continue;
|
||||
};
|
||||
// If the target address is within the function range, set it as a branch destination
|
||||
if target_address >= function_start && target_address < function_end {
|
||||
ins.branch_dest = Some(target_address);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Parse an instruction to gather its mnemonic and arguments for more detailed comparison.
|
||||
///
|
||||
/// This is called only when we need to compare the arguments of an instruction.
|
||||
pub fn process_instruction(
|
||||
&self,
|
||||
resolved: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<ParsedInstruction> {
|
||||
let mut mnemonic = None;
|
||||
let mut args = Vec::with_capacity(8);
|
||||
let mut relocation_emitted = false;
|
||||
self.display_instruction(resolved, diff_config, &mut |part| {
|
||||
match part {
|
||||
InstructionPart::Opcode(m, _) => mnemonic = Some(Cow::Owned(m.into_owned())),
|
||||
InstructionPart::Arg(arg) => {
|
||||
if arg == InstructionArg::Reloc {
|
||||
relocation_emitted = true;
|
||||
// If the relocation was resolved to a branch destination, emit that instead.
|
||||
if let Some(dest) = resolved.ins_ref.branch_dest {
|
||||
args.push(InstructionArg::BranchDest(dest));
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
args.push(arg.into_static());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
// If the instruction has a relocation, but we didn't format it in the display, add it to
|
||||
// the end of the arguments list.
|
||||
if resolved.relocation.is_some() && !relocation_emitted {
|
||||
args.push(InstructionArg::Reloc);
|
||||
}
|
||||
Ok(ParsedInstruction {
|
||||
ins_ref: resolved.ins_ref,
|
||||
mnemonic: mnemonic.unwrap_or_default(),
|
||||
args,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Arch: Any + Debug + Send + Sync {
|
||||
/// Finishes arch-specific initialization that must be done after sections have been combined.
|
||||
fn post_init(&mut self, _sections: &[Section], _symbols: &[Symbol]) {}
|
||||
|
||||
/// Generate a list of instructions references (offset, size, opcode) from the given code.
|
||||
///
|
||||
/// The opcode IDs are used to generate the initial diff. Implementations should do as little
|
||||
/// parsing as possible here: just enough to identify the base instruction opcode, size, and
|
||||
/// possible branch destination (for visual representation). As needed, instructions are parsed
|
||||
/// via `process_instruction` to compare their arguments.
|
||||
fn scan_instructions_internal(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult>;
|
||||
relocations: &[Relocation],
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<Vec<InstructionRef>>;
|
||||
|
||||
fn implcit_addend(
|
||||
/// Format an instruction for display.
|
||||
///
|
||||
/// Implementations should call the callback for each part of the instruction: usually the
|
||||
/// mnemonic and arguments, plus any separators and visual formatting.
|
||||
fn display_instruction(
|
||||
&self,
|
||||
file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> Result<i64>;
|
||||
resolved: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()>;
|
||||
|
||||
fn demangle(&self, _name: &str) -> Option<String> { None }
|
||||
/// Generate a list of fake relocations from the given code that represent pooled data accesses.
|
||||
fn generate_pooled_relocations(
|
||||
&self,
|
||||
_address: u64,
|
||||
_code: &[u8],
|
||||
_relocations: &[Relocation],
|
||||
_symbols: &[Symbol],
|
||||
) -> Vec<Relocation> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
|
||||
// Perform detailed data flow analysis
|
||||
fn data_flow_analysis(
|
||||
&self,
|
||||
_obj: &Object,
|
||||
_symbol: &Symbol,
|
||||
_code: &[u8],
|
||||
_relocations: &[Relocation],
|
||||
) -> Option<Box<dyn FlowAnalysisResult>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
|
||||
fn relocation_override(
|
||||
&self,
|
||||
_file: &object::File<'_>,
|
||||
_section: &object::Section,
|
||||
_address: u64,
|
||||
_relocation: &object::Relocation,
|
||||
) -> Result<Option<RelocationOverride>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn reloc_name(&self, _flags: RelocationFlags) -> Option<&'static str> { None }
|
||||
|
||||
fn data_reloc_size(&self, flags: RelocationFlags) -> usize;
|
||||
|
||||
fn symbol_address(&self, address: u64, _kind: SymbolKind) -> u64 { address }
|
||||
|
||||
fn extra_symbol_flags(&self, _symbol: &object::Symbol) -> SymbolFlagSet {
|
||||
SymbolFlagSet::default()
|
||||
}
|
||||
|
||||
fn guess_data_type(
|
||||
&self,
|
||||
_resolved: ResolvedInstructionRef,
|
||||
_bytes: &[u8],
|
||||
) -> Option<DataType> {
|
||||
None
|
||||
}
|
||||
|
||||
fn symbol_hover(&self, _obj: &Object, _symbol_index: usize) -> Vec<HoverItem> { Vec::new() }
|
||||
|
||||
fn symbol_context(&self, _obj: &Object, _symbol_index: usize) -> Vec<ContextItem> { Vec::new() }
|
||||
|
||||
fn instruction_hover(
|
||||
&self,
|
||||
_obj: &Object,
|
||||
_resolved: ResolvedInstructionRef,
|
||||
) -> Vec<HoverItem> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn instruction_context(
|
||||
&self,
|
||||
_obj: &Object,
|
||||
_resolved: ResolvedInstructionRef,
|
||||
) -> Vec<ContextItem> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn infer_function_size(
|
||||
&self,
|
||||
symbol: &Symbol,
|
||||
_section: &Section,
|
||||
next_address: u64,
|
||||
) -> Result<u64> {
|
||||
Ok(next_address.saturating_sub(symbol.address))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProcessCodeResult {
|
||||
pub ops: Vec<u16>,
|
||||
pub insts: Vec<ObjIns>,
|
||||
}
|
||||
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;
|
||||
|
||||
pub fn new_arch(object: &object::File) -> Result<Box<dyn ObjArch>> {
|
||||
Ok(match object.architecture() {
|
||||
#[cfg(feature = "ppc")]
|
||||
Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?),
|
||||
object::Architecture::PowerPc | object::Architecture::PowerPc64 => {
|
||||
Box::new(ppc::ArchPpc::new(object)?)
|
||||
}
|
||||
#[cfg(feature = "mips")]
|
||||
Architecture::Mips => Box::new(mips::ObjArchMips::new(object)?),
|
||||
object::Architecture::Mips => Box::new(mips::ArchMips::new(object, diff_side)?),
|
||||
#[cfg(feature = "x86")]
|
||||
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
|
||||
object::Architecture::I386 | object::Architecture::X86_64 => {
|
||||
Box::new(x86::ArchX86::new(object)?)
|
||||
}
|
||||
#[cfg(feature = "arm")]
|
||||
Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?),
|
||||
object::Architecture::Arm => Box::new(arm::ArchArm::new(object)?),
|
||||
#[cfg(feature = "arm64")]
|
||||
object::Architecture::Aarch64 => Box::new(arm64::ArchArm64::new(object)?),
|
||||
#[cfg(feature = "superh")]
|
||||
object::Architecture::SuperH => Box::new(superh::ArchSuperH::new(object)?),
|
||||
arch => bail!("Unsupported architecture: {arch:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ArchDummy {}
|
||||
|
||||
impl ArchDummy {
|
||||
pub fn new() -> Box<Self> { Box::new(Self {}) }
|
||||
}
|
||||
|
||||
impl Arch for ArchDummy {
|
||||
fn scan_instructions_internal(
|
||||
&self,
|
||||
_address: u64,
|
||||
_code: &[u8],
|
||||
_section_index: usize,
|
||||
_relocations: &[Relocation],
|
||||
_diff_config: &DiffObjConfig,
|
||||
) -> Result<Vec<InstructionRef>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn display_instruction(
|
||||
&self,
|
||||
_resolved: ResolvedInstructionRef,
|
||||
_diff_config: &DiffObjConfig,
|
||||
_cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data_reloc_size(&self, _flags: RelocationFlags) -> usize { 0 }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RelocationOverrideTarget {
|
||||
Keep,
|
||||
Skip,
|
||||
Symbol(object::SymbolIndex),
|
||||
Section(object::SectionIndex),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RelocationOverride {
|
||||
pub target: RelocationOverrideTarget,
|
||||
pub addend: i64,
|
||||
}
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use object::{elf, File, Relocation, RelocationFlags};
|
||||
use ppc750cl::{Argument, InsIter, GPR};
|
||||
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
diff::DiffObjConfig,
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
||||
};
|
||||
|
||||
// Relative relocation, can be Simm, Offset or BranchDest
|
||||
fn is_relative_arg(arg: &Argument) -> bool {
|
||||
matches!(arg, Argument::Simm(_) | Argument::Offset(_) | Argument::BranchDest(_))
|
||||
}
|
||||
|
||||
// Relative or absolute relocation, can be Uimm, Simm or Offset
|
||||
fn is_rel_abs_arg(arg: &Argument) -> bool {
|
||||
matches!(arg, Argument::Uimm(_) | Argument::Simm(_) | Argument::Offset(_))
|
||||
}
|
||||
|
||||
fn is_offset_arg(arg: &Argument) -> bool { matches!(arg, Argument::Offset(_)) }
|
||||
|
||||
pub struct ObjArchPpc {}
|
||||
|
||||
impl ObjArchPpc {
|
||||
pub fn new(_file: &File) -> Result<Self> { Ok(Self {}) }
|
||||
}
|
||||
|
||||
impl ObjArch for ObjArchPpc {
|
||||
fn process_code(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
_section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let ins_count = code.len() / 4;
|
||||
let mut ops = Vec::<u16>::with_capacity(ins_count);
|
||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||
for (cur_addr, mut ins) in InsIter::new(code, address as u32) {
|
||||
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||
if let Some(reloc) = reloc {
|
||||
// Zero out relocations
|
||||
ins.code = match reloc.flags {
|
||||
RelocationFlags::Elf { r_type: elf::R_PPC_EMB_SDA21 } => ins.code & !0x1FFFFF,
|
||||
RelocationFlags::Elf { r_type: elf::R_PPC_REL24 } => ins.code & !0x3FFFFFC,
|
||||
RelocationFlags::Elf { r_type: elf::R_PPC_REL14 } => ins.code & !0xFFFC,
|
||||
RelocationFlags::Elf {
|
||||
r_type: elf::R_PPC_ADDR16_HI | elf::R_PPC_ADDR16_HA | elf::R_PPC_ADDR16_LO,
|
||||
} => ins.code & !0xFFFF,
|
||||
_ => ins.code,
|
||||
};
|
||||
}
|
||||
|
||||
let orig = ins.basic().to_string();
|
||||
let simplified = ins.simplified();
|
||||
let formatted = simplified.to_string();
|
||||
|
||||
let mut reloc_arg = None;
|
||||
if let Some(reloc) = reloc {
|
||||
match reloc.flags {
|
||||
RelocationFlags::Elf { r_type: elf::R_PPC_EMB_SDA21 } => {
|
||||
reloc_arg = Some(1);
|
||||
}
|
||||
RelocationFlags::Elf { r_type: elf::R_PPC_REL24 | elf::R_PPC_REL14 } => {
|
||||
reloc_arg = simplified.args.iter().rposition(is_relative_arg);
|
||||
}
|
||||
RelocationFlags::Elf {
|
||||
r_type: elf::R_PPC_ADDR16_HI | elf::R_PPC_ADDR16_HA | elf::R_PPC_ADDR16_LO,
|
||||
} => {
|
||||
reloc_arg = simplified.args.iter().rposition(is_rel_abs_arg);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut args = vec![];
|
||||
let mut branch_dest = None;
|
||||
let mut writing_offset = false;
|
||||
for (idx, arg) in simplified.args_iter().enumerate() {
|
||||
if idx > 0 && !writing_offset {
|
||||
args.push(ObjInsArg::PlainText(config.separator().into()));
|
||||
}
|
||||
|
||||
if reloc_arg == Some(idx) {
|
||||
let reloc = reloc.unwrap();
|
||||
push_reloc(&mut args, reloc)?;
|
||||
// For @sda21, we can omit the register argument
|
||||
if matches!(reloc.flags, RelocationFlags::Elf { r_type: elf::R_PPC_EMB_SDA21 })
|
||||
// Sanity check: the next argument should be r0
|
||||
&& matches!(simplified.args.get(idx + 1), Some(Argument::GPR(GPR(0))))
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
match arg {
|
||||
Argument::Simm(simm) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(simm.0 as i64)));
|
||||
}
|
||||
Argument::Uimm(uimm) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(uimm.0 as u64)));
|
||||
}
|
||||
Argument::Offset(offset) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(offset.0 as i64)));
|
||||
}
|
||||
Argument::BranchDest(dest) => {
|
||||
let dest = cur_addr.wrapping_add_signed(dest.0) as u64;
|
||||
args.push(ObjInsArg::BranchDest(dest));
|
||||
branch_dest = Some(dest);
|
||||
}
|
||||
_ => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
arg.to_string().into(),
|
||||
)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if writing_offset {
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
writing_offset = false;
|
||||
}
|
||||
if is_offset_arg(arg) {
|
||||
args.push(ObjInsArg::PlainText("(".into()));
|
||||
writing_offset = true;
|
||||
}
|
||||
}
|
||||
|
||||
ops.push(ins.op as u16);
|
||||
let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
|
||||
insts.push(ObjIns {
|
||||
address: cur_addr as u64,
|
||||
size: 4,
|
||||
mnemonic: simplified.mnemonic.to_string(),
|
||||
args,
|
||||
reloc: reloc.cloned(),
|
||||
op: ins.op as u16,
|
||||
branch_dest,
|
||||
line,
|
||||
formatted,
|
||||
orig: Some(orig),
|
||||
});
|
||||
}
|
||||
Ok(ProcessCodeResult { ops, insts })
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
_file: &File<'_>,
|
||||
_section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> Result<i64> {
|
||||
bail!("Unsupported PPC implicit relocation {:#x}:{:?}", address, reloc.flags())
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cwdemangle::demangle(name, &cwdemangle::DemangleOptions::default())
|
||||
}
|
||||
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf { r_type } => match r_type {
|
||||
elf::R_PPC_ADDR16_LO => Cow::Borrowed("R_PPC_ADDR16_LO"),
|
||||
elf::R_PPC_ADDR16_HI => Cow::Borrowed("R_PPC_ADDR16_HI"),
|
||||
elf::R_PPC_ADDR16_HA => Cow::Borrowed("R_PPC_ADDR16_HA"),
|
||||
elf::R_PPC_EMB_SDA21 => Cow::Borrowed("R_PPC_EMB_SDA21"),
|
||||
elf::R_PPC_ADDR32 => Cow::Borrowed("R_PPC_ADDR32"),
|
||||
elf::R_PPC_UADDR32 => Cow::Borrowed("R_PPC_UADDR32"),
|
||||
elf::R_PPC_REL24 => Cow::Borrowed("R_PPC_REL24"),
|
||||
elf::R_PPC_REL14 => Cow::Borrowed("R_PPC_REL14"),
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
},
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
|
||||
match reloc.flags {
|
||||
RelocationFlags::Elf { r_type } => match r_type {
|
||||
elf::R_PPC_ADDR16_LO => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText("@l".into()));
|
||||
}
|
||||
elf::R_PPC_ADDR16_HI => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText("@h".into()));
|
||||
}
|
||||
elf::R_PPC_ADDR16_HA => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText("@ha".into()));
|
||||
}
|
||||
elf::R_PPC_EMB_SDA21 => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText("@sda21".into()));
|
||||
}
|
||||
elf::R_PPC_ADDR32 | elf::R_PPC_UADDR32 | elf::R_PPC_REL24 | elf::R_PPC_REL14 => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
}
|
||||
_ => bail!("Unsupported ELF PPC relocation type {r_type}"),
|
||||
},
|
||||
flags => bail!("Unsupported PPC relocation kind: {flags:?}"),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
649
objdiff-core/src/arch/ppc/flow_analysis.rs
Normal file
649
objdiff-core/src/arch/ppc/flow_analysis.rs
Normal file
@@ -0,0 +1,649 @@
|
||||
use alloc::{
|
||||
boxed::Box,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
format,
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
};
|
||||
use core::{
|
||||
ffi::CStr,
|
||||
ops::{Index, IndexMut},
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
use powerpc::{Extensions, Simm};
|
||||
|
||||
use crate::{
|
||||
arch::DataType,
|
||||
obj::{FlowAnalysisResult, FlowAnalysisValue, Object, Relocation, Symbol},
|
||||
util::{RawDouble, RawFloat},
|
||||
};
|
||||
|
||||
fn is_store_instruction(op: powerpc::Opcode) -> bool {
|
||||
use powerpc::Opcode;
|
||||
matches!(
|
||||
op,
|
||||
Opcode::Stbux
|
||||
| Opcode::Stbx
|
||||
| Opcode::Stfdux
|
||||
| Opcode::Stfdx
|
||||
| Opcode::Stfiwx
|
||||
| Opcode::Stfsux
|
||||
| Opcode::Stfsx
|
||||
| Opcode::Sthbrx
|
||||
| Opcode::Sthux
|
||||
| Opcode::Sthx
|
||||
| Opcode::Stswi
|
||||
| Opcode::Stswx
|
||||
| Opcode::Stwbrx
|
||||
| Opcode::Stwcx_
|
||||
| Opcode::Stwux
|
||||
| Opcode::Stwx
|
||||
| Opcode::Stwu
|
||||
| Opcode::Stb
|
||||
| Opcode::Stbu
|
||||
| Opcode::Sth
|
||||
| Opcode::Sthu
|
||||
| Opcode::Stmw
|
||||
| Opcode::Stfs
|
||||
| Opcode::Stfsu
|
||||
| Opcode::Stfd
|
||||
| Opcode::Stfdu
|
||||
)
|
||||
}
|
||||
|
||||
pub fn guess_data_type_from_load_store_inst_op(inst_op: powerpc::Opcode) -> Option<DataType> {
|
||||
use powerpc::Opcode;
|
||||
match inst_op {
|
||||
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
|
||||
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
|
||||
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
|
||||
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
|
||||
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
|
||||
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
|
||||
|
||||
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
|
||||
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
|
||||
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
|
||||
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
|
||||
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord)]
|
||||
enum RegisterContent {
|
||||
#[default]
|
||||
Unknown,
|
||||
Variable, // Multiple potential values
|
||||
FloatConstant(RawFloat),
|
||||
DoubleConstant(RawDouble),
|
||||
IntConstant(i32),
|
||||
InputRegister(u8),
|
||||
Symbol(usize),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for RegisterContent {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
RegisterContent::Unknown => write!(f, "unknown"),
|
||||
RegisterContent::Variable => write!(f, "variable"),
|
||||
RegisterContent::IntConstant(i) =>
|
||||
// -i is safe because it's at most a 16 bit constant in the i32
|
||||
{
|
||||
if *i >= 0 {
|
||||
write!(f, "0x{i:x}")
|
||||
} else {
|
||||
write!(f, "-0x{:x}", -i)
|
||||
}
|
||||
}
|
||||
RegisterContent::FloatConstant(RawFloat(fp)) => write!(f, "{fp:?}f"),
|
||||
RegisterContent::DoubleConstant(RawDouble(fp)) => write!(f, "{fp:?}d"),
|
||||
RegisterContent::InputRegister(p) => write!(f, "input{p}"),
|
||||
RegisterContent::Symbol(_u) => write!(f, "relocation"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Ord, PartialOrd)]
|
||||
struct RegisterState {
|
||||
gpr: [RegisterContent; 32],
|
||||
fpr: [RegisterContent; 32],
|
||||
}
|
||||
|
||||
impl RegisterState {
|
||||
fn new() -> Self {
|
||||
RegisterState { gpr: [RegisterContent::Unknown; 32], fpr: [RegisterContent::Unknown; 32] }
|
||||
}
|
||||
|
||||
// During a function call, these registers must be assumed trashed.
|
||||
fn clear_volatile(&mut self) {
|
||||
self[powerpc::GPR(0)] = RegisterContent::Unknown;
|
||||
for i in 0..=13 {
|
||||
self[powerpc::GPR(i)] = RegisterContent::Unknown;
|
||||
}
|
||||
for i in 0..=13 {
|
||||
self[powerpc::FPR(i)] = RegisterContent::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark potential input values.
|
||||
// Subsequent flow analysis will "realize" that they are not actually inputs if
|
||||
// they get overwritten with another value before getting read.
|
||||
fn set_potential_inputs(&mut self) {
|
||||
for g_reg in 3..=13 {
|
||||
self[powerpc::GPR(g_reg)] = RegisterContent::InputRegister(g_reg);
|
||||
}
|
||||
for f_reg in 1..=13 {
|
||||
self[powerpc::FPR(f_reg)] = RegisterContent::InputRegister(f_reg);
|
||||
}
|
||||
}
|
||||
|
||||
// If the there is no value, we can take the new known value.
|
||||
// If there's a known value different than the new value, the content
|
||||
// must is variable.
|
||||
// Returns whether the current value was updated.
|
||||
fn unify_values(current: &mut RegisterContent, new: &RegisterContent) -> bool {
|
||||
if *current == *new {
|
||||
false
|
||||
} else if *current == RegisterContent::Unknown {
|
||||
*current = *new;
|
||||
true
|
||||
} else if *current == RegisterContent::Variable {
|
||||
// Already variable
|
||||
false
|
||||
} else {
|
||||
*current = RegisterContent::Variable;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// Unify currently known register contents in a give situation with new
|
||||
// information about the register contents in that situation.
|
||||
// Currently unknown register contents can be filled, but if there are
|
||||
// conflicting contents, we go back to unknown.
|
||||
fn unify(&mut self, other: &RegisterState) -> bool {
|
||||
let mut updated = false;
|
||||
for i in 0..32 {
|
||||
updated |= Self::unify_values(&mut self.gpr[i], &other.gpr[i]);
|
||||
updated |= Self::unify_values(&mut self.fpr[i], &other.fpr[i]);
|
||||
}
|
||||
updated
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<powerpc::GPR> for RegisterState {
|
||||
type Output = RegisterContent;
|
||||
|
||||
fn index(&self, gpr: powerpc::GPR) -> &Self::Output { &self.gpr[gpr.0 as usize] }
|
||||
}
|
||||
impl IndexMut<powerpc::GPR> for RegisterState {
|
||||
fn index_mut(&mut self, gpr: powerpc::GPR) -> &mut Self::Output {
|
||||
&mut self.gpr[gpr.0 as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<powerpc::FPR> for RegisterState {
|
||||
type Output = RegisterContent;
|
||||
|
||||
fn index(&self, fpr: powerpc::FPR) -> &Self::Output { &self.fpr[fpr.0 as usize] }
|
||||
}
|
||||
impl IndexMut<powerpc::FPR> for RegisterState {
|
||||
fn index_mut(&mut self, fpr: powerpc::FPR) -> &mut Self::Output {
|
||||
&mut self.fpr[fpr.0 as usize]
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_instruction(
|
||||
registers: &mut RegisterState,
|
||||
op: &powerpc::Opcode,
|
||||
args: &powerpc::Arguments,
|
||||
) {
|
||||
use powerpc::{Argument, GPR, Opcode};
|
||||
match (op, args[0], args[1], args[2]) {
|
||||
(Opcode::Or, Argument::GPR(a), Argument::GPR(b), Argument::GPR(c)) => {
|
||||
// Move is implemented as or with self for ints
|
||||
if b == c {
|
||||
registers[a] = registers[b];
|
||||
} else {
|
||||
registers[a] = RegisterContent::Unknown;
|
||||
}
|
||||
}
|
||||
(Opcode::Fmr, Argument::FPR(a), Argument::FPR(b), _) => {
|
||||
registers[a] = registers[b];
|
||||
}
|
||||
(Opcode::Addi, Argument::GPR(a), Argument::GPR(GPR(0)), Argument::Simm(c)) => {
|
||||
// Load immidiate implemented as addi with addend = r0
|
||||
// Let Addi with other addends fall through to the case which
|
||||
// overwrites the destination
|
||||
registers[a] = RegisterContent::IntConstant(c.0 as i32);
|
||||
}
|
||||
(Opcode::Bcctr, _, _, _) => {
|
||||
// Called a function pointer, may have erased volatile registers
|
||||
registers.clear_volatile();
|
||||
}
|
||||
(Opcode::B, _, _, _) => {
|
||||
if get_branch_offset(args) == 0 {
|
||||
// Call to another function
|
||||
registers.clear_volatile();
|
||||
}
|
||||
}
|
||||
(
|
||||
Opcode::Stbu | Opcode::Sthu | Opcode::Stwu | Opcode::Stfsu | Opcode::Stfdu,
|
||||
_,
|
||||
_,
|
||||
Argument::GPR(rel),
|
||||
) => {
|
||||
// Storing with update, clear updated register (third arg)
|
||||
registers[rel] = RegisterContent::Unknown;
|
||||
}
|
||||
(
|
||||
Opcode::Stbux | Opcode::Sthux | Opcode::Stwux | Opcode::Stfsux | Opcode::Stfdux,
|
||||
_,
|
||||
Argument::GPR(rel),
|
||||
_,
|
||||
) => {
|
||||
// Storing indexed with update, clear updated register (second arg)
|
||||
registers[rel] = RegisterContent::Unknown;
|
||||
}
|
||||
(Opcode::Lmw, Argument::GPR(target), _, _) => {
|
||||
// `lmw` overwrites all registers from rd to r31.
|
||||
for reg in target.0..31 {
|
||||
registers[GPR(reg)] = RegisterContent::Unknown;
|
||||
}
|
||||
}
|
||||
(_, Argument::GPR(a), _, _) => {
|
||||
// Store instructions don't modify the GPR
|
||||
if !is_store_instruction(*op) {
|
||||
// Other operations which write to GPR a
|
||||
registers[a] = RegisterContent::Unknown;
|
||||
}
|
||||
}
|
||||
(_, Argument::FPR(a), _, _) => {
|
||||
// Store instructions don't modify the FPR
|
||||
if !is_store_instruction(*op) {
|
||||
// Other operations which write to FPR a
|
||||
registers[a] = RegisterContent::Unknown;
|
||||
}
|
||||
}
|
||||
(_, _, _, _) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_branch_offset(args: &powerpc::Arguments) -> i32 {
|
||||
for arg in args.iter() {
|
||||
match arg {
|
||||
powerpc::Argument::BranchDest(dest) => return dest.0 / 4,
|
||||
powerpc::Argument::None => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PPCFlowAnalysisResult {
|
||||
argument_contents: BTreeMap<(u64, u8), FlowAnalysisValue>,
|
||||
}
|
||||
|
||||
impl PPCFlowAnalysisResult {
|
||||
fn set_argument_value_at_address(
|
||||
&mut self,
|
||||
address: u64,
|
||||
argument: u8,
|
||||
value: FlowAnalysisValue,
|
||||
) {
|
||||
self.argument_contents.insert((address, argument), value);
|
||||
}
|
||||
|
||||
fn new() -> Self { PPCFlowAnalysisResult { argument_contents: Default::default() } }
|
||||
}
|
||||
|
||||
impl FlowAnalysisResult for PPCFlowAnalysisResult {
|
||||
fn get_argument_value_at_address(
|
||||
&self,
|
||||
address: u64,
|
||||
argument: u8,
|
||||
) -> Option<&FlowAnalysisValue> {
|
||||
self.argument_contents.get(&(address, argument))
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_text_length(s: String, max: usize) -> String {
|
||||
if s.len() <= max { s } else { format!("{}…", s.chars().take(max - 3).collect::<String>()) }
|
||||
}
|
||||
|
||||
fn get_register_content_from_reloc(
|
||||
reloc: &Relocation,
|
||||
obj: &Object,
|
||||
op: powerpc::Opcode,
|
||||
) -> RegisterContent {
|
||||
if let Some(bytes) = obj.symbol_data(reloc.target_symbol) {
|
||||
match guess_data_type_from_load_store_inst_op(op) {
|
||||
Some(DataType::Float) => {
|
||||
RegisterContent::FloatConstant(RawFloat(match obj.endianness {
|
||||
object::Endianness::Little => {
|
||||
f32::from_le_bytes(bytes.try_into().unwrap_or([0; 4]))
|
||||
}
|
||||
object::Endianness::Big => {
|
||||
f32::from_be_bytes(bytes.try_into().unwrap_or([0; 4]))
|
||||
}
|
||||
}))
|
||||
}
|
||||
Some(DataType::Double) => {
|
||||
RegisterContent::DoubleConstant(RawDouble(match obj.endianness {
|
||||
object::Endianness::Little => {
|
||||
f64::from_le_bytes(bytes.try_into().unwrap_or([0; 8]))
|
||||
}
|
||||
object::Endianness::Big => {
|
||||
f64::from_be_bytes(bytes.try_into().unwrap_or([0; 8]))
|
||||
}
|
||||
}))
|
||||
}
|
||||
_ => RegisterContent::Symbol(reloc.target_symbol),
|
||||
}
|
||||
} else {
|
||||
RegisterContent::Symbol(reloc.target_symbol)
|
||||
}
|
||||
}
|
||||
|
||||
// Executing op with args at cur_address, update current_state with symbols that
|
||||
// come from relocations. That is, references to globals, floating point
|
||||
// constants, string constants, etc.
|
||||
fn fill_registers_from_relocation(
|
||||
reloc: &Relocation,
|
||||
current_state: &mut RegisterState,
|
||||
obj: &Object,
|
||||
op: powerpc::Opcode,
|
||||
args: &powerpc::Arguments,
|
||||
) {
|
||||
// Only update the register state for loads. We may store to a reloc
|
||||
// address but that doesn't update register contents.
|
||||
if !is_store_instruction(op) {
|
||||
match (op, args[0]) {
|
||||
// Everything else is a load of some sort
|
||||
(_, powerpc::Argument::GPR(gpr)) => {
|
||||
current_state[gpr] = get_register_content_from_reloc(reloc, obj, op);
|
||||
}
|
||||
(_, powerpc::Argument::FPR(fpr)) => {
|
||||
current_state[fpr] = get_register_content_from_reloc(reloc, obj, op);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Special helper fragments generated by MWCC.
|
||||
// See: https://github.com/encounter/decomp-toolkit/blob/main/src/analysis/pass.rs
|
||||
const SLEDS: [&str; 6] = ["_savefpr_", "_restfpr_", "_savegpr_", "_restgpr_", "_savev", "_restv"];
|
||||
|
||||
fn is_sled_function(name: &str) -> bool { SLEDS.iter().any(|sled| name.starts_with(sled)) }
|
||||
|
||||
pub fn ppc_data_flow_analysis(
|
||||
obj: &Object,
|
||||
func_symbol: &Symbol,
|
||||
code: &[u8],
|
||||
relocations: &[Relocation],
|
||||
extensions: Extensions,
|
||||
) -> Box<dyn FlowAnalysisResult> {
|
||||
use alloc::collections::VecDeque;
|
||||
|
||||
use powerpc::InsIter;
|
||||
let instructions = InsIter::new(code, func_symbol.address as u32, extensions)
|
||||
.map(|(_addr, ins)| (ins.op, ins.basic().args))
|
||||
.collect_vec();
|
||||
|
||||
let func_address = func_symbol.address;
|
||||
|
||||
// Get initial register values from function parameters
|
||||
let mut initial_register_state = RegisterState::new();
|
||||
initial_register_state.set_potential_inputs();
|
||||
|
||||
let mut execution_queue = VecDeque::<(usize, RegisterState)>::new();
|
||||
execution_queue.push_back((0, initial_register_state));
|
||||
|
||||
// Execute the instructions against abstract data
|
||||
let mut failsafe_counter = 0;
|
||||
let mut taken_branches = BTreeSet::<(usize, RegisterState)>::new();
|
||||
let mut register_state_at = Vec::<RegisterState>::new();
|
||||
let mut completed_first_pass = false;
|
||||
register_state_at.resize_with(instructions.len(), RegisterState::new);
|
||||
while let Some((mut index, mut current_state)) = execution_queue.pop_front() {
|
||||
while let Some((op, args)) = instructions.get(index) {
|
||||
// Record the state at this index
|
||||
// If recording does not result in any changes to the known values
|
||||
// we're done, because the subsequent values are a function of the
|
||||
// current values so we'll get the same result as the last time
|
||||
// we went down this path.
|
||||
// Don't break out if we haven't even completed the first pass
|
||||
// through the function though.
|
||||
if !register_state_at[index].unify(¤t_state) && completed_first_pass {
|
||||
break;
|
||||
}
|
||||
|
||||
// Get symbol used in this instruction
|
||||
let cur_addr = (func_address as u32) + ((index * 4) as u32);
|
||||
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||
|
||||
// Is this a branch to a compiler generated helper? These helpers
|
||||
// do not trash registers like normal function calls, so we don't
|
||||
// want to treat this as normal execution.
|
||||
let symbol = reloc.and_then(|r| obj.symbols.get(r.target_symbol));
|
||||
let is_sled_invocation = symbol.is_some_and(|x| is_sled_function(&x.name));
|
||||
|
||||
// Execute the instruction to update the state
|
||||
// Since sled invocations are only used to save / restore registers
|
||||
// as part of prelude / cleanup in a function call we don't have to
|
||||
// do any execution for them.
|
||||
if !is_sled_invocation {
|
||||
execute_instruction(&mut current_state, op, args);
|
||||
}
|
||||
|
||||
// Fill in register state coming from relocations at this line. This
|
||||
// handles references to global variables, floating point constants,
|
||||
// etc.
|
||||
if let Some(reloc) = reloc {
|
||||
fill_registers_from_relocation(reloc, &mut current_state, obj, *op, args);
|
||||
}
|
||||
|
||||
// Add conditional branches to execution queue
|
||||
// Only take a given (address, register state) combination once. If
|
||||
// the known register state is different we have to take the branch
|
||||
// again to stabilize the known values for backwards branches.
|
||||
if op == &powerpc::Opcode::Bc {
|
||||
let branch_state = (index, current_state.clone());
|
||||
if !taken_branches.contains(&branch_state) {
|
||||
let offset = get_branch_offset(args);
|
||||
let target_index = ((index as i32) + offset) as usize;
|
||||
execution_queue.push_back((target_index, current_state.clone()));
|
||||
taken_branches.insert(branch_state);
|
||||
|
||||
// We should never hit this case, but avoid getting stuck in
|
||||
// an infinite loop if we hit some kind of bad behavior.
|
||||
failsafe_counter += 1;
|
||||
if failsafe_counter > 256 {
|
||||
//println!("Analysis of {} failed to stabilize", func_symbol.name);
|
||||
return Box::new(PPCFlowAnalysisResult::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update index
|
||||
if op == &powerpc::Opcode::B {
|
||||
// Unconditional branch
|
||||
let offset = get_branch_offset(args);
|
||||
if offset > 0 {
|
||||
// Jump table or branch to over else clause.
|
||||
index += offset as usize;
|
||||
} else if offset == 0 {
|
||||
// Function call with relocation. We'll return to
|
||||
// the next instruction.
|
||||
index += 1;
|
||||
} else {
|
||||
// Unconditional branch (E.g.: loop { ... })
|
||||
// Also some compilations of loops put the conditional at
|
||||
// the end and B to it for the check of the first iteration.
|
||||
let branch_state = (index, current_state.clone());
|
||||
if taken_branches.contains(&branch_state) {
|
||||
break;
|
||||
}
|
||||
taken_branches.insert(branch_state);
|
||||
index = ((index as i32) + offset) as usize;
|
||||
}
|
||||
} else {
|
||||
// Normal execution of next instruction
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark that we've completed at least one pass over the function, at
|
||||
// this point we can break out if the code we're running doesn't change
|
||||
// any register outcomes.
|
||||
completed_first_pass = true;
|
||||
}
|
||||
|
||||
// Store the relevant data flow values for simplified instructions
|
||||
generate_flow_analysis_result(
|
||||
obj,
|
||||
func_address,
|
||||
code,
|
||||
register_state_at,
|
||||
relocations,
|
||||
extensions,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_string_data(obj: &Object, symbol_index: usize, offset: Simm) -> Option<&str> {
|
||||
if let Some(sym) = obj.symbols.get(symbol_index)
|
||||
&& sym.name.starts_with("@stringBase")
|
||||
&& offset.0 != 0
|
||||
&& let Some(data) = obj.symbol_data(symbol_index)
|
||||
{
|
||||
let bytes = &data[offset.0 as usize..];
|
||||
if let Ok(Ok(str)) = CStr::from_bytes_until_nul(bytes).map(|x| x.to_str()) {
|
||||
return Some(str);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Write the relevant part of the flow analysis out into the FlowAnalysisResult
|
||||
// the rest of the application will use to query results of the flow analysis.
|
||||
// Flow analysis will compute the known contents of every register at every
|
||||
// line, but we only need to record the values of registers that are actually
|
||||
// referenced at each line.
|
||||
fn generate_flow_analysis_result(
|
||||
obj: &Object,
|
||||
base_address: u64,
|
||||
code: &[u8],
|
||||
register_state_at: Vec<RegisterState>,
|
||||
relocations: &[Relocation],
|
||||
extensions: Extensions,
|
||||
) -> Box<PPCFlowAnalysisResult> {
|
||||
use powerpc::{Argument, InsIter};
|
||||
let mut analysis_result = PPCFlowAnalysisResult::new();
|
||||
let default_register_state = RegisterState::new();
|
||||
for (addr, ins) in InsIter::new(code, 0, extensions) {
|
||||
let ins_address = base_address + (addr as u64);
|
||||
let index = addr / 4;
|
||||
let powerpc::ParsedIns { mnemonic: _, args } = ins.simplified();
|
||||
|
||||
// If we're already showing relocations on a line don't also show data flow
|
||||
let reloc = relocations.iter().find(|r| (r.address & !3) == ins_address);
|
||||
|
||||
// Special case to show float and double constants on the line where
|
||||
// they are being loaded.
|
||||
// We need to do this before we break out on showing relocations in the
|
||||
// subsequent if statement.
|
||||
if let (powerpc::Opcode::Lfs | powerpc::Opcode::Lfd, Some(reloc)) = (ins.op, reloc) {
|
||||
let content = get_register_content_from_reloc(reloc, obj, ins.op);
|
||||
if matches!(
|
||||
content,
|
||||
RegisterContent::FloatConstant(_) | RegisterContent::DoubleConstant(_)
|
||||
) {
|
||||
analysis_result.set_argument_value_at_address(
|
||||
ins_address,
|
||||
1,
|
||||
FlowAnalysisValue::Text(content.to_string()),
|
||||
);
|
||||
|
||||
// Don't need to show any other data flow if we're showing that
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Special case to show string constants on the line where they are
|
||||
// being indexed to. This will typically be "addi t, stringbase, offset"
|
||||
let registers = register_state_at.get(index as usize).unwrap_or(&default_register_state);
|
||||
if let (powerpc::Opcode::Addi, Argument::GPR(rel), Argument::Simm(offset)) =
|
||||
(ins.op, args[1], args[2])
|
||||
&& let RegisterContent::Symbol(sym_index) = registers[rel]
|
||||
&& let Some(str) = get_string_data(obj, sym_index, offset)
|
||||
{
|
||||
// Show the string constant in the analysis result
|
||||
let formatted = format!("\"{str}\"");
|
||||
analysis_result.set_argument_value_at_address(
|
||||
ins_address,
|
||||
2,
|
||||
FlowAnalysisValue::Text(clamp_text_length(formatted, 20)),
|
||||
);
|
||||
// Don't continue, we want to show the stringbase value as well
|
||||
}
|
||||
|
||||
let is_store = is_store_instruction(ins.op);
|
||||
for (arg_index, arg) in args.into_iter().enumerate() {
|
||||
// Hacky shorthand for determining which arguments are sources,
|
||||
// We only want to show data flow for source registers, not target
|
||||
// registers. Technically there are some non-"st_" operations which
|
||||
// read from their first argument but they're rare.
|
||||
if (arg_index == 0) && !is_store {
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = match arg {
|
||||
Argument::GPR(gpr) => Some(registers[gpr]),
|
||||
Argument::FPR(fpr) => Some(registers[fpr]),
|
||||
_ => None,
|
||||
};
|
||||
let analysis_value = match content {
|
||||
Some(RegisterContent::Symbol(s)) => {
|
||||
if reloc.is_none() {
|
||||
// Only symbols if there isn't already a relocation, because
|
||||
// code other than the data flow analysis will be showing
|
||||
// the symbol for a relocation on the line it is for. If we
|
||||
// also showed it as data flow analysis value we would be
|
||||
// showing redundant information.
|
||||
obj.symbols.get(s).map(|sym| {
|
||||
FlowAnalysisValue::Text(clamp_text_length(
|
||||
sym.demangled_name.as_ref().unwrap_or(&sym.name).clone(),
|
||||
20,
|
||||
))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Some(RegisterContent::InputRegister(reg)) => {
|
||||
let reg_name = match arg {
|
||||
Argument::GPR(_) => format!("in_r{reg}"),
|
||||
Argument::FPR(_) => format!("in_f{reg}"),
|
||||
_ => panic!("Register content should only be in a register"),
|
||||
};
|
||||
Some(FlowAnalysisValue::Text(reg_name))
|
||||
}
|
||||
Some(RegisterContent::Unknown) | Some(RegisterContent::Variable) => None,
|
||||
Some(value) => Some(FlowAnalysisValue::Text(value.to_string())),
|
||||
None => None,
|
||||
};
|
||||
if let Some(analysis_value) = analysis_value {
|
||||
analysis_result.set_argument_value_at_address(
|
||||
ins_address,
|
||||
arg_index as u8,
|
||||
analysis_value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box::new(analysis_result)
|
||||
}
|
||||
1038
objdiff-core/src/arch/ppc/mod.rs
Normal file
1038
objdiff-core/src/arch/ppc/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
1372
objdiff-core/src/arch/superh/disasm.rs
Normal file
1372
objdiff-core/src/arch/superh/disasm.rs
Normal file
File diff suppressed because it is too large
Load Diff
804
objdiff-core/src/arch/superh/mod.rs
Normal file
804
objdiff-core/src/arch/superh/mod.rs
Normal file
@@ -0,0 +1,804 @@
|
||||
use alloc::{collections::BTreeMap, format, vec, vec::Vec};
|
||||
|
||||
use anyhow::Result;
|
||||
use object::elf;
|
||||
|
||||
use crate::{
|
||||
arch::{Arch, superh::disasm::sh2_disasm},
|
||||
diff::{DiffObjConfig, display::InstructionPart},
|
||||
obj::{InstructionRef, Relocation, RelocationFlags, ResolvedInstructionRef},
|
||||
};
|
||||
|
||||
pub mod disasm;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArchSuperH {}
|
||||
|
||||
impl ArchSuperH {
|
||||
pub fn new(_file: &object::File) -> Result<Self> { Ok(Self {}) }
|
||||
}
|
||||
|
||||
struct DataInfo {
|
||||
address: u64,
|
||||
size: u32,
|
||||
}
|
||||
|
||||
impl Arch for ArchSuperH {
|
||||
fn scan_instructions_internal(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
_section_index: usize,
|
||||
_relocations: &[Relocation],
|
||||
_diff_config: &DiffObjConfig,
|
||||
) -> Result<Vec<InstructionRef>> {
|
||||
let mut ops = Vec::<InstructionRef>::with_capacity(code.len() / 2);
|
||||
let mut offset = address;
|
||||
|
||||
for chunk in code.chunks_exact(2) {
|
||||
let opcode = u16::from_be_bytes(chunk.try_into().unwrap());
|
||||
let mut parts: Vec<InstructionPart> = vec![];
|
||||
let resolved: ResolvedInstructionRef = Default::default();
|
||||
let mut branch_dest: Option<u64> = None;
|
||||
sh2_disasm(
|
||||
offset.try_into().unwrap(),
|
||||
opcode,
|
||||
true,
|
||||
&mut parts,
|
||||
&resolved,
|
||||
&mut branch_dest,
|
||||
);
|
||||
|
||||
let opcode_enum: u16 = match parts.first() {
|
||||
Some(InstructionPart::Opcode(_, val)) => *val,
|
||||
_ => 0,
|
||||
};
|
||||
ops.push(InstructionRef { address: offset, size: 2, opcode: opcode_enum, branch_dest });
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
Ok(ops)
|
||||
}
|
||||
|
||||
fn display_instruction(
|
||||
&self,
|
||||
resolved: ResolvedInstructionRef,
|
||||
_diff_config: &DiffObjConfig,
|
||||
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let opcode = u16::from_be_bytes(resolved.code.try_into().unwrap());
|
||||
let mut parts: Vec<InstructionPart> = vec![];
|
||||
let mut branch_dest: Option<u64> = None;
|
||||
|
||||
sh2_disasm(0, opcode, true, &mut parts, &resolved, &mut branch_dest);
|
||||
|
||||
if let Some(symbol_data) =
|
||||
resolved.section.data_range(resolved.symbol.address, resolved.symbol.size as usize)
|
||||
{
|
||||
// scan for data
|
||||
// map of instruction offsets to data target offsets
|
||||
let mut data_offsets = BTreeMap::<u64, DataInfo>::new();
|
||||
|
||||
let mut pos: u64 = 0;
|
||||
for chunk in symbol_data.chunks_exact(2) {
|
||||
let opcode = u16::from_be_bytes(chunk.try_into().unwrap());
|
||||
// mov.w
|
||||
if (opcode & 0xf000) == 0x9000 {
|
||||
let target = (opcode as u64 & 0xff) * 2 + 4 + pos;
|
||||
let data_info = DataInfo { address: target, size: 2 };
|
||||
data_offsets.insert(pos, data_info);
|
||||
}
|
||||
// mov.l
|
||||
else if (opcode & 0xf000) == 0xd000 {
|
||||
let target = ((opcode as u64 & 0xff) * 4 + 4 + pos) & 0xfffffffc;
|
||||
let data_info = DataInfo { address: target, size: 4 };
|
||||
data_offsets.insert(pos, data_info);
|
||||
}
|
||||
pos += 2;
|
||||
}
|
||||
|
||||
let pos = resolved.ins_ref.address - resolved.symbol.address;
|
||||
|
||||
// add the data info
|
||||
if let Some(value) = data_offsets.get(&pos) {
|
||||
if value.size == 2 && value.address as usize + 1 < symbol_data.len() {
|
||||
let data = u16::from_be_bytes(
|
||||
symbol_data[value.address as usize..value.address as usize + 2]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
parts.push(InstructionPart::basic(" /* "));
|
||||
parts.push(InstructionPart::basic("0x"));
|
||||
parts.push(InstructionPart::basic(format!("{data:04X}")));
|
||||
parts.push(InstructionPart::basic(" */"));
|
||||
} else if value.size == 4 && value.address as usize + 3 < symbol_data.len() {
|
||||
let data = u32::from_be_bytes(
|
||||
symbol_data[value.address as usize..value.address as usize + 4]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
parts.push(InstructionPart::basic(" /* "));
|
||||
parts.push(InstructionPart::basic("0x"));
|
||||
parts.push(InstructionPart::basic(format!("{data:08X}")));
|
||||
parts.push(InstructionPart::basic(" */"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for part in parts {
|
||||
cb(part)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
elf::R_SH_NONE => Some("R_SH_NONE"),
|
||||
elf::R_SH_DIR32 => Some("R_SH_DIR32"),
|
||||
elf::R_SH_REL32 => Some("R_SH_REL32"),
|
||||
elf::R_SH_DIR8WPN => Some("R_SH_DIR8WPN"),
|
||||
elf::R_SH_IND12W => Some("R_SH_IND12W"),
|
||||
elf::R_SH_DIR8WPL => Some("R_SH_DIR8WPL"),
|
||||
elf::R_SH_DIR8WPZ => Some("R_SH_DIR8WPZ"),
|
||||
elf::R_SH_DIR8BP => Some("R_SH_DIR8BP"),
|
||||
elf::R_SH_DIR8W => Some("R_SH_DIR8W"),
|
||||
elf::R_SH_DIR8L => Some("R_SH_DIR8L"),
|
||||
elf::R_SH_SWITCH16 => Some("R_SH_SWITCH16"),
|
||||
elf::R_SH_SWITCH32 => Some("R_SH_SWITCH32"),
|
||||
elf::R_SH_USES => Some("R_SH_USES"),
|
||||
elf::R_SH_COUNT => Some("R_SH_COUNT"),
|
||||
elf::R_SH_ALIGN => Some("R_SH_ALIGN"),
|
||||
elf::R_SH_CODE => Some("R_SH_CODE"),
|
||||
elf::R_SH_DATA => Some("R_SH_DATA"),
|
||||
elf::R_SH_LABEL => Some("R_SH_LABEL"),
|
||||
elf::R_SH_SWITCH8 => Some("R_SH_SWITCH8"),
|
||||
elf::R_SH_GNU_VTINHERIT => Some("R_SH_GNU_VTINHERIT"),
|
||||
elf::R_SH_GNU_VTENTRY => Some("R_SH_GNU_VTENTRY"),
|
||||
elf::R_SH_TLS_GD_32 => Some("R_SH_TLS_GD_32"),
|
||||
elf::R_SH_TLS_LD_32 => Some("R_SH_TLS_LD_32"),
|
||||
elf::R_SH_TLS_LDO_32 => Some("R_SH_TLS_LDO_32"),
|
||||
elf::R_SH_TLS_IE_32 => Some("R_SH_TLS_IE_32"),
|
||||
elf::R_SH_TLS_LE_32 => Some("R_SH_TLS_LE_32"),
|
||||
elf::R_SH_TLS_DTPMOD32 => Some("R_SH_TLS_DTPMOD32"),
|
||||
elf::R_SH_TLS_DTPOFF32 => Some("R_SH_TLS_DTPOFF32"),
|
||||
elf::R_SH_TLS_TPOFF32 => Some("R_SH_TLS_TPOFF32"),
|
||||
elf::R_SH_GOT32 => Some("R_SH_GOT32"),
|
||||
elf::R_SH_PLT32 => Some("R_SH_PLT32"),
|
||||
elf::R_SH_COPY => Some("R_SH_COPY"),
|
||||
elf::R_SH_GLOB_DAT => Some("R_SH_GLOB_DAT"),
|
||||
elf::R_SH_JMP_SLOT => Some("R_SH_JMP_SLOT"),
|
||||
elf::R_SH_RELATIVE => Some("R_SH_RELATIVE"),
|
||||
elf::R_SH_GOTOFF => Some("R_SH_GOTOFF"),
|
||||
elf::R_SH_GOTPC => Some("R_SH_GOTPC"),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn data_reloc_size(&self, flags: RelocationFlags) -> usize {
|
||||
match flags {
|
||||
RelocationFlags::Elf(elf::R_SH_DIR32) => 4,
|
||||
RelocationFlags::Elf(_) => 1,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
use super::*;
|
||||
use crate::obj::{InstructionArg, Section, SectionData, Symbol};
|
||||
|
||||
impl Display for InstructionPart<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
InstructionPart::Basic(s) => f.write_str(s),
|
||||
InstructionPart::Opcode(s, _o) => write!(f, "{s} "),
|
||||
InstructionPart::Arg(arg) => write!(f, "{arg}"),
|
||||
InstructionPart::Separator => f.write_str(", "),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for InstructionArg<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
InstructionArg::Value(v) => write!(f, "{v}"),
|
||||
InstructionArg::BranchDest(v) => write!(f, "{v}"),
|
||||
InstructionArg::Reloc => f.write_str("reloc"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sh2_display_instruction_basic_ops() {
|
||||
let arch = ArchSuperH {};
|
||||
let ops: [(u16, &str); 8] = [
|
||||
(0x0008, "clrt "),
|
||||
(0x0028, "clrmac "),
|
||||
(0x0019, "div0u "),
|
||||
(0x0009, "nop "),
|
||||
(0x002b, "rte "),
|
||||
(0x000b, "rts "),
|
||||
(0x0018, "sett "),
|
||||
(0x001b, "sleep "),
|
||||
];
|
||||
|
||||
for (opcode, expected_str) in ops {
|
||||
let code = opcode.to_be_bytes();
|
||||
let mut parts = Vec::new();
|
||||
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef { address: 0x1000, size: 2, opcode, branch_dest: None },
|
||||
code: &code,
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let joined_str: String = parts.iter().map(<_>::to_string).collect();
|
||||
assert_eq!(joined_str, expected_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sh2_display_instruction_f0ff_ops() {
|
||||
let arch = ArchSuperH {};
|
||||
let ops: [(u16, &str); 49] = [
|
||||
(0x4015, "cmp/pl r0"),
|
||||
(0x4115, "cmp/pl r1"),
|
||||
(0x4215, "cmp/pl r2"),
|
||||
(0x4315, "cmp/pl r3"),
|
||||
(0x4011, "cmp/pz r0"),
|
||||
(0x4010, "dt r0"),
|
||||
(0x0029, "movt r0"),
|
||||
(0x4004, "rotl r0"),
|
||||
(0x4005, "rotr r0"),
|
||||
(0x4024, "rotcl r0"),
|
||||
(0x4025, "rotcr r0"),
|
||||
(0x4020, "shal r0"),
|
||||
(0x4021, "shar r0"),
|
||||
(0x4000, "shll r0"),
|
||||
(0x4001, "shlr r0"),
|
||||
(0x4008, "shll2 r0"),
|
||||
(0x4009, "shlr2 r0"),
|
||||
(0x4018, "shll8 r0"),
|
||||
(0x4019, "shlr8 r0"),
|
||||
(0x4028, "shll16 r0"),
|
||||
(0x4029, "shlr16 r0"),
|
||||
(0x0002, "stc sr, r0"),
|
||||
(0x0012, "stc gbr, r0"),
|
||||
(0x0022, "stc vbr, r0"),
|
||||
(0x000a, "sts mach, r0"),
|
||||
(0x001a, "sts macl, r0"),
|
||||
(0x402a, "lds r0, pr"),
|
||||
(0x401b, "tas.b r0"),
|
||||
(0x4003, "stc.l sr, @-r0"),
|
||||
(0x4013, "stc.l gbr, @-r0"),
|
||||
(0x4023, "stc.l vbr, @-r0"),
|
||||
(0x4002, "sts.l mach, @-r0"),
|
||||
(0x4012, "sts.l macl, @-r0"),
|
||||
(0x4022, "sts.l pr, @-r0"),
|
||||
(0x400e, "ldc r0, sr"),
|
||||
(0x401e, "ldc r0, gbr"),
|
||||
(0x402e, "ldc r0, vbr"),
|
||||
(0x400a, "lds r0, mach"),
|
||||
(0x401a, "lds r0, macl"),
|
||||
(0x402b, "jmp @r0"),
|
||||
(0x400b, "jsr @r0"),
|
||||
(0x4007, "ldc.l @r0+, sr"),
|
||||
(0x4017, "ldc.l @r0+, gbr"),
|
||||
(0x4027, "ldc.l @r0+, vbr"),
|
||||
(0x4006, "lds.l @r0+, mach"),
|
||||
(0x4016, "lds.l @r0+, macl"),
|
||||
(0x4026, "lds.l @r0+, pr"),
|
||||
(0x0023, "braf r0"),
|
||||
(0x0003, "bsrf r0"),
|
||||
];
|
||||
|
||||
for (opcode, expected_str) in ops {
|
||||
let code = opcode.to_be_bytes();
|
||||
let mut parts = Vec::new();
|
||||
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef { address: 0x1000, size: 2, opcode, branch_dest: None },
|
||||
code: &code,
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let joined_str: String = parts.iter().map(<_>::to_string).collect();
|
||||
assert_eq!(joined_str, expected_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sh2_display_instructions_f00f() {
|
||||
let arch = ArchSuperH {};
|
||||
let ops: [(u16, &str); 54] = [
|
||||
(0x300c, "add r0, r0"),
|
||||
(0x300e, "addc r0, r0"),
|
||||
(0x300f, "addv r0, r0"),
|
||||
(0x2009, "and r0, r0"),
|
||||
(0x3000, "cmp/eq r0, r0"),
|
||||
(0x3002, "cmp/hs r0, r0"),
|
||||
(0x3003, "cmp/ge r0, r0"),
|
||||
(0x3006, "cmp/hi r0, r0"),
|
||||
(0x3007, "cmp/gt r0, r0"),
|
||||
(0x200c, "cmp/str r0, r0"),
|
||||
(0x3004, "div1 r0, r0"),
|
||||
(0x2007, "div0s r0, r0"),
|
||||
(0x300d, "dmuls.l r0, r0"),
|
||||
(0x3005, "dmulu.l r0, r0"),
|
||||
(0x600e, "exts.b r0, r0"),
|
||||
(0x600f, "exts.w r0, r0"),
|
||||
(0x600c, "extu.b r0, r0"),
|
||||
(0x600d, "extu.w r0, r0"),
|
||||
(0x6003, "mov r0, r0"),
|
||||
(0x0007, "mul.l r0, r0"),
|
||||
(0x200f, "muls r0, r0"),
|
||||
(0x200e, "mulu r0, r0"),
|
||||
(0x600b, "neg r0, r0"),
|
||||
(0x600a, "negc r0, r0"),
|
||||
(0x6007, "not r0, r0"),
|
||||
(0x200b, "or r0, r0"),
|
||||
(0x3008, "sub r0, r0"),
|
||||
(0x300a, "subc r0, r0"),
|
||||
(0x300b, "subv r0, r0"),
|
||||
(0x6008, "swap.b r0, r0"),
|
||||
(0x6009, "swap.w r0, r0"),
|
||||
(0x2008, "tst r0, r0"),
|
||||
(0x200a, "xor r0, r0"),
|
||||
(0x200d, "xtrct r0, r0"),
|
||||
(0x2000, "mov.b r0, @r0"),
|
||||
(0x2001, "mov.w r0, @r0"),
|
||||
(0x2002, "mov.l r0, @r0"),
|
||||
(0x6000, "mov.b @r0, r0"),
|
||||
(0x6001, "mov.w @r0, r0"),
|
||||
(0x6002, "mov.l @r0, r0"),
|
||||
(0x000f, "mac.l @r0+, @r0+"),
|
||||
(0x400f, "mac.w @r0+, @r0+"),
|
||||
(0x6004, "mov.b @r0+, r0"),
|
||||
(0x6005, "mov.w @r0+, r0"),
|
||||
(0x6006, "mov.l @r0+, r0"),
|
||||
(0x2004, "mov.b r0, @-r0"),
|
||||
(0x2005, "mov.w r0, @-r0"),
|
||||
(0x2006, "mov.l r0, @-r0"),
|
||||
(0x0004, "mov.b r0, @(r0, r0)"),
|
||||
(0x0005, "mov.w r0, @(r0, r0)"),
|
||||
(0x0006, "mov.l r0, @(r0, r0)"),
|
||||
(0x000c, "mov.b @(r0, r0), r0"),
|
||||
(0x000d, "mov.w @(r0, r0), r0"),
|
||||
(0x000e, "mov.l @(r0, r0), r0"),
|
||||
];
|
||||
|
||||
for (opcode, expected_str) in ops {
|
||||
let code = opcode.to_be_bytes();
|
||||
let mut parts = Vec::new();
|
||||
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef { address: 0x1000, size: 2, opcode, branch_dest: None },
|
||||
code: &code,
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let joined_str: String = parts.iter().map(<_>::to_string).collect();
|
||||
assert_eq!(joined_str, expected_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sh2_display_instruction_mov_immediate_offset() {
|
||||
let arch = ArchSuperH {};
|
||||
let ops: [(u16, &str); 8] = [
|
||||
(0x8000, "mov.b r0, @(0x0, r0)"),
|
||||
(0x8011, "mov.b r0, @(0x1, r1)"),
|
||||
(0x8102, "mov.w r0, @(0x4, r0)"),
|
||||
(0x8113, "mov.w r0, @(0x6, r1)"),
|
||||
(0x8404, "mov.b @(0x4, r0), r0"),
|
||||
(0x8415, "mov.b @(0x5, r1), r0"),
|
||||
(0x8506, "mov.w @(0xc, r0), r0"),
|
||||
(0x8517, "mov.w @(0xe, r1), r0"),
|
||||
];
|
||||
|
||||
for (opcode, expected_str) in ops {
|
||||
let code = opcode.to_be_bytes();
|
||||
let mut parts = Vec::new();
|
||||
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef { address: 0x1000, size: 2, opcode, branch_dest: None },
|
||||
code: &code,
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let joined_str: String = parts.iter().map(<_>::to_string).collect();
|
||||
assert_eq!(joined_str, expected_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sh2_display_instruction_gbr_and_branches() {
|
||||
let arch = ArchSuperH {};
|
||||
let ops: &[(u16, u32, &str)] = &[
|
||||
(0xc000, 0x0000, "mov.b r0, @(0x0, gbr)"),
|
||||
(0xc07f, 0x0000, "mov.b r0, @(0x7f, gbr)"),
|
||||
(0xc100, 0x0000, "mov.w r0, @(0x0, gbr)"),
|
||||
(0xc17f, 0x0000, "mov.w r0, @(0xfe, gbr)"),
|
||||
(0xc200, 0x0000, "mov.l r0, @(0x0, gbr)"),
|
||||
(0xc27f, 0x0000, "mov.l r0, @(0x1fc, gbr)"),
|
||||
(0xc400, 0x0000, "mov.b @(0x0, gbr), r0"),
|
||||
(0xc47f, 0x0000, "mov.b @(0x7f, gbr), r0"),
|
||||
(0xc500, 0x0000, "mov.w @(0x0, gbr), r0"),
|
||||
(0xc57f, 0x0000, "mov.w @(0xfe, gbr), r0"),
|
||||
(0xc600, 0x0000, "mov.l @(0x0, gbr), r0"),
|
||||
(0xc67f, 0x0000, "mov.l @(0x1fc, gbr), r0"),
|
||||
(0x8b20, 0x1000, "bf 0x44"),
|
||||
(0x8b80, 0x1000, "bf 0xffffff04"),
|
||||
(0x8f10, 0x2000, "bf.s 0x24"),
|
||||
(0x8f90, 0x2000, "bf.s 0xffffff24"),
|
||||
(0x8904, 0x3000, "bt 0xc"),
|
||||
(0x8980, 0x3000, "bt 0xffffff04"),
|
||||
(0x8d04, 0x4000, "bt.s 0xc"),
|
||||
(0x8d80, 0x4000, "bt.s 0xffffff04"),
|
||||
];
|
||||
|
||||
for &(opcode, addr, expected_str) in ops {
|
||||
let code = opcode.to_be_bytes();
|
||||
let mut parts = Vec::new();
|
||||
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef {
|
||||
address: addr as u64,
|
||||
size: 2,
|
||||
opcode,
|
||||
branch_dest: None,
|
||||
},
|
||||
code: &code,
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let joined_str: String = parts.iter().map(<_>::to_string).collect();
|
||||
assert_eq!(joined_str, expected_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sh2_display_instruction_mov_l() {
|
||||
let arch = ArchSuperH {};
|
||||
let ops: &[(u16, u32, &str)] = &[
|
||||
// mov.l rX, @(0xXXX, rY)
|
||||
(0x1000, 0x0000, "mov.l r0, @(0x0, r0)"),
|
||||
(0x1001, 0x0000, "mov.l r0, @(0x4, r0)"),
|
||||
(0x100f, 0x0000, "mov.l r0, @(0x3c, r0)"),
|
||||
(0x101f, 0x0000, "mov.l r1, @(0x3c, r0)"),
|
||||
// mov.l @(0xXXX, rY), rX
|
||||
(0x5000, 0x0000, "mov.l @(0x0, r0), r0"),
|
||||
];
|
||||
|
||||
for &(opcode, addr, expected_str) in ops {
|
||||
let code = opcode.to_be_bytes();
|
||||
let mut parts = Vec::new();
|
||||
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef {
|
||||
address: addr as u64,
|
||||
size: 2,
|
||||
opcode,
|
||||
branch_dest: None,
|
||||
},
|
||||
code: &code,
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let joined_str: String = parts.iter().map(<_>::to_string).collect();
|
||||
assert_eq!(joined_str, expected_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sh2_display_instruction_bra_bsr() {
|
||||
let arch: ArchSuperH = ArchSuperH {};
|
||||
let ops: &[(u16, u32, &str)] = &[
|
||||
// bra
|
||||
(0xa000, 0x0000, "bra 0x4"),
|
||||
(0xa001, 0x0000, "bra 0x6"),
|
||||
(0xa800, 0x0000, "bra 0xfffff004"),
|
||||
(0xa801, 0x0000, "bra 0xfffff006"),
|
||||
// bsr
|
||||
(0xb000, 0x0000, "bsr 0x4"),
|
||||
(0xb001, 0x0000, "bsr 0x6"),
|
||||
(0xb800, 0x0000, "bsr 0xfffff004"),
|
||||
(0xb801, 0x0000, "bsr 0xfffff006"),
|
||||
];
|
||||
|
||||
for &(opcode, addr, expected_str) in ops {
|
||||
let code = opcode.to_be_bytes();
|
||||
let mut parts = Vec::new();
|
||||
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef {
|
||||
address: addr as u64,
|
||||
size: 2,
|
||||
opcode,
|
||||
branch_dest: None,
|
||||
},
|
||||
code: &code,
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let joined_str: String = parts.iter().map(<_>::to_string).collect();
|
||||
assert_eq!(joined_str, expected_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sh2_display_instruction_operations() {
|
||||
let arch = ArchSuperH {};
|
||||
let ops: &[(u16, u32, &str)] = &[
|
||||
(0xcdff, 0x0000, "and.b #0xff, @(r0, gbr)"),
|
||||
(0xcfff, 0x0000, "or.b #0xff, @(r0, gbr)"),
|
||||
(0xccff, 0x0000, "tst.b #0xff, @(r0, gbr)"),
|
||||
(0xceff, 0x0000, "xor.b #0xff, @(r0, gbr)"),
|
||||
(0xc9ff, 0x0000, "and #0xff, r0"),
|
||||
(0x88ff, 0x0000, "cmp/eq #0xff, r0"),
|
||||
(0xcbff, 0x0000, "or #0xff, r0"),
|
||||
(0xc8ff, 0x0000, "tst #0xff, r0"),
|
||||
(0xcaff, 0x0000, "xor #0xff, r0"),
|
||||
(0xc3ff, 0x0000, "trapa #0xff"),
|
||||
];
|
||||
|
||||
for &(opcode, addr, expected_str) in ops {
|
||||
let code = opcode.to_be_bytes();
|
||||
let mut parts = Vec::new();
|
||||
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef {
|
||||
address: addr as u64,
|
||||
size: 2,
|
||||
opcode,
|
||||
branch_dest: None,
|
||||
},
|
||||
code: &code,
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let joined_str: String = parts.iter().map(<_>::to_string).collect();
|
||||
assert_eq!(joined_str, expected_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sh2_add_mov_unknown_instructions() {
|
||||
let arch = ArchSuperH {};
|
||||
let ops: &[(u16, u32, &str)] = &[
|
||||
(0x70FF, 0x0000, "add #0xff, r0"),
|
||||
(0xE0FF, 0x0000, "mov #0xff, r0"),
|
||||
(0x0000, 0x0000, ".word 0x0000 /* unknown instruction */"),
|
||||
];
|
||||
|
||||
for &(opcode, addr, expected_str) in ops {
|
||||
let code = opcode.to_be_bytes();
|
||||
let mut parts = Vec::new();
|
||||
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef {
|
||||
address: addr as u64,
|
||||
size: 2,
|
||||
opcode,
|
||||
branch_dest: None,
|
||||
},
|
||||
code: &code,
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let joined_str: String = parts.iter().map(<_>::to_string).collect();
|
||||
assert_eq!(joined_str, expected_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sh2_mov_instructions_with_labels() {
|
||||
let arch = ArchSuperH {};
|
||||
let ops: &[(u16, u32, &str)] =
|
||||
&[(0x9000, 0x0000, "mov.w @(0x4, pc), r0"), (0xd000, 0x0000, "mov.l @(0x4, pc), r0")];
|
||||
|
||||
for &(opcode, addr, expected_str) in ops {
|
||||
let code = opcode.to_be_bytes();
|
||||
let mut parts = Vec::new();
|
||||
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef {
|
||||
address: addr as u64,
|
||||
size: 2,
|
||||
opcode,
|
||||
branch_dest: None,
|
||||
},
|
||||
code: &code,
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let joined_str: String = parts.iter().map(<_>::to_string).collect();
|
||||
assert_eq!(joined_str, expected_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_func_0606_f378_mov_w_data_labeling() {
|
||||
let arch = ArchSuperH {};
|
||||
let ops: &[(u16, u32, &str)] = &[(0x9000, 0x0606F378, "mov.w @(0x4, pc), r0 /* 0x00B0 */")];
|
||||
|
||||
let mut code = Vec::new();
|
||||
code.extend_from_slice(&0x9000_u16.to_be_bytes());
|
||||
code.extend_from_slice(&0x0009_u16.to_be_bytes());
|
||||
code.extend_from_slice(&0x00B0_u16.to_be_bytes());
|
||||
|
||||
for &(opcode, addr, expected_str) in ops {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef {
|
||||
address: addr as u64,
|
||||
size: 2,
|
||||
opcode,
|
||||
branch_dest: None,
|
||||
},
|
||||
code: &opcode.to_be_bytes(),
|
||||
symbol: &Symbol {
|
||||
address: 0x0606F378, // func base address
|
||||
size: code.len() as u64,
|
||||
..Default::default()
|
||||
},
|
||||
section: &Section {
|
||||
address: 0x0606F378,
|
||||
size: code.len() as u64,
|
||||
data: SectionData(code.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let joined_str: String = parts.iter().map(<_>::to_string).collect();
|
||||
assert_eq!(joined_str, expected_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_func_0606_f378_mov_l_data_labeling() {
|
||||
let arch = ArchSuperH {};
|
||||
let ops: &[(u16, u32, &str)] =
|
||||
&[(0xd000, 0x0606F378, "mov.l @(0x4, pc), r0 /* 0x00B000B0 */")];
|
||||
|
||||
let mut code = Vec::new();
|
||||
code.extend_from_slice(&0xd000_u16.to_be_bytes());
|
||||
code.extend_from_slice(&0x0009_u16.to_be_bytes());
|
||||
code.extend_from_slice(&0x00B0_u16.to_be_bytes());
|
||||
code.extend_from_slice(&0x00B0_u16.to_be_bytes());
|
||||
|
||||
for &(opcode, addr, expected_str) in ops {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
arch.display_instruction(
|
||||
ResolvedInstructionRef {
|
||||
ins_ref: InstructionRef {
|
||||
address: addr as u64,
|
||||
size: 2,
|
||||
opcode,
|
||||
branch_dest: None,
|
||||
},
|
||||
code: &opcode.to_be_bytes(),
|
||||
symbol: &Symbol {
|
||||
address: 0x0606F378, // func base address
|
||||
size: code.len() as u64,
|
||||
..Default::default()
|
||||
},
|
||||
section: &Section {
|
||||
address: 0x0606F378,
|
||||
size: code.len() as u64,
|
||||
data: SectionData(code.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
&DiffObjConfig::default(),
|
||||
&mut |part| {
|
||||
parts.push(part.into_static());
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let joined_str: String = parts.iter().map(<_>::to_string).collect();
|
||||
assert_eq!(joined_str, expected_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,244 +0,0 @@
|
||||
use crate::{
|
||||
diff::{
|
||||
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
|
||||
ObjInsDiff, ObjInsDiffKind, ObjSectionDiff, ObjSymbolDiff,
|
||||
},
|
||||
obj::{
|
||||
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSectionKind, ObjSymbol,
|
||||
ObjSymbolFlagSet, ObjSymbolFlags,
|
||||
},
|
||||
};
|
||||
|
||||
// Protobuf diff types
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.serde.rs"));
|
||||
|
||||
impl DiffResult {
|
||||
pub fn new(left: Option<(&ObjInfo, &ObjDiff)>, right: Option<(&ObjInfo, &ObjDiff)>) -> Self {
|
||||
Self {
|
||||
left: left.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
|
||||
right: right.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectDiff {
|
||||
pub fn new(obj: &ObjInfo, diff: &ObjDiff) -> Self {
|
||||
Self {
|
||||
sections: diff
|
||||
.sections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, d)| SectionDiff::new(obj, i, d))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SectionDiff {
|
||||
pub fn new(obj: &ObjInfo, section_index: usize, section_diff: &ObjSectionDiff) -> Self {
|
||||
let section = &obj.sections[section_index];
|
||||
let functions = section_diff.symbols.iter().map(|d| FunctionDiff::new(obj, d)).collect();
|
||||
let data = section_diff.data_diff.iter().map(|d| DataDiff::new(obj, d)).collect();
|
||||
Self {
|
||||
name: section.name.to_string(),
|
||||
kind: SectionKind::from(section.kind) as i32,
|
||||
size: section.size,
|
||||
address: section.address,
|
||||
functions,
|
||||
data,
|
||||
match_percent: section_diff.match_percent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjSectionKind> for SectionKind {
|
||||
fn from(value: ObjSectionKind) -> Self {
|
||||
match value {
|
||||
ObjSectionKind::Code => SectionKind::SectionText,
|
||||
ObjSectionKind::Data => SectionKind::SectionData,
|
||||
ObjSectionKind::Bss => SectionKind::SectionBss,
|
||||
// TODO common
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionDiff {
|
||||
pub fn new(object: &ObjInfo, symbol_diff: &ObjSymbolDiff) -> Self {
|
||||
let (_section, symbol) = object.section_symbol(symbol_diff.symbol_ref);
|
||||
// let diff_symbol = symbol_diff.diff_symbol.map(|symbol_ref| {
|
||||
// let (_section, symbol) = object.section_symbol(symbol_ref);
|
||||
// Symbol::from(symbol)
|
||||
// });
|
||||
let instructions = symbol_diff.instructions.iter().map(InstructionDiff::from).collect();
|
||||
Self {
|
||||
symbol: Some(Symbol::from(symbol)),
|
||||
// diff_symbol,
|
||||
instructions,
|
||||
match_percent: symbol_diff.match_percent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataDiff {
|
||||
pub fn new(_object: &ObjInfo, data_diff: &ObjDataDiff) -> Self {
|
||||
Self {
|
||||
kind: DiffKind::from(data_diff.kind) as i32,
|
||||
data: data_diff.data.clone(),
|
||||
size: data_diff.len as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjSymbol> for Symbol {
|
||||
fn from(value: &'a ObjSymbol) -> Self {
|
||||
Self {
|
||||
name: value.name.to_string(),
|
||||
demangled_name: value.demangled_name.clone(),
|
||||
address: value.address,
|
||||
size: value.size,
|
||||
flags: symbol_flags(value.flags),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
|
||||
let mut flags = 0u32;
|
||||
if value.0.contains(ObjSymbolFlags::Global) {
|
||||
flags |= SymbolFlag::SymbolNone as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Local) {
|
||||
flags |= SymbolFlag::SymbolLocal as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Weak) {
|
||||
flags |= SymbolFlag::SymbolWeak as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Common) {
|
||||
flags |= SymbolFlag::SymbolCommon as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Hidden) {
|
||||
flags |= SymbolFlag::SymbolHidden as u32;
|
||||
}
|
||||
flags
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjIns> for Instruction {
|
||||
fn from(value: &'a ObjIns) -> Self {
|
||||
Self {
|
||||
address: value.address,
|
||||
size: value.size as u32,
|
||||
opcode: value.op as u32,
|
||||
mnemonic: value.mnemonic.clone(),
|
||||
formatted: value.formatted.clone(),
|
||||
arguments: value.args.iter().map(Argument::from).collect(),
|
||||
relocation: value.reloc.as_ref().map(Relocation::from),
|
||||
branch_dest: value.branch_dest,
|
||||
line_number: value.line,
|
||||
original: value.orig.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjInsArg> for Argument {
|
||||
fn from(value: &'a ObjInsArg) -> Self {
|
||||
Self {
|
||||
value: Some(match value {
|
||||
ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
|
||||
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::from(v)),
|
||||
ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
|
||||
ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ObjInsArgValue> for ArgumentValue {
|
||||
fn from(value: &ObjInsArgValue) -> Self {
|
||||
Self {
|
||||
value: Some(match value {
|
||||
ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v),
|
||||
ObjInsArgValue::Unsigned(v) => argument_value::Value::Unsigned(*v),
|
||||
ObjInsArgValue::Opaque(v) => argument_value::Value::Opaque(v.to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjReloc> for Relocation {
|
||||
fn from(value: &ObjReloc) -> Self {
|
||||
Self {
|
||||
r#type: match value.flags {
|
||||
object::RelocationFlags::Elf { r_type } => r_type,
|
||||
object::RelocationFlags::MachO { r_type, .. } => r_type as u32,
|
||||
object::RelocationFlags::Coff { typ } => typ as u32,
|
||||
object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
type_name: String::new(), // TODO
|
||||
target: Some(RelocationTarget::from(&value.target)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjSymbol> for RelocationTarget {
|
||||
fn from(value: &'a ObjSymbol) -> Self {
|
||||
Self { symbol: Some(Symbol::from(value)), addend: value.addend }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjInsDiff> for InstructionDiff {
|
||||
fn from(value: &'a ObjInsDiff) -> Self {
|
||||
Self {
|
||||
instruction: value.ins.as_ref().map(Instruction::from),
|
||||
diff_kind: DiffKind::from(value.kind) as i32,
|
||||
branch_from: value.branch_from.as_ref().map(InstructionBranchFrom::from),
|
||||
branch_to: value.branch_to.as_ref().map(InstructionBranchTo::from),
|
||||
arg_diff: value.arg_diff.iter().map(ArgumentDiff::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Option<ObjInsArgDiff>> for ArgumentDiff {
|
||||
fn from(value: &Option<ObjInsArgDiff>) -> Self {
|
||||
Self { diff_index: value.as_ref().map(|v| v.idx as u32) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjInsDiffKind> for DiffKind {
|
||||
fn from(value: ObjInsDiffKind) -> Self {
|
||||
match value {
|
||||
ObjInsDiffKind::None => DiffKind::DiffNone,
|
||||
ObjInsDiffKind::OpMismatch => DiffKind::DiffOpMismatch,
|
||||
ObjInsDiffKind::ArgMismatch => DiffKind::DiffArgMismatch,
|
||||
ObjInsDiffKind::Replace => DiffKind::DiffReplace,
|
||||
ObjInsDiffKind::Delete => DiffKind::DiffDelete,
|
||||
ObjInsDiffKind::Insert => DiffKind::DiffInsert,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjDataDiffKind> for DiffKind {
|
||||
fn from(value: ObjDataDiffKind) -> Self {
|
||||
match value {
|
||||
ObjDataDiffKind::None => DiffKind::DiffNone,
|
||||
ObjDataDiffKind::Replace => DiffKind::DiffReplace,
|
||||
ObjDataDiffKind::Delete => DiffKind::DiffDelete,
|
||||
ObjDataDiffKind::Insert => DiffKind::DiffInsert,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom {
|
||||
fn from(value: &'a ObjInsBranchFrom) -> Self {
|
||||
Self {
|
||||
instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
|
||||
branch_index: value.branch_idx as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjInsBranchTo> for InstructionBranchTo {
|
||||
fn from(value: &'a ObjInsBranchTo) -> Self {
|
||||
Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1 @@
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod diff;
|
||||
pub mod report;
|
||||
#[cfg(feature = "wasm")]
|
||||
pub mod wasm;
|
||||
|
||||
@@ -1,34 +1,50 @@
|
||||
use std::ops::AddAssign;
|
||||
#![allow(clippy::needless_lifetimes)] // Generated serde code
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use alloc::{
|
||||
string::{String, ToString},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
use core::ops::AddAssign;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use prost::Message;
|
||||
use serde_json::error::Category;
|
||||
|
||||
// Protobuf report types
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
|
||||
#[cfg(feature = "serde")]
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
|
||||
|
||||
pub const REPORT_VERSION: u32 = 1;
|
||||
pub const REPORT_VERSION: u32 = 2;
|
||||
|
||||
impl Report {
|
||||
/// Attempts to parse the report as binary protobuf or JSON.
|
||||
pub fn parse(data: &[u8]) -> Result<Self> {
|
||||
if data.is_empty() {
|
||||
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
|
||||
bail!("Empty data");
|
||||
}
|
||||
let report = if data[0] == b'{' {
|
||||
// Load as JSON
|
||||
Self::from_json(data)?
|
||||
#[cfg(feature = "serde")]
|
||||
{
|
||||
Self::from_json(data)?
|
||||
}
|
||||
#[cfg(not(feature = "serde"))]
|
||||
bail!("JSON report parsing requires the `serde` feature")
|
||||
} else {
|
||||
// Load as binary protobuf
|
||||
Self::decode(data)?
|
||||
Self::decode(data).map_err(|e| anyhow::Error::msg(e.to_string()))?
|
||||
};
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
/// Attempts to parse the report as JSON, migrating from the legacy report format if necessary.
|
||||
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
|
||||
match serde_json::from_slice::<Self>(bytes) {
|
||||
Ok(report) => Ok(report),
|
||||
Err(e) => {
|
||||
use serde_json::error::Category;
|
||||
match e.classify() {
|
||||
Category::Io | Category::Eof | Category::Syntax => Err(e),
|
||||
Category::Data => {
|
||||
@@ -43,16 +59,23 @@ impl Report {
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates the report to the latest version.
|
||||
/// Fails if the report version is newer than supported.
|
||||
pub fn migrate(&mut self) -> Result<()> {
|
||||
if self.version == 0 {
|
||||
self.migrate_v0()?;
|
||||
}
|
||||
if self.version == 1 {
|
||||
self.migrate_v1()?;
|
||||
}
|
||||
if self.version != REPORT_VERSION {
|
||||
bail!("Unsupported report version: {}", self.version);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds `complete_code`, `complete_data`, `complete_code_percent`, and `complete_data_percent`
|
||||
/// to measures, and sets `progress_categories` in unit metadata.
|
||||
fn migrate_v0(&mut self) -> Result<()> {
|
||||
let Some(measures) = &mut self.measures else {
|
||||
bail!("Missing measures in report");
|
||||
@@ -61,15 +84,16 @@ impl Report {
|
||||
let Some(unit_measures) = &mut unit.measures else {
|
||||
bail!("Missing measures in report unit");
|
||||
};
|
||||
let Some(metadata) = &mut unit.metadata else {
|
||||
bail!("Missing metadata in report unit");
|
||||
let mut complete = false;
|
||||
if let Some(metadata) = &mut unit.metadata {
|
||||
if metadata.module_name.is_some() || metadata.module_id.is_some() {
|
||||
metadata.progress_categories = vec!["modules".to_string()];
|
||||
} else {
|
||||
metadata.progress_categories = vec!["dol".to_string()];
|
||||
}
|
||||
complete = metadata.complete.unwrap_or(false);
|
||||
};
|
||||
if metadata.module_name.is_some() || metadata.module_id.is_some() {
|
||||
metadata.progress_categories = vec!["modules".to_string()];
|
||||
} else {
|
||||
metadata.progress_categories = vec!["dol".to_string()];
|
||||
}
|
||||
if metadata.complete.unwrap_or(false) {
|
||||
if complete {
|
||||
unit_measures.complete_code = unit_measures.total_code;
|
||||
unit_measures.complete_data = unit_measures.total_data;
|
||||
unit_measures.complete_code_percent = 100.0;
|
||||
@@ -84,10 +108,42 @@ impl Report {
|
||||
measures.complete_data += unit_measures.complete_data;
|
||||
}
|
||||
measures.calc_matched_percent();
|
||||
self.calculate_progress_categories();
|
||||
self.version = 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds `total_units` and `complete_units` to measures.
|
||||
fn migrate_v1(&mut self) -> Result<()> {
|
||||
let Some(total_measures) = &mut self.measures else {
|
||||
bail!("Missing measures in report");
|
||||
};
|
||||
for unit in &mut self.units {
|
||||
let Some(measures) = &mut unit.measures else {
|
||||
bail!("Missing measures in report unit");
|
||||
};
|
||||
let complete = unit.metadata.as_ref().and_then(|m| m.complete).unwrap_or(false) as u32;
|
||||
let progress_categories =
|
||||
unit.metadata.as_ref().map(|m| m.progress_categories.as_slice()).unwrap_or(&[]);
|
||||
measures.total_units = 1;
|
||||
measures.complete_units = complete;
|
||||
total_measures.total_units += 1;
|
||||
total_measures.complete_units += complete;
|
||||
for id in progress_categories {
|
||||
if let Some(category) = self.categories.iter_mut().find(|c| &c.id == id) {
|
||||
let Some(measures) = &mut category.measures else {
|
||||
bail!("Missing measures in category");
|
||||
};
|
||||
measures.total_units += 1;
|
||||
measures.complete_units += complete;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.version = 2;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calculate progress categories based on unit metadata.
|
||||
pub fn calculate_progress_categories(&mut self) {
|
||||
for unit in &self.units {
|
||||
let Some(metadata) = unit.metadata.as_ref() else {
|
||||
@@ -117,6 +173,71 @@ impl Report {
|
||||
measures.calc_matched_percent();
|
||||
}
|
||||
}
|
||||
|
||||
/// Split the report into multiple reports based on progress categories.
|
||||
/// Assumes progress categories are in the format `version`, `version.category`.
|
||||
/// This is a hack for projects that generate all versions in a single report.
|
||||
pub fn split(self) -> Vec<(String, Report)> {
|
||||
let mut reports = Vec::new();
|
||||
// Map units to Option to allow taking ownership
|
||||
let mut units = self.units.into_iter().map(Some).collect::<Vec<_>>();
|
||||
for category in &self.categories {
|
||||
if category.id.contains(".") {
|
||||
// Skip subcategories
|
||||
continue;
|
||||
}
|
||||
fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
|
||||
id.starts_with(parent) && id.get(parent.len()..).is_some_and(|s| s.starts_with(sep))
|
||||
}
|
||||
let mut sub_categories = self
|
||||
.categories
|
||||
.iter()
|
||||
.filter(|c| is_sub_category(&c.id, &category.id, '.'))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
// Remove category prefix
|
||||
for sub_category in &mut sub_categories {
|
||||
sub_category.id = sub_category.id[category.id.len() + 1..].to_string();
|
||||
}
|
||||
let mut sub_units = units
|
||||
.iter_mut()
|
||||
.filter_map(|opt| {
|
||||
let unit = opt.as_mut()?;
|
||||
let metadata = unit.metadata.as_ref()?;
|
||||
if metadata.progress_categories.contains(&category.id) {
|
||||
opt.take()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for sub_unit in &mut sub_units {
|
||||
// Remove leading version/ from unit name
|
||||
if let Some(name) =
|
||||
sub_unit.name.strip_prefix(&category.id).and_then(|s| s.strip_prefix('/'))
|
||||
{
|
||||
sub_unit.name = name.to_string();
|
||||
}
|
||||
// Filter progress categories
|
||||
let Some(metadata) = sub_unit.metadata.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
metadata.progress_categories = metadata
|
||||
.progress_categories
|
||||
.iter()
|
||||
.filter(|c| is_sub_category(c, &category.id, '.'))
|
||||
.map(|c| c[category.id.len() + 1..].to_string())
|
||||
.collect();
|
||||
}
|
||||
reports.push((category.id.clone(), Report {
|
||||
measures: category.measures,
|
||||
units: sub_units,
|
||||
version: self.version,
|
||||
categories: sub_categories,
|
||||
}));
|
||||
}
|
||||
reports
|
||||
}
|
||||
}
|
||||
|
||||
impl Measures {
|
||||
@@ -176,6 +297,8 @@ impl AddAssign for Measures {
|
||||
self.matched_functions += other.matched_functions;
|
||||
self.complete_code += other.complete_code;
|
||||
self.complete_data += other.complete_data;
|
||||
self.total_units += other.total_units;
|
||||
self.complete_units += other.complete_units;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +317,8 @@ impl FromIterator<Measures> for Measures {
|
||||
}
|
||||
|
||||
// Older JSON report types
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
struct LegacyReport {
|
||||
fuzzy_match_percent: f32,
|
||||
total_code: u64,
|
||||
@@ -231,7 +355,8 @@ impl From<LegacyReport> for Report {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
struct LegacyReportUnit {
|
||||
name: String,
|
||||
fuzzy_match_percent: f32,
|
||||
@@ -241,11 +366,11 @@ struct LegacyReportUnit {
|
||||
matched_data: u64,
|
||||
total_functions: u32,
|
||||
matched_functions: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
complete: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
module_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
module_id: Option<u32>,
|
||||
sections: Vec<LegacyReportItem>,
|
||||
functions: Vec<LegacyReportItem>,
|
||||
@@ -279,16 +404,20 @@ impl From<LegacyReportUnit> for ReportUnit {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
struct LegacyReportItem {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
demangled_name: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "serialize_hex",
|
||||
deserialize_with = "deserialize_hex"
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "serialize_hex",
|
||||
deserialize_with = "deserialize_hex"
|
||||
)
|
||||
)]
|
||||
address: Option<u64>,
|
||||
size: u64,
|
||||
@@ -305,19 +434,18 @@ impl From<LegacyReportItem> for ReportItem {
|
||||
demangled_name: value.demangled_name,
|
||||
virtual_address: value.address,
|
||||
}),
|
||||
address: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
|
||||
where S: serde::Serializer {
|
||||
if let Some(x) = x {
|
||||
s.serialize_str(&format!("{:#x}", x))
|
||||
} else {
|
||||
s.serialize_none()
|
||||
}
|
||||
if let Some(x) = x { s.serialize_str(&format!("{x:#x}")) } else { s.serialize_none() }
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
fn deserialize_hex<'de, D>(d: D) -> Result<Option<u64>, D::Error>
|
||||
where D: serde::Deserializer<'de> {
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
use prost::Message;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{bindings::diff::DiffResult, diff, obj};
|
||||
|
||||
fn parse_object(
|
||||
data: Option<Box<[u8]>>,
|
||||
config: &diff::DiffObjConfig,
|
||||
) -> Result<Option<obj::ObjInfo>, JsError> {
|
||||
data.as_ref().map(|data| obj::read::parse(data, config)).transpose().to_js()
|
||||
}
|
||||
|
||||
fn parse_and_run_diff(
|
||||
left: Option<Box<[u8]>>,
|
||||
right: Option<Box<[u8]>>,
|
||||
config: diff::DiffObjConfig,
|
||||
) -> Result<DiffResult, JsError> {
|
||||
let target = parse_object(left, &config)?;
|
||||
let base = parse_object(right, &config)?;
|
||||
run_diff(target.as_ref(), base.as_ref(), config)
|
||||
}
|
||||
|
||||
fn run_diff(
|
||||
left: Option<&obj::ObjInfo>,
|
||||
right: Option<&obj::ObjInfo>,
|
||||
config: diff::DiffObjConfig,
|
||||
) -> Result<DiffResult, JsError> {
|
||||
log::debug!("Running diff with config: {:?}", config);
|
||||
let result = diff::diff_objs(&config, left, right, None).to_js()?;
|
||||
let left = left.and_then(|o| result.left.as_ref().map(|d| (o, d)));
|
||||
let right = right.and_then(|o| result.right.as_ref().map(|d| (o, d)));
|
||||
Ok(DiffResult::new(left, right))
|
||||
}
|
||||
|
||||
// #[wasm_bindgen]
|
||||
// pub fn run_diff_json(
|
||||
// left: Option<Box<[u8]>>,
|
||||
// right: Option<Box<[u8]>>,
|
||||
// config: diff::DiffObjConfig,
|
||||
// ) -> Result<String, JsError> {
|
||||
// let out = run_diff_opt_box(left, right, config)?;
|
||||
// serde_json::to_string(&out).map_err(|e| JsError::new(&e.to_string()))
|
||||
// }
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn run_diff_proto(
|
||||
left: Option<Box<[u8]>>,
|
||||
right: Option<Box<[u8]>>,
|
||||
config: diff::DiffObjConfig,
|
||||
) -> Result<Box<[u8]>, JsError> {
|
||||
let out = parse_and_run_diff(left, right, config)?;
|
||||
Ok(out.encode_to_vec().into_boxed_slice())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
fn start() -> Result<(), JsError> {
|
||||
console_error_panic_hook::set_once();
|
||||
#[cfg(debug_assertions)]
|
||||
console_log::init_with_level(log::Level::Debug).to_js()?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
console_log::init_with_level(log::Level::Info).to_js()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn to_js_error(e: impl std::fmt::Display) -> JsError { JsError::new(&e.to_string()) }
|
||||
|
||||
trait ToJsResult {
|
||||
type Output;
|
||||
|
||||
fn to_js(self) -> Result<Self::Output, JsError>;
|
||||
}
|
||||
|
||||
impl<T, E: std::fmt::Display> ToJsResult for Result<T, E> {
|
||||
type Output = T;
|
||||
|
||||
fn to_js(self) -> Result<T, JsError> { self.map_err(to_js_error) }
|
||||
}
|
||||
109
objdiff-core/src/build/mod.rs
Normal file
109
objdiff-core/src/build/mod.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
pub mod watcher;
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use typed_path::{Utf8PlatformPathBuf, Utf8UnixPath};
|
||||
|
||||
pub struct BuildStatus {
|
||||
pub success: bool,
|
||||
pub cmdline: String,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
impl Default for BuildStatus {
|
||||
fn default() -> Self {
|
||||
BuildStatus {
|
||||
success: true,
|
||||
cmdline: String::new(),
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BuildConfig {
|
||||
pub project_dir: Option<Utf8PlatformPathBuf>,
|
||||
pub custom_make: Option<String>,
|
||||
pub custom_args: Option<Vec<String>>,
|
||||
#[allow(unused)]
|
||||
pub selected_wsl_distro: Option<String>,
|
||||
}
|
||||
|
||||
pub fn run_make(config: &BuildConfig, arg: &Utf8UnixPath) -> BuildStatus {
|
||||
let Some(cwd) = &config.project_dir else {
|
||||
return BuildStatus {
|
||||
success: false,
|
||||
stderr: "Missing project dir".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
};
|
||||
let make = config.custom_make.as_deref().unwrap_or("make");
|
||||
let make_args = config.custom_args.as_deref().unwrap_or(&[]);
|
||||
#[cfg(not(windows))]
|
||||
let mut command = {
|
||||
let mut command = Command::new(make);
|
||||
command.current_dir(cwd).args(make_args).arg(arg);
|
||||
command
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let mut command = {
|
||||
use alloc::borrow::Cow;
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
let mut command = if config.selected_wsl_distro.is_some() {
|
||||
Command::new("wsl")
|
||||
} else {
|
||||
Command::new(make)
|
||||
};
|
||||
if let Some(distro) = &config.selected_wsl_distro {
|
||||
// Strip distro root prefix \\wsl.localhost\{distro}
|
||||
let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro);
|
||||
let cwd = match cwd.strip_prefix(wsl_path_prefix) {
|
||||
// Convert to absolute Unix path
|
||||
Ok(new_cwd) => Cow::Owned(
|
||||
Utf8UnixPath::new("/").join(new_cwd.with_unix_encoding()).into_string(),
|
||||
),
|
||||
// Otherwise, use the Windows path as is
|
||||
Err(_) => Cow::Borrowed(cwd.as_str()),
|
||||
};
|
||||
|
||||
command
|
||||
.arg("--cd")
|
||||
.arg::<&str>(cwd.as_ref())
|
||||
.arg("-d")
|
||||
.arg(distro)
|
||||
.arg("--")
|
||||
.arg(make)
|
||||
.args(make_args)
|
||||
.arg(arg.as_str());
|
||||
} else {
|
||||
command.current_dir(cwd).args(make_args).arg(arg.as_str());
|
||||
}
|
||||
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
|
||||
command
|
||||
};
|
||||
let mut cmdline = shell_escape::escape(command.get_program().to_string_lossy()).into_owned();
|
||||
for arg in command.get_args() {
|
||||
cmdline.push(' ');
|
||||
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
|
||||
}
|
||||
let output = match command.output() {
|
||||
Ok(output) => output,
|
||||
Err(e) => {
|
||||
return BuildStatus {
|
||||
success: false,
|
||||
cmdline,
|
||||
stdout: Default::default(),
|
||||
stderr: e.to_string(),
|
||||
};
|
||||
}
|
||||
};
|
||||
// Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy
|
||||
let stdout = String::from_utf8(output.stdout)
|
||||
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
|
||||
let stderr = String::from_utf8(output.stderr)
|
||||
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
|
||||
BuildStatus { success: output.status.success(), cmdline, stdout, stderr }
|
||||
}
|
||||
76
objdiff-core/src/build/watcher.rs
Normal file
76
objdiff-core/src/build/watcher.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
task::Waker,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use globset::GlobSet;
|
||||
use notify::RecursiveMode;
|
||||
use notify_debouncer_full::{DebounceEventResult, new_debouncer_opt};
|
||||
|
||||
pub type Watcher = notify_debouncer_full::Debouncer<
|
||||
notify::RecommendedWatcher,
|
||||
notify_debouncer_full::RecommendedCache,
|
||||
>;
|
||||
|
||||
pub struct WatcherState {
|
||||
pub config_path: Option<PathBuf>,
|
||||
pub left_obj_path: Option<PathBuf>,
|
||||
pub right_obj_path: Option<PathBuf>,
|
||||
pub patterns: GlobSet,
|
||||
}
|
||||
|
||||
pub fn create_watcher(
|
||||
modified: Arc<AtomicBool>,
|
||||
project_dir: &Path,
|
||||
patterns: GlobSet,
|
||||
ignore_patterns: GlobSet,
|
||||
waker: Waker,
|
||||
) -> notify::Result<Watcher> {
|
||||
let base_dir = fs::canonicalize(project_dir)?;
|
||||
let base_dir_clone = base_dir.clone();
|
||||
let timeout = Duration::from_millis(200);
|
||||
let config = notify::Config::default().with_poll_interval(Duration::from_secs(2));
|
||||
let mut debouncer = new_debouncer_opt(
|
||||
timeout,
|
||||
None,
|
||||
move |result: DebounceEventResult| match result {
|
||||
Ok(events) => {
|
||||
let mut any_match = false;
|
||||
for event in events.iter() {
|
||||
if !matches!(
|
||||
event.kind,
|
||||
notify::EventKind::Modify(..)
|
||||
| notify::EventKind::Create(..)
|
||||
| notify::EventKind::Remove(..)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for path in &event.paths {
|
||||
let Ok(path) = path.strip_prefix(&base_dir_clone) else {
|
||||
continue;
|
||||
};
|
||||
if patterns.is_match(path) && !ignore_patterns.is_match(path) {
|
||||
log::debug!("File modified: {}", path.display());
|
||||
any_match = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if any_match {
|
||||
modified.store(true, Ordering::Relaxed);
|
||||
waker.wake_by_ref();
|
||||
}
|
||||
}
|
||||
Err(errors) => errors.iter().for_each(|e| log::error!("Watch error: {e:?}")),
|
||||
},
|
||||
notify_debouncer_full::RecommendedCache::new(),
|
||||
config,
|
||||
)?;
|
||||
debouncer.watch(base_dir, RecursiveMode::Recursive)?;
|
||||
Ok(debouncer)
|
||||
}
|
||||
@@ -1,162 +1,254 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
pub mod path;
|
||||
|
||||
use alloc::{
|
||||
borrow::Cow,
|
||||
collections::BTreeMap,
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use filetime::FileTime;
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use path::unix_path_serde_option;
|
||||
use typed_path::Utf8UnixPathBuf;
|
||||
|
||||
#[inline]
|
||||
fn bool_true() -> bool { true }
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
|
||||
pub struct ProjectConfig {
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub min_version: Option<String>,
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub custom_make: Option<String>,
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub custom_args: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub target_dir: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub base_dir: Option<PathBuf>,
|
||||
#[serde(default = "bool_true")]
|
||||
pub build_base: bool,
|
||||
#[serde(default)]
|
||||
pub build_target: bool,
|
||||
#[serde(default)]
|
||||
pub watch_patterns: Option<Vec<Glob>>,
|
||||
#[serde(default, alias = "units")]
|
||||
pub objects: Vec<ProjectObject>,
|
||||
#[serde(default)]
|
||||
pub progress_categories: Vec<ProjectProgressCategory>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub target_dir: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub base_dir: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub build_base: Option<bool>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub build_target: Option<bool>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub watch_patterns: Option<Vec<String>>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub ignore_patterns: Option<Vec<String>>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(alias = "objects", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub units: Option<Vec<ProjectObject>>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub progress_categories: Option<Vec<ProjectProgressCategory>>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub options: Option<ProjectOptions>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
impl ProjectConfig {
|
||||
#[inline]
|
||||
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
|
||||
|
||||
#[inline]
|
||||
pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
|
||||
self.progress_categories.as_deref().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
|
||||
self.progress_categories.get_or_insert_with(Vec::new)
|
||||
}
|
||||
|
||||
pub fn build_watch_patterns(&self) -> Result<Vec<Glob>, globset::Error> {
|
||||
Ok(if let Some(watch_patterns) = &self.watch_patterns {
|
||||
watch_patterns
|
||||
.iter()
|
||||
.map(|s| Glob::new(s))
|
||||
.collect::<Result<Vec<Glob>, globset::Error>>()?
|
||||
} else {
|
||||
default_watch_patterns()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_ignore_patterns(&self) -> Result<Vec<Glob>, globset::Error> {
|
||||
Ok(if let Some(ignore_patterns) = &self.ignore_patterns {
|
||||
ignore_patterns
|
||||
.iter()
|
||||
.map(|s| Glob::new(s))
|
||||
.collect::<Result<Vec<Glob>, globset::Error>>()?
|
||||
} else {
|
||||
default_ignore_patterns()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
|
||||
pub struct ProjectObject {
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub target_path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub base_path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub path: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub target_path: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub base_path: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
#[deprecated(note = "Use metadata.reverse_fn_order")]
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
#[deprecated(note = "Use metadata.complete")]
|
||||
pub complete: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub scratch: Option<ScratchConfig>,
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub metadata: Option<ProjectObjectMetadata>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub symbol_mappings: Option<BTreeMap<String, String>>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub options: Option<ProjectOptions>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
|
||||
pub struct ProjectObjectMetadata {
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub complete: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub source_path: Option<String>,
|
||||
#[serde(default)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub source_path: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub progress_categories: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub auto_generated: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
#[derive(Default, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
|
||||
pub struct ProjectProgressCategory {
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub type ProjectOptions = BTreeMap<String, ProjectOptionValue>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))]
|
||||
pub enum ProjectOptionValue {
|
||||
Bool(bool),
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl ProjectObject {
|
||||
pub fn name(&self) -> &str {
|
||||
if let Some(name) = &self.name {
|
||||
name
|
||||
} else if let Some(path) = &self.path {
|
||||
path.to_str().unwrap_or("[invalid path]")
|
||||
path.as_str()
|
||||
} else {
|
||||
"[unknown]"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_paths(
|
||||
&mut self,
|
||||
project_dir: &Path,
|
||||
target_obj_dir: Option<&Path>,
|
||||
base_obj_dir: Option<&Path>,
|
||||
) {
|
||||
if let (Some(target_obj_dir), Some(path), None) =
|
||||
(target_obj_dir, &self.path, &self.target_path)
|
||||
{
|
||||
self.target_path = Some(target_obj_dir.join(path));
|
||||
} else if let Some(path) = &self.target_path {
|
||||
self.target_path = Some(project_dir.join(path));
|
||||
}
|
||||
if let (Some(base_obj_dir), Some(path), None) = (base_obj_dir, &self.path, &self.base_path)
|
||||
{
|
||||
self.base_path = Some(base_obj_dir.join(path));
|
||||
} else if let Some(path) = &self.base_path {
|
||||
self.base_path = Some(project_dir.join(path));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete(&self) -> Option<bool> {
|
||||
#[allow(deprecated)]
|
||||
#[expect(deprecated)]
|
||||
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
|
||||
}
|
||||
|
||||
pub fn reverse_fn_order(&self) -> Option<bool> {
|
||||
#[allow(deprecated)]
|
||||
#[expect(deprecated)]
|
||||
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
|
||||
}
|
||||
|
||||
pub fn hidden(&self) -> bool {
|
||||
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn source_path(&self) -> Option<&Utf8UnixPathBuf> {
|
||||
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
|
||||
}
|
||||
|
||||
pub fn progress_categories(&self) -> &[String] {
|
||||
self.metadata.as_ref().and_then(|m| m.progress_categories.as_deref()).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn auto_generated(&self) -> Option<bool> {
|
||||
self.metadata.as_ref().and_then(|m| m.auto_generated)
|
||||
}
|
||||
|
||||
pub fn options(&self) -> Option<&ProjectOptions> { self.options.as_ref() }
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[derive(Default, Clone, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(default))]
|
||||
pub struct ScratchConfig {
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub platform: Option<String>,
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub compiler: Option<String>,
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub c_flags: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ctx_path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub build_ctx: bool,
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
||||
)]
|
||||
pub ctx_path: Option<Utf8UnixPathBuf>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub build_ctx: Option<bool>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub preset_id: Option<u32>,
|
||||
}
|
||||
|
||||
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",
|
||||
];
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct ProjectConfigInfo {
|
||||
pub path: PathBuf,
|
||||
pub timestamp: FileTime,
|
||||
pub const DEFAULT_IGNORE_PATTERNS: &[&str] = &["build/**/*"];
|
||||
|
||||
pub fn default_watch_patterns() -> Vec<Glob> {
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||
}
|
||||
|
||||
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
||||
pub fn default_ignore_patterns() -> Vec<Glob> {
|
||||
DEFAULT_IGNORE_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct ProjectConfigInfo {
|
||||
pub path: std::path::PathBuf,
|
||||
pub timestamp: Option<filetime::FileTime>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn try_project_config(
|
||||
dir: &std::path::Path,
|
||||
) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
||||
for filename in CONFIG_FILENAMES.iter() {
|
||||
let config_path = dir.join(filename);
|
||||
let Ok(mut file) = File::open(&config_path) else {
|
||||
let Ok(file) = std::fs::File::open(&config_path) else {
|
||||
continue;
|
||||
};
|
||||
let metadata = file.metadata();
|
||||
@@ -164,28 +256,57 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
|
||||
if !metadata.is_file() {
|
||||
continue;
|
||||
}
|
||||
let ts = FileTime::from_last_modification_time(&metadata);
|
||||
let mut result = match filename.contains("json") {
|
||||
true => read_json_config(&mut file),
|
||||
false => read_yml_config(&mut file),
|
||||
};
|
||||
let ts = filetime::FileTime::from_last_modification_time(&metadata);
|
||||
let mut reader = std::io::BufReader::new(file);
|
||||
let mut result = read_json_config(&mut reader);
|
||||
if let Ok(config) = &result {
|
||||
// Validate min_version if present
|
||||
if let Err(e) = validate_min_version(config) {
|
||||
result = Err(e);
|
||||
}
|
||||
}
|
||||
return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts }));
|
||||
return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn save_project_config(
|
||||
config: &ProjectConfig,
|
||||
info: &ProjectConfigInfo,
|
||||
) -> Result<ProjectConfigInfo> {
|
||||
if let Some(last_ts) = info.timestamp {
|
||||
// Check if the file has changed since we last read it
|
||||
if let Ok(metadata) = std::fs::metadata(&info.path) {
|
||||
let ts = filetime::FileTime::from_last_modification_time(&metadata);
|
||||
if ts != last_ts {
|
||||
return Err(anyhow!("Config file has changed since last read"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut writer = std::io::BufWriter::new(
|
||||
std::fs::File::create(&info.path).context("Failed to create config file")?,
|
||||
);
|
||||
let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
|
||||
match ext {
|
||||
"json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"),
|
||||
_ => Err(anyhow!("Unknown config file extension: {ext}")),
|
||||
}?;
|
||||
let file = writer.into_inner().context("Failed to flush file")?;
|
||||
let metadata = file.metadata().context("Failed to get file metadata")?;
|
||||
let ts = filetime::FileTime::from_last_modification_time(&metadata);
|
||||
Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
|
||||
}
|
||||
|
||||
fn validate_min_version(config: &ProjectConfig) -> Result<()> {
|
||||
let Some(min_version) = &config.min_version else { return Ok(()) };
|
||||
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
|
||||
.map_err(|e| anyhow::Error::msg(e.to_string()))
|
||||
.context("Failed to parse package version")?;
|
||||
let min_version = semver::Version::parse(min_version).context("Failed to parse min_version")?;
|
||||
let min_version = semver::Version::parse(min_version)
|
||||
.map_err(|e| anyhow::Error::msg(e.to_string()))
|
||||
.context("Failed to parse min_version")?;
|
||||
if version >= min_version {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -193,18 +314,59 @@ fn validate_min_version(config: &ProjectConfig) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_yml_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
|
||||
Ok(serde_yaml::from_reader(reader)?)
|
||||
}
|
||||
|
||||
fn read_json_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
|
||||
#[cfg(feature = "std")]
|
||||
fn read_json_config<R: std::io::Read>(reader: &mut R) -> Result<ProjectConfig> {
|
||||
Ok(serde_json::from_reader(reader)?)
|
||||
}
|
||||
|
||||
pub fn build_globset(vec: &[Glob]) -> std::result::Result<GlobSet, globset::Error> {
|
||||
pub fn build_globset(vec: &[Glob]) -> Result<GlobSet, globset::Error> {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
for glob in vec {
|
||||
builder.add(glob.clone());
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub fn apply_project_options(
|
||||
diff_config: &mut crate::diff::DiffObjConfig,
|
||||
options: &ProjectOptions,
|
||||
) -> Result<()> {
|
||||
use core::str::FromStr;
|
||||
|
||||
use crate::diff::{ConfigEnum, ConfigPropertyId, ConfigPropertyKind};
|
||||
|
||||
let mut result = Ok(());
|
||||
for (key, value) in options.iter() {
|
||||
let property_id = ConfigPropertyId::from_str(key)
|
||||
.map_err(|()| anyhow!("Invalid configuration property: {key}"))?;
|
||||
let value = match value {
|
||||
ProjectOptionValue::Bool(value) => Cow::Borrowed(if *value { "true" } else { "false" }),
|
||||
ProjectOptionValue::String(value) => Cow::Borrowed(value.as_str()),
|
||||
};
|
||||
if diff_config.set_property_value_str(property_id, &value).is_err() {
|
||||
if result.is_err() {
|
||||
// Already returning an error, skip further errors
|
||||
continue;
|
||||
}
|
||||
let mut expected = String::new();
|
||||
match property_id.kind() {
|
||||
ConfigPropertyKind::Boolean => expected.push_str("true, false"),
|
||||
ConfigPropertyKind::Choice(variants) => {
|
||||
for (idx, variant) in variants.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
expected.push_str(", ");
|
||||
}
|
||||
expected.push_str(variant.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
result = Err(anyhow!(
|
||||
"Invalid value for {}. Expected one of: {}",
|
||||
property_id.name(),
|
||||
expected
|
||||
));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
57
objdiff-core/src/config/path.rs
Normal file
57
objdiff-core/src/config/path.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
// For argp::FromArgs
|
||||
#[cfg(feature = "std")]
|
||||
pub fn platform_path(value: &str) -> Result<typed_path::Utf8PlatformPathBuf, String> {
|
||||
Ok(typed_path::Utf8PlatformPathBuf::from(value))
|
||||
}
|
||||
|
||||
/// Checks if the path is valid UTF-8 and returns it as a [`Utf8PlatformPath`].
|
||||
#[cfg(feature = "std")]
|
||||
pub fn check_path(
|
||||
path: &std::path::Path,
|
||||
) -> Result<&typed_path::Utf8PlatformPath, core::str::Utf8Error> {
|
||||
typed_path::Utf8PlatformPath::from_bytes_path(typed_path::PlatformPath::new(
|
||||
path.as_os_str().as_encoded_bytes(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Checks if the path is valid UTF-8 and returns it as a [`Utf8NativePathBuf`].
|
||||
#[cfg(feature = "std")]
|
||||
pub fn check_path_buf(
|
||||
path: std::path::PathBuf,
|
||||
) -> Result<typed_path::Utf8PlatformPathBuf, alloc::string::FromUtf8Error> {
|
||||
typed_path::Utf8PlatformPathBuf::from_bytes_path_buf(typed_path::PlatformPathBuf::from(
|
||||
path.into_os_string().into_encoded_bytes(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
pub mod unix_path_serde_option {
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
use typed_path::Utf8UnixPathBuf;
|
||||
|
||||
pub fn serialize<S>(path: &Option<Utf8UnixPathBuf>, s: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer {
|
||||
if let Some(path) = path { s.serialize_some(path.as_str()) } else { s.serialize_none() }
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Utf8UnixPathBuf>, D::Error>
|
||||
where D: Deserializer<'de> {
|
||||
Ok(Option::<String>::deserialize(deserializer)?.map(Utf8UnixPathBuf::from))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "serde", feature = "std"))]
|
||||
pub mod platform_path_serde_option {
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
use typed_path::Utf8PlatformPathBuf;
|
||||
|
||||
pub fn serialize<S>(path: &Option<Utf8PlatformPathBuf>, s: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer {
|
||||
if let Some(path) = path { s.serialize_some(path.as_str()) } else { s.serialize_none() }
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Utf8PlatformPathBuf>, D::Error>
|
||||
where D: Deserializer<'de> {
|
||||
Ok(Option::<String>::deserialize(deserializer)?.map(Utf8PlatformPathBuf::from))
|
||||
}
|
||||
}
|
||||
@@ -1,334 +1,515 @@
|
||||
use std::{cmp::max, collections::BTreeMap};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use similar::{capture_diff_slices_deadline, Algorithm};
|
||||
|
||||
use crate::{
|
||||
arch::ProcessCodeResult,
|
||||
diff::{
|
||||
DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind,
|
||||
ObjSymbolDiff,
|
||||
},
|
||||
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSymbol, ObjSymbolFlags, SymbolRef},
|
||||
use alloc::{
|
||||
collections::{BTreeMap, btree_map},
|
||||
string::{String, ToString},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
pub fn process_code_symbol(
|
||||
obj: &ObjInfo,
|
||||
symbol_ref: SymbolRef,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let (section, symbol) = obj.section_symbol(symbol_ref);
|
||||
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
|
||||
let code = §ion.data
|
||||
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
|
||||
obj.arch.process_code(
|
||||
symbol.address,
|
||||
code,
|
||||
section.orig_index,
|
||||
§ion.relocations,
|
||||
§ion.line_info,
|
||||
config,
|
||||
)
|
||||
use anyhow::{Context, Result, anyhow, ensure};
|
||||
|
||||
use super::{
|
||||
DiffObjConfig, FunctionRelocDiffs, InstructionArgDiffIndex, InstructionBranchFrom,
|
||||
InstructionBranchTo, InstructionDiffKind, InstructionDiffRow, SymbolDiff,
|
||||
display::display_ins_data_literals,
|
||||
};
|
||||
use crate::obj::{
|
||||
InstructionArg, InstructionArgValue, InstructionRef, Object, ResolvedInstructionRef,
|
||||
ResolvedRelocation, ResolvedSymbol, SymbolKind,
|
||||
};
|
||||
|
||||
pub fn no_diff_code(
|
||||
obj: &Object,
|
||||
symbol_index: usize,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<SymbolDiff> {
|
||||
let symbol = &obj.symbols[symbol_index];
|
||||
let section_index = symbol.section.ok_or_else(|| anyhow!("Missing section for symbol"))?;
|
||||
let section = &obj.sections[section_index];
|
||||
let data = section.data_range(symbol.address, symbol.size as usize).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Symbol data out of bounds: {:#x}..{:#x}",
|
||||
symbol.address,
|
||||
symbol.address + symbol.size
|
||||
)
|
||||
})?;
|
||||
let ops = obj.arch.scan_instructions(
|
||||
ResolvedSymbol { obj, symbol_index, symbol, section_index, section, data },
|
||||
diff_config,
|
||||
)?;
|
||||
let mut instruction_rows = Vec::<InstructionDiffRow>::new();
|
||||
for i in &ops {
|
||||
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,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<ObjSymbolDiff> {
|
||||
let mut diff = Vec::<ObjInsDiff>::new();
|
||||
for i in &out.insts {
|
||||
diff.push(ObjInsDiff {
|
||||
ins: Some(i.clone()),
|
||||
kind: ObjInsDiffKind::None,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
resolve_branches(&mut diff);
|
||||
Ok(ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: diff, match_percent: None })
|
||||
}
|
||||
const PENALTY_IMM_DIFF: u64 = 1;
|
||||
const PENALTY_REG_DIFF: u64 = 5;
|
||||
const PENALTY_REPLACE: u64 = 60;
|
||||
const PENALTY_INSERT_DELETE: u64 = 100;
|
||||
|
||||
pub fn diff_code(
|
||||
left_out: &ProcessCodeResult,
|
||||
right_out: &ProcessCodeResult,
|
||||
left_symbol_ref: SymbolRef,
|
||||
right_symbol_ref: SymbolRef,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<(ObjSymbolDiff, ObjSymbolDiff)> {
|
||||
let mut left_diff = Vec::<ObjInsDiff>::new();
|
||||
let mut right_diff = Vec::<ObjInsDiff>::new();
|
||||
diff_instructions(&mut left_diff, &mut right_diff, left_out, right_out)?;
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_symbol_idx: usize,
|
||||
right_symbol_idx: usize,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<(SymbolDiff, SymbolDiff)> {
|
||||
let left_symbol = &left_obj.symbols[left_symbol_idx];
|
||||
let right_symbol = &right_obj.symbols[right_symbol_idx];
|
||||
let left_section = left_symbol
|
||||
.section
|
||||
.and_then(|i| left_obj.sections.get(i))
|
||||
.ok_or_else(|| anyhow!("Missing section for symbol"))?;
|
||||
let right_section = right_symbol
|
||||
.section
|
||||
.and_then(|i| right_obj.sections.get(i))
|
||||
.ok_or_else(|| anyhow!("Missing section for symbol"))?;
|
||||
let left_data = left_section
|
||||
.data_range(left_symbol.address, left_symbol.size as usize)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Symbol data out of bounds: {:#x}..{:#x}",
|
||||
left_symbol.address,
|
||||
left_symbol.address + left_symbol.size
|
||||
)
|
||||
})?;
|
||||
let right_data = right_section
|
||||
.data_range(right_symbol.address, right_symbol.size as usize)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Symbol data out of bounds: {:#x}..{:#x}",
|
||||
right_symbol.address,
|
||||
right_symbol.address + right_symbol.size
|
||||
)
|
||||
})?;
|
||||
|
||||
resolve_branches(&mut left_diff);
|
||||
resolve_branches(&mut right_diff);
|
||||
let left_section_idx = left_symbol.section.unwrap();
|
||||
let right_section_idx = right_symbol.section.unwrap();
|
||||
let left_ops = left_obj.arch.scan_instructions(
|
||||
ResolvedSymbol {
|
||||
obj: left_obj,
|
||||
symbol_index: left_symbol_idx,
|
||||
symbol: left_symbol,
|
||||
section_index: left_section_idx,
|
||||
section: left_section,
|
||||
data: left_data,
|
||||
},
|
||||
diff_config,
|
||||
)?;
|
||||
let right_ops = right_obj.arch.scan_instructions(
|
||||
ResolvedSymbol {
|
||||
obj: right_obj,
|
||||
symbol_index: right_symbol_idx,
|
||||
symbol: right_symbol,
|
||||
section_index: right_section_idx,
|
||||
section: right_section,
|
||||
data: right_data,
|
||||
},
|
||||
diff_config,
|
||||
)?;
|
||||
let (mut left_rows, mut right_rows) = diff_instructions(&left_ops, &right_ops)?;
|
||||
resolve_branches(&left_ops, &mut left_rows);
|
||||
resolve_branches(&right_ops, &mut right_rows);
|
||||
|
||||
let mut diff_state = InsDiffState::default();
|
||||
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
|
||||
let result = compare_ins(config, left, right, &mut diff_state)?;
|
||||
left.kind = result.kind;
|
||||
right.kind = result.kind;
|
||||
left.arg_diff = result.left_args_diff;
|
||||
right.arg_diff = result.right_args_diff;
|
||||
let mut diff_state = InstructionDiffState::default();
|
||||
for (left_row, right_row) in left_rows.iter_mut().zip(right_rows.iter_mut()) {
|
||||
let result = diff_instruction(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_symbol_idx,
|
||||
right_symbol_idx,
|
||||
left_row.ins_ref,
|
||||
right_row.ins_ref,
|
||||
left_row,
|
||||
right_row,
|
||||
diff_config,
|
||||
&mut diff_state,
|
||||
)?;
|
||||
left_row.kind = result.kind;
|
||||
right_row.kind = result.kind;
|
||||
left_row.arg_diff = result.left_args_diff;
|
||||
right_row.arg_diff = result.right_args_diff;
|
||||
}
|
||||
|
||||
let total = left_out.insts.len();
|
||||
let percent = if diff_state.diff_count >= total {
|
||||
0.0
|
||||
let max_score = left_ops.len() as u64 * PENALTY_INSERT_DELETE;
|
||||
let diff_score = diff_state.diff_score.min(max_score);
|
||||
let match_percent = if max_score == 0 {
|
||||
100.0
|
||||
} else {
|
||||
((total - diff_state.diff_count) as f32 / total as f32) * 100.0
|
||||
((1.0 - (diff_score as f64 / max_score as f64)) * 100.0) as f32
|
||||
};
|
||||
|
||||
Ok((
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: left_symbol_ref,
|
||||
diff_symbol: Some(right_symbol_ref),
|
||||
instructions: left_diff,
|
||||
match_percent: Some(percent),
|
||||
SymbolDiff {
|
||||
target_symbol: Some(right_symbol_idx),
|
||||
match_percent: Some(match_percent),
|
||||
diff_score: Some((diff_score, max_score)),
|
||||
instruction_rows: left_rows,
|
||||
..Default::default()
|
||||
},
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: right_symbol_ref,
|
||||
diff_symbol: Some(left_symbol_ref),
|
||||
instructions: right_diff,
|
||||
match_percent: Some(percent),
|
||||
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()
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn diff_instructions(
|
||||
left_diff: &mut Vec<ObjInsDiff>,
|
||||
right_diff: &mut Vec<ObjInsDiff>,
|
||||
left_code: &ProcessCodeResult,
|
||||
right_code: &ProcessCodeResult,
|
||||
) -> Result<()> {
|
||||
let ops =
|
||||
capture_diff_slices_deadline(Algorithm::Patience, &left_code.ops, &right_code.ops, None);
|
||||
left_insts: &[InstructionRef],
|
||||
right_insts: &[InstructionRef],
|
||||
) -> Result<(Vec<InstructionDiffRow>, Vec<InstructionDiffRow>)> {
|
||||
let left_ops = left_insts.iter().map(|i| i.opcode).collect::<Vec<_>>();
|
||||
let right_ops = right_insts.iter().map(|i| i.opcode).collect::<Vec<_>>();
|
||||
let ops = similar::capture_diff_slices(similar::Algorithm::Patience, &left_ops, &right_ops);
|
||||
if ops.is_empty() {
|
||||
left_diff.extend(
|
||||
left_code
|
||||
.insts
|
||||
.iter()
|
||||
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
|
||||
);
|
||||
right_diff.extend(
|
||||
right_code
|
||||
.insts
|
||||
.iter()
|
||||
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
|
||||
);
|
||||
return Ok(());
|
||||
ensure!(left_insts.len() == right_insts.len());
|
||||
let left_diff = left_insts
|
||||
.iter()
|
||||
.map(|i| InstructionDiffRow { ins_ref: Some(*i), ..Default::default() })
|
||||
.collect();
|
||||
let right_diff = right_insts
|
||||
.iter()
|
||||
.map(|i| InstructionDiffRow { ins_ref: Some(*i), ..Default::default() })
|
||||
.collect();
|
||||
return Ok((left_diff, right_diff));
|
||||
}
|
||||
|
||||
let row_count = ops
|
||||
.iter()
|
||||
.map(|op| match *op {
|
||||
similar::DiffOp::Equal { len, .. } => len,
|
||||
similar::DiffOp::Delete { old_len, .. } => old_len,
|
||||
similar::DiffOp::Insert { new_len, .. } => new_len,
|
||||
similar::DiffOp::Replace { old_len, new_len, .. } => old_len.max(new_len),
|
||||
})
|
||||
.sum();
|
||||
let mut left_diff = Vec::<InstructionDiffRow>::with_capacity(row_count);
|
||||
let mut right_diff = Vec::<InstructionDiffRow>::with_capacity(row_count);
|
||||
for op in ops {
|
||||
let (_tag, left_range, right_range) = op.as_tag_tuple();
|
||||
let len = max(left_range.len(), right_range.len());
|
||||
let len = left_range.len().max(right_range.len());
|
||||
left_diff.extend(
|
||||
left_code.insts[left_range.clone()]
|
||||
.iter()
|
||||
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
|
||||
left_range
|
||||
.clone()
|
||||
.map(|i| InstructionDiffRow { ins_ref: Some(left_insts[i]), ..Default::default() }),
|
||||
);
|
||||
right_diff.extend(
|
||||
right_code.insts[right_range.clone()]
|
||||
.iter()
|
||||
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
|
||||
right_range.clone().map(|i| InstructionDiffRow {
|
||||
ins_ref: Some(right_insts[i]),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
if left_range.len() < len {
|
||||
left_diff.extend((left_range.len()..len).map(|_| ObjInsDiff::default()));
|
||||
left_diff.extend((left_range.len()..len).map(|_| InstructionDiffRow::default()));
|
||||
}
|
||||
if right_range.len() < len {
|
||||
right_diff.extend((right_range.len()..len).map(|_| ObjInsDiff::default()));
|
||||
right_diff.extend((right_range.len()..len).map(|_| InstructionDiffRow::default()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok((left_diff, right_diff))
|
||||
}
|
||||
|
||||
fn resolve_branches(vec: &mut [ObjInsDiff]) {
|
||||
let mut branch_idx = 0usize;
|
||||
fn arg_to_string(arg: &InstructionArg, reloc: Option<ResolvedRelocation>) -> String {
|
||||
match arg {
|
||||
InstructionArg::Value(arg) => arg.to_string(),
|
||||
InstructionArg::Reloc => {
|
||||
reloc.as_ref().map_or_else(|| "<unknown>".to_string(), |r| r.symbol.name.clone())
|
||||
}
|
||||
InstructionArg::BranchDest(arg) => arg.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_branches(ops: &[InstructionRef], rows: &mut [InstructionDiffRow]) {
|
||||
let mut branch_idx = 0u32;
|
||||
// Map addresses to indices
|
||||
let mut addr_map = BTreeMap::<u64, usize>::new();
|
||||
for (i, ins_diff) in vec.iter().enumerate() {
|
||||
if let Some(ins) = &ins_diff.ins {
|
||||
addr_map.insert(ins.address, i);
|
||||
let mut addr_map = BTreeMap::<u64, u32>::new();
|
||||
for (i, ins_diff) in rows.iter().enumerate() {
|
||||
if let Some(ins) = ins_diff.ins_ref {
|
||||
addr_map.insert(ins.address, i as u32);
|
||||
}
|
||||
}
|
||||
// Generate branches
|
||||
let mut branches = BTreeMap::<usize, ObjInsBranchFrom>::new();
|
||||
for (i, ins_diff) in vec.iter_mut().enumerate() {
|
||||
if let Some(ins) = &ins_diff.ins {
|
||||
if let Some(ins_idx) = ins.branch_dest.and_then(|a| addr_map.get(&a)) {
|
||||
if let Some(branch) = branches.get_mut(ins_idx) {
|
||||
ins_diff.branch_to =
|
||||
Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx: branch.branch_idx });
|
||||
branch.ins_idx.push(i);
|
||||
} else {
|
||||
ins_diff.branch_to = Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx });
|
||||
branches.insert(*ins_idx, ObjInsBranchFrom { ins_idx: vec![i], branch_idx });
|
||||
let mut branches = BTreeMap::<u32, InstructionBranchFrom>::new();
|
||||
for ((i, ins_diff), ins) in
|
||||
rows.iter_mut().enumerate().filter(|(_, row)| row.ins_ref.is_some()).zip(ops)
|
||||
{
|
||||
if let Some(ins_idx) = ins.branch_dest.and_then(|a| addr_map.get(&a).copied()) {
|
||||
match branches.entry(ins_idx) {
|
||||
btree_map::Entry::Vacant(e) => {
|
||||
ins_diff.branch_to = Some(InstructionBranchTo { ins_idx, branch_idx });
|
||||
e.insert(InstructionBranchFrom { ins_idx: vec![i as u32], branch_idx });
|
||||
branch_idx += 1;
|
||||
}
|
||||
btree_map::Entry::Occupied(e) => {
|
||||
let branch = e.into_mut();
|
||||
ins_diff.branch_to =
|
||||
Some(InstructionBranchTo { ins_idx, branch_idx: branch.branch_idx });
|
||||
branch.ins_idx.push(i as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Store branch from
|
||||
for (i, branch) in branches {
|
||||
vec[i].branch_from = Some(branch);
|
||||
rows[i as usize].branch_from = Some(branch);
|
||||
}
|
||||
}
|
||||
|
||||
fn address_eq(left: &ObjSymbol, right: &ObjSymbol) -> bool {
|
||||
left.address as i64 + left.addend == right.address as i64 + right.addend
|
||||
pub(crate) fn address_eq(left: ResolvedRelocation, right: ResolvedRelocation) -> bool {
|
||||
if right.symbol.size == 0 && left.symbol.size != 0 {
|
||||
// The base relocation is against a pool but the target relocation isn't.
|
||||
// This can happen in rare cases where the compiler will generate a pool+addend relocation
|
||||
// in the base's data, but the one detected in the target is direct with no addend.
|
||||
// Just check that the final address is the same so these count as a match.
|
||||
left.symbol.address as i64 + left.relocation.addend
|
||||
== right.symbol.address as i64 + right.relocation.addend
|
||||
} else {
|
||||
// But otherwise, if the compiler isn't using a pool, we're more strict and check that the
|
||||
// target symbol address and relocation addend both match exactly.
|
||||
left.symbol.address == right.symbol.address
|
||||
&& left.relocation.addend == right.relocation.addend
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn section_name_eq(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_section_index: usize,
|
||||
right_section_index: usize,
|
||||
) -> bool {
|
||||
left_obj.sections.get(left_section_index).is_some_and(|left_section| {
|
||||
right_obj
|
||||
.sections
|
||||
.get(right_section_index)
|
||||
.is_some_and(|right_section| left_section.name == right_section.name)
|
||||
})
|
||||
}
|
||||
|
||||
fn reloc_eq(
|
||||
config: &DiffObjConfig,
|
||||
left_reloc: Option<&ObjReloc>,
|
||||
right_reloc: Option<&ObjReloc>,
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_ins: ResolvedInstructionRef,
|
||||
right_ins: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> bool {
|
||||
let (Some(left), Some(right)) = (left_reloc, right_reloc) else {
|
||||
return false;
|
||||
let relax_reloc_diffs = diff_config.function_reloc_diffs == FunctionRelocDiffs::None;
|
||||
let (left_reloc, right_reloc) = match (left_ins.relocation, right_ins.relocation) {
|
||||
(Some(left_reloc), Some(right_reloc)) => (left_reloc, right_reloc),
|
||||
// If relocations are relaxed, match if left is missing a reloc
|
||||
(None, Some(_)) => return relax_reloc_diffs,
|
||||
(None, None) => return true,
|
||||
_ => return false,
|
||||
};
|
||||
if left.flags != right.flags {
|
||||
if left_reloc.relocation.flags != right_reloc.relocation.flags {
|
||||
return false;
|
||||
}
|
||||
if config.relax_reloc_diffs {
|
||||
if relax_reloc_diffs {
|
||||
return true;
|
||||
}
|
||||
|
||||
let name_matches = left.target.name == right.target.name;
|
||||
match (&left.target_section, &right.target_section) {
|
||||
let symbol_name_addend_matches = left_reloc.symbol.name == right_reloc.symbol.name
|
||||
&& left_reloc.relocation.addend == right_reloc.relocation.addend;
|
||||
match (&left_reloc.symbol.section, &right_reloc.symbol.section) {
|
||||
(Some(sl), Some(sr)) => {
|
||||
// Match if section and name or address match
|
||||
sl == sr && (name_matches || address_eq(&left.target, &right.target))
|
||||
section_name_eq(left_obj, right_obj, *sl, *sr)
|
||||
&& (diff_config.function_reloc_diffs == FunctionRelocDiffs::DataValue
|
||||
|| symbol_name_addend_matches
|
||||
|| address_eq(left_reloc, right_reloc))
|
||||
&& (diff_config.function_reloc_diffs == FunctionRelocDiffs::NameAddress
|
||||
|| left_reloc.symbol.kind != SymbolKind::Object
|
||||
|| right_reloc.symbol.size == 0 // Likely a pool symbol like ...data, don't treat this as a diff
|
||||
|| display_ins_data_literals(left_obj, left_ins)
|
||||
== display_ins_data_literals(right_obj, right_ins))
|
||||
}
|
||||
(Some(_), None) => false,
|
||||
(None, Some(_)) => {
|
||||
// Match if possibly stripped weak symbol
|
||||
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
|
||||
}
|
||||
(None, None) => name_matches,
|
||||
(Some(_), None) | (None, Some(_)) | (None, None) => symbol_name_addend_matches,
|
||||
}
|
||||
}
|
||||
|
||||
fn arg_eq(
|
||||
config: &DiffObjConfig,
|
||||
left: &ObjInsArg,
|
||||
right: &ObjInsArg,
|
||||
left_diff: &ObjInsDiff,
|
||||
right_diff: &ObjInsDiff,
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_row: &InstructionDiffRow,
|
||||
right_row: &InstructionDiffRow,
|
||||
left_arg: &InstructionArg,
|
||||
right_arg: &InstructionArg,
|
||||
left_ins: ResolvedInstructionRef,
|
||||
right_ins: ResolvedInstructionRef,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> bool {
|
||||
return match left {
|
||||
ObjInsArg::PlainText(l) => match right {
|
||||
ObjInsArg::PlainText(r) => l == r,
|
||||
_ => false,
|
||||
},
|
||||
ObjInsArg::Arg(l) => match right {
|
||||
ObjInsArg::Arg(r) => l == r,
|
||||
match left_arg {
|
||||
InstructionArg::Value(l) => match right_arg {
|
||||
InstructionArg::Value(r) => l.loose_eq(r),
|
||||
// If relocations are relaxed, match if left is a constant and right is a reloc
|
||||
// Useful for instances where the target object is created without relocations
|
||||
ObjInsArg::Reloc => config.relax_reloc_diffs,
|
||||
InstructionArg::Reloc => diff_config.function_reloc_diffs == FunctionRelocDiffs::None,
|
||||
_ => false,
|
||||
},
|
||||
ObjInsArg::Reloc => {
|
||||
matches!(right, ObjInsArg::Reloc)
|
||||
&& reloc_eq(
|
||||
config,
|
||||
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
|
||||
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
|
||||
)
|
||||
InstructionArg::Reloc => {
|
||||
matches!(right_arg, InstructionArg::Reloc)
|
||||
&& reloc_eq(left_obj, right_obj, left_ins, right_ins, diff_config)
|
||||
}
|
||||
ObjInsArg::BranchDest(_) => {
|
||||
InstructionArg::BranchDest(_) => match right_arg {
|
||||
// Compare dest instruction idx after diffing
|
||||
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
InstructionArg::BranchDest(_) => {
|
||||
left_row.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
== right_row.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
}
|
||||
// If relocations are relaxed, match if left is a constant and right is a reloc
|
||||
// Useful for instances where the target object is created without relocations
|
||||
InstructionArg::Reloc => diff_config.function_reloc_diffs == FunctionRelocDiffs::None,
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InstructionDiffState {
|
||||
diff_score: u64,
|
||||
left_arg_idx: u32,
|
||||
right_arg_idx: u32,
|
||||
left_args_idx: BTreeMap<String, u32>,
|
||||
right_args_idx: BTreeMap<String, u32>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InstructionDiffResult {
|
||||
kind: InstructionDiffKind,
|
||||
left_args_diff: Vec<InstructionArgDiffIndex>,
|
||||
right_args_diff: Vec<InstructionArgDiffIndex>,
|
||||
}
|
||||
|
||||
impl InstructionDiffResult {
|
||||
#[inline]
|
||||
const fn new(kind: InstructionDiffKind) -> Self {
|
||||
Self { kind, left_args_diff: Vec::new(), right_args_diff: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_instruction(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_symbol_idx: usize,
|
||||
right_symbol_idx: usize,
|
||||
l: Option<InstructionRef>,
|
||||
r: Option<InstructionRef>,
|
||||
left_row: &InstructionDiffRow,
|
||||
right_row: &InstructionDiffRow,
|
||||
diff_config: &DiffObjConfig,
|
||||
state: &mut InstructionDiffState,
|
||||
) -> Result<InstructionDiffResult> {
|
||||
let (l, r) = match (l, r) {
|
||||
(Some(l), Some(r)) => (l, r),
|
||||
(Some(_), None) => {
|
||||
state.diff_score += PENALTY_INSERT_DELETE;
|
||||
return Ok(InstructionDiffResult::new(InstructionDiffKind::Delete));
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
state.diff_score += PENALTY_INSERT_DELETE;
|
||||
return Ok(InstructionDiffResult::new(InstructionDiffKind::Insert));
|
||||
}
|
||||
(None, None) => return Ok(InstructionDiffResult::new(InstructionDiffKind::None)),
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InsDiffState {
|
||||
diff_count: usize,
|
||||
left_arg_idx: usize,
|
||||
right_arg_idx: usize,
|
||||
left_args_idx: BTreeMap<String, usize>,
|
||||
right_args_idx: BTreeMap<String, usize>,
|
||||
}
|
||||
// If opcodes don't match, replace
|
||||
if l.opcode != r.opcode {
|
||||
state.diff_score += PENALTY_REPLACE;
|
||||
return Ok(InstructionDiffResult::new(InstructionDiffKind::Replace));
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InsDiffResult {
|
||||
kind: ObjInsDiffKind,
|
||||
left_args_diff: Vec<Option<ObjInsArgDiff>>,
|
||||
right_args_diff: Vec<Option<ObjInsArgDiff>>,
|
||||
}
|
||||
let left_resolved = left_obj
|
||||
.resolve_instruction_ref(left_symbol_idx, l)
|
||||
.context("Failed to resolve left instruction")?;
|
||||
let right_resolved = right_obj
|
||||
.resolve_instruction_ref(right_symbol_idx, r)
|
||||
.context("Failed to resolve right instruction")?;
|
||||
|
||||
fn compare_ins(
|
||||
config: &DiffObjConfig,
|
||||
left: &ObjInsDiff,
|
||||
right: &ObjInsDiff,
|
||||
state: &mut InsDiffState,
|
||||
) -> Result<InsDiffResult> {
|
||||
let mut result = InsDiffResult::default();
|
||||
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
|
||||
if left_ins.args.len() != right_ins.args.len()
|
||||
|| left_ins.op != right_ins.op
|
||||
// Check if any PlainText segments differ (punctuation and spacing)
|
||||
// This indicates a more significant difference than a simple arg mismatch
|
||||
|| !left_ins.args.iter().zip(&right_ins.args).all(|(a, b)| match (a, b) {
|
||||
(ObjInsArg::PlainText(l), ObjInsArg::PlainText(r)) => l == r,
|
||||
_ => true,
|
||||
})
|
||||
{
|
||||
// Totally different op
|
||||
result.kind = ObjInsDiffKind::Replace;
|
||||
state.diff_count += 1;
|
||||
return Ok(result);
|
||||
if left_resolved.code != right_resolved.code
|
||||
|| !reloc_eq(left_obj, right_obj, left_resolved, right_resolved, diff_config)
|
||||
{
|
||||
// If either the raw code bytes or relocations don't match, process instructions and compare args
|
||||
let left_ins = left_obj.arch.process_instruction(left_resolved, diff_config)?;
|
||||
let right_ins = right_obj.arch.process_instruction(right_resolved, diff_config)?;
|
||||
if left_ins.args.len() != right_ins.args.len() {
|
||||
state.diff_score += PENALTY_REPLACE;
|
||||
return Ok(InstructionDiffResult::new(InstructionDiffKind::Replace));
|
||||
}
|
||||
let mut result = InstructionDiffResult::new(InstructionDiffKind::None);
|
||||
if left_ins.mnemonic != right_ins.mnemonic {
|
||||
// Same op but different mnemonic, still cmp args
|
||||
result.kind = ObjInsDiffKind::OpMismatch;
|
||||
state.diff_count += 1;
|
||||
state.diff_score += PENALTY_REG_DIFF;
|
||||
result.kind = InstructionDiffKind::OpMismatch;
|
||||
}
|
||||
for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
|
||||
if arg_eq(config, a, b, left, right) {
|
||||
result.left_args_diff.push(None);
|
||||
result.right_args_diff.push(None);
|
||||
for (a, b) in left_ins.args.iter().zip(right_ins.args.iter()) {
|
||||
if arg_eq(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_row,
|
||||
right_row,
|
||||
a,
|
||||
b,
|
||||
left_resolved,
|
||||
right_resolved,
|
||||
diff_config,
|
||||
) {
|
||||
result.left_args_diff.push(InstructionArgDiffIndex::NONE);
|
||||
result.right_args_diff.push(InstructionArgDiffIndex::NONE);
|
||||
} else {
|
||||
if result.kind == ObjInsDiffKind::None {
|
||||
result.kind = ObjInsDiffKind::ArgMismatch;
|
||||
state.diff_count += 1;
|
||||
state.diff_score += if let InstructionArg::Value(
|
||||
InstructionArgValue::Signed(_) | InstructionArgValue::Unsigned(_),
|
||||
) = a
|
||||
{
|
||||
PENALTY_IMM_DIFF
|
||||
} else {
|
||||
PENALTY_REG_DIFF
|
||||
};
|
||||
if result.kind == InstructionDiffKind::None {
|
||||
result.kind = InstructionDiffKind::ArgMismatch;
|
||||
}
|
||||
let a_str = match a {
|
||||
ObjInsArg::PlainText(arg) => arg.to_string(),
|
||||
ObjInsArg::Arg(arg) => arg.to_string(),
|
||||
ObjInsArg::Reloc => String::new(),
|
||||
ObjInsArg::BranchDest(arg) => format!("{arg}"),
|
||||
let a_str = arg_to_string(a, left_resolved.relocation);
|
||||
let a_diff = match state.left_args_idx.entry(a_str) {
|
||||
btree_map::Entry::Vacant(e) => {
|
||||
let idx = state.left_arg_idx;
|
||||
state.left_arg_idx = idx + 1;
|
||||
e.insert(idx);
|
||||
idx
|
||||
}
|
||||
btree_map::Entry::Occupied(e) => *e.get(),
|
||||
};
|
||||
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
|
||||
ObjInsArgDiff { idx: *idx }
|
||||
} else {
|
||||
let idx = state.left_arg_idx;
|
||||
state.left_args_idx.insert(a_str, idx);
|
||||
state.left_arg_idx += 1;
|
||||
ObjInsArgDiff { idx }
|
||||
let b_str = arg_to_string(b, right_resolved.relocation);
|
||||
let b_diff = match state.right_args_idx.entry(b_str) {
|
||||
btree_map::Entry::Vacant(e) => {
|
||||
let idx = state.right_arg_idx;
|
||||
state.right_arg_idx = idx + 1;
|
||||
e.insert(idx);
|
||||
idx
|
||||
}
|
||||
btree_map::Entry::Occupied(e) => *e.get(),
|
||||
};
|
||||
let b_str = match b {
|
||||
ObjInsArg::PlainText(arg) => arg.to_string(),
|
||||
ObjInsArg::Arg(arg) => arg.to_string(),
|
||||
ObjInsArg::Reloc => String::new(),
|
||||
ObjInsArg::BranchDest(arg) => format!("{arg}"),
|
||||
};
|
||||
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
|
||||
ObjInsArgDiff { idx: *idx }
|
||||
} else {
|
||||
let idx = state.right_arg_idx;
|
||||
state.right_args_idx.insert(b_str, idx);
|
||||
state.right_arg_idx += 1;
|
||||
ObjInsArgDiff { idx }
|
||||
};
|
||||
result.left_args_diff.push(Some(a_diff));
|
||||
result.right_args_diff.push(Some(b_diff));
|
||||
result.left_args_diff.push(InstructionArgDiffIndex::new(a_diff));
|
||||
result.right_args_diff.push(InstructionArgDiffIndex::new(b_diff));
|
||||
}
|
||||
}
|
||||
} else if left.ins.is_some() {
|
||||
result.kind = ObjInsDiffKind::Delete;
|
||||
state.diff_count += 1;
|
||||
} else {
|
||||
result.kind = ObjInsDiffKind::Insert;
|
||||
state.diff_count += 1;
|
||||
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);
|
||||
}
|
||||
Ok(result)
|
||||
|
||||
Ok(InstructionDiffResult::new(InstructionDiffKind::None))
|
||||
}
|
||||
|
||||
@@ -1,119 +1,146 @@
|
||||
use std::cmp::{max, min, Ordering};
|
||||
use alloc::{vec, vec::Vec};
|
||||
use core::{cmp::Ordering, ops::Range};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use similar::{capture_diff_slices_deadline, get_diff_ratio, Algorithm};
|
||||
use anyhow::{Result, anyhow};
|
||||
use similar::{Algorithm, capture_diff_slices, get_diff_ratio};
|
||||
|
||||
use crate::{
|
||||
diff::{ObjDataDiff, ObjDataDiffKind, ObjSectionDiff, ObjSymbolDiff},
|
||||
obj::{ObjInfo, ObjSection, SymbolRef},
|
||||
use super::{
|
||||
DataDiff, DataDiffKind, DataDiffRow, DataRelocationDiff, ObjectDiff, SectionDiff, SymbolDiff,
|
||||
code::{address_eq, section_name_eq},
|
||||
};
|
||||
use crate::obj::{Object, Relocation, ResolvedRelocation, Symbol, SymbolFlag, SymbolKind};
|
||||
|
||||
pub fn diff_bss_symbol(
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left_symbol_ref: SymbolRef,
|
||||
right_symbol_ref: SymbolRef,
|
||||
) -> Result<(ObjSymbolDiff, ObjSymbolDiff)> {
|
||||
let (_, left_symbol) = left_obj.section_symbol(left_symbol_ref);
|
||||
let (_, right_symbol) = right_obj.section_symbol(right_symbol_ref);
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_symbol_ref: usize,
|
||||
right_symbol_ref: usize,
|
||||
) -> Result<(SymbolDiff, SymbolDiff)> {
|
||||
let left_symbol = &left_obj.symbols[left_symbol_ref];
|
||||
let right_symbol = &right_obj.symbols[right_symbol_ref];
|
||||
let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 };
|
||||
Ok((
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: left_symbol_ref,
|
||||
diff_symbol: Some(right_symbol_ref),
|
||||
instructions: vec![],
|
||||
SymbolDiff {
|
||||
target_symbol: Some(right_symbol_ref),
|
||||
match_percent: Some(percent),
|
||||
diff_score: None,
|
||||
..Default::default()
|
||||
},
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: right_symbol_ref,
|
||||
diff_symbol: Some(left_symbol_ref),
|
||||
instructions: vec![],
|
||||
SymbolDiff {
|
||||
target_symbol: Some(left_symbol_ref),
|
||||
match_percent: Some(percent),
|
||||
diff_score: None,
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
|
||||
ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: vec![], match_percent: None }
|
||||
pub fn symbol_name_matches(left_name: &str, right_name: &str) -> bool {
|
||||
// Match Metrowerks symbol$1234 against symbol$2345
|
||||
// and GCC 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
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare the data sections of two object files.
|
||||
pub fn diff_data_section(
|
||||
left: &ObjSection,
|
||||
right: &ObjSection,
|
||||
left_section_diff: &ObjSectionDiff,
|
||||
right_section_diff: &ObjSectionDiff,
|
||||
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
||||
let left_max =
|
||||
left.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(left.size);
|
||||
let right_max =
|
||||
right.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(right.size);
|
||||
let left_data = &left.data[..left_max as usize];
|
||||
let right_data = &right.data[..right_max as usize];
|
||||
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
|
||||
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
|
||||
fn reloc_eq(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left: ResolvedRelocation,
|
||||
right: ResolvedRelocation,
|
||||
) -> bool {
|
||||
if left.relocation.flags != right.relocation.flags {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut left_diff = Vec::<ObjDataDiff>::new();
|
||||
let mut right_diff = Vec::<ObjDataDiff>::new();
|
||||
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
|
||||
section_name_eq(left_obj, right_obj, sl, sr)
|
||||
&& (symbol_name_addend_matches || address_eq(left, right))
|
||||
}
|
||||
(Some(_), None) | (None, Some(_)) | (None, None) => symbol_name_addend_matches,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn resolve_relocation<'obj>(
|
||||
symbols: &'obj [Symbol],
|
||||
reloc: &'obj Relocation,
|
||||
) -> ResolvedRelocation<'obj> {
|
||||
let symbol = &symbols[reloc.target_symbol];
|
||||
ResolvedRelocation { relocation: reloc, symbol }
|
||||
}
|
||||
|
||||
/// 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 = max(left_len, right_len);
|
||||
let mut len = left_len.max(right_len);
|
||||
let kind = match tag {
|
||||
similar::DiffTag::Equal => ObjDataDiffKind::None,
|
||||
similar::DiffTag::Delete => ObjDataDiffKind::Delete,
|
||||
similar::DiffTag::Insert => ObjDataDiffKind::Insert,
|
||||
similar::DiffTag::Equal => DataDiffKind::None,
|
||||
similar::DiffTag::Delete => DataDiffKind::Delete,
|
||||
similar::DiffTag::Insert => DataDiffKind::Insert,
|
||||
similar::DiffTag::Replace => {
|
||||
// Ensure replacements are equal length
|
||||
len = min(left_len, right_len);
|
||||
ObjDataDiffKind::Replace
|
||||
len = left_len.min(right_len);
|
||||
DataDiffKind::Replace
|
||||
}
|
||||
};
|
||||
let left_data = &left.data[left_range];
|
||||
let right_data = &right.data[right_range];
|
||||
left_diff.push(ObjDataDiff {
|
||||
data: left_data[..min(len, left_data.len())].to_vec(),
|
||||
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()
|
||||
size: len,
|
||||
});
|
||||
right_diff.push(ObjDataDiff {
|
||||
data: right_data[..min(len, right_data.len())].to_vec(),
|
||||
right_data_diff.push(DataDiff {
|
||||
data: right_data[..len.min(right_data.len())].to_vec(),
|
||||
kind,
|
||||
len,
|
||||
..Default::default()
|
||||
size: len,
|
||||
});
|
||||
if kind == ObjDataDiffKind::Replace {
|
||||
if kind == DataDiffKind::Replace {
|
||||
match left_len.cmp(&right_len) {
|
||||
Ordering::Less => {
|
||||
let len = right_len - left_len;
|
||||
left_diff.push(ObjDataDiff {
|
||||
left_data_diff.push(DataDiff {
|
||||
data: vec![],
|
||||
kind: ObjDataDiffKind::Insert,
|
||||
len,
|
||||
..Default::default()
|
||||
kind: DataDiffKind::Insert,
|
||||
size: len,
|
||||
});
|
||||
right_diff.push(ObjDataDiff {
|
||||
right_data_diff.push(DataDiff {
|
||||
data: right_data[left_len..right_len].to_vec(),
|
||||
kind: ObjDataDiffKind::Insert,
|
||||
len,
|
||||
..Default::default()
|
||||
kind: DataDiffKind::Insert,
|
||||
size: len,
|
||||
});
|
||||
}
|
||||
Ordering::Greater => {
|
||||
let len = left_len - right_len;
|
||||
left_diff.push(ObjDataDiff {
|
||||
left_data_diff.push(DataDiff {
|
||||
data: left_data[right_len..left_len].to_vec(),
|
||||
kind: ObjDataDiffKind::Delete,
|
||||
len,
|
||||
..Default::default()
|
||||
kind: DataDiffKind::Delete,
|
||||
size: len,
|
||||
});
|
||||
right_diff.push(ObjDataDiff {
|
||||
right_data_diff.push(DataDiff {
|
||||
data: vec![],
|
||||
kind: ObjDataDiffKind::Delete,
|
||||
len,
|
||||
..Default::default()
|
||||
kind: DataDiffKind::Delete,
|
||||
size: len,
|
||||
});
|
||||
}
|
||||
Ordering::Equal => {}
|
||||
@@ -121,52 +148,360 @@ pub fn diff_data_section(
|
||||
}
|
||||
}
|
||||
|
||||
let (mut left_section_diff, mut right_section_diff) =
|
||||
diff_generic_section(left, right, left_section_diff, right_section_diff)?;
|
||||
left_section_diff.data_diff = left_diff;
|
||||
right_section_diff.data_diff = right_diff;
|
||||
// Use the highest match percent between two options:
|
||||
// - Left symbols matching right symbols by name
|
||||
// - Diff of the data itself
|
||||
if left_section_diff.match_percent.unwrap_or(-1.0) < match_percent {
|
||||
left_section_diff.match_percent = Some(match_percent);
|
||||
right_section_diff.match_percent = Some(match_percent);
|
||||
(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,
|
||||
left_section_idx: usize,
|
||||
right_section_idx: usize,
|
||||
left_range: Range<usize>,
|
||||
right_range: Range<usize>,
|
||||
) -> Vec<(DataDiffKind, Option<ResolvedRelocation<'left>>, Option<ResolvedRelocation<'right>>)> {
|
||||
let left_section = &left_obj.sections[left_section_idx];
|
||||
let right_section = &right_obj.sections[right_section_idx];
|
||||
let mut diffs = Vec::new();
|
||||
for left_reloc in left_section.relocations.iter() {
|
||||
if !left_range.contains(&(left_reloc.address as usize)) {
|
||||
continue;
|
||||
}
|
||||
let left_offset = left_reloc.address as usize - left_range.start;
|
||||
let left_reloc = resolve_relocation(&left_obj.symbols, left_reloc);
|
||||
let Some(right_reloc) = right_section.relocations.iter().find(|r| {
|
||||
if !right_range.contains(&(r.address as usize)) {
|
||||
return false;
|
||||
}
|
||||
let right_offset = r.address as usize - right_range.start;
|
||||
right_offset == left_offset
|
||||
}) else {
|
||||
diffs.push((DataDiffKind::Delete, Some(left_reloc), None));
|
||||
continue;
|
||||
};
|
||||
let right_reloc = resolve_relocation(&right_obj.symbols, right_reloc);
|
||||
if reloc_eq(left_obj, right_obj, left_reloc, right_reloc) {
|
||||
diffs.push((DataDiffKind::None, Some(left_reloc), Some(right_reloc)));
|
||||
} else {
|
||||
diffs.push((DataDiffKind::Replace, Some(left_reloc), Some(right_reloc)));
|
||||
}
|
||||
}
|
||||
for right_reloc in right_section.relocations.iter() {
|
||||
if !right_range.contains(&(right_reloc.address as usize)) {
|
||||
continue;
|
||||
}
|
||||
let right_offset = right_reloc.address as usize - right_range.start;
|
||||
let right_reloc = resolve_relocation(&right_obj.symbols, right_reloc);
|
||||
let Some(_) = left_section.relocations.iter().find(|r| {
|
||||
if !left_range.contains(&(r.address as usize)) {
|
||||
return false;
|
||||
}
|
||||
let left_offset = r.address as usize - left_range.start;
|
||||
left_offset == right_offset
|
||||
}) else {
|
||||
diffs.push((DataDiffKind::Insert, None, Some(right_reloc)));
|
||||
continue;
|
||||
};
|
||||
// No need to check the cases for relocations being deleted or matching again.
|
||||
// They were already handled in the loop over the left relocs.
|
||||
}
|
||||
diffs
|
||||
}
|
||||
|
||||
pub fn no_diff_data_section(obj: &Object, section_idx: usize) -> Result<SectionDiff> {
|
||||
let section = &obj.sections[section_idx];
|
||||
|
||||
let data_diff = vec![DataDiff {
|
||||
data: section.data.0.clone(),
|
||||
kind: DataDiffKind::None,
|
||||
size: section.data.len(),
|
||||
}];
|
||||
|
||||
let mut reloc_diffs = Vec::new();
|
||||
for reloc in section.relocations.iter() {
|
||||
let reloc_len = obj.arch.data_reloc_size(reloc.flags);
|
||||
let range = reloc.address..reloc.address + reloc_len as u64;
|
||||
reloc_diffs.push(DataRelocationDiff {
|
||||
reloc: reloc.clone(),
|
||||
kind: DataDiffKind::None,
|
||||
range,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(SectionDiff { match_percent: Some(0.0), data_diff, reloc_diff: reloc_diffs })
|
||||
}
|
||||
|
||||
/// Compare the data sections of two object files.
|
||||
pub fn diff_data_section(
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_diff: &ObjectDiff,
|
||||
right_diff: &ObjectDiff,
|
||||
left_section_idx: usize,
|
||||
right_section_idx: usize,
|
||||
) -> Result<(SectionDiff, SectionDiff)> {
|
||||
let left_section = &left_obj.sections[left_section_idx];
|
||||
let right_section = &right_obj.sections[right_section_idx];
|
||||
let left_max = symbols_matching_section(&left_obj.symbols, left_section_idx)
|
||||
.filter_map(|(_, s)| s.address.checked_sub(left_section.address).map(|a| a + s.size))
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
.min(left_section.size);
|
||||
let right_max = symbols_matching_section(&right_obj.symbols, right_section_idx)
|
||||
.filter_map(|(_, s)| s.address.checked_sub(right_section.address).map(|a| a + s.size))
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
.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 (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();
|
||||
for (diff_kind, left_reloc, right_reloc) in diff_data_relocs_for_range(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_section_idx,
|
||||
right_section_idx,
|
||||
0..left_max as usize,
|
||||
0..right_max as usize,
|
||||
) {
|
||||
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..left_reloc.relocation.address + len as u64;
|
||||
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..right_reloc.relocation.address + len as u64;
|
||||
right_reloc_diffs.push(DataRelocationDiff {
|
||||
reloc: right_reloc.relocation.clone(),
|
||||
kind: diff_kind,
|
||||
range,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let (mut left_section_diff, mut right_section_diff) = diff_generic_section(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_diff,
|
||||
right_diff,
|
||||
left_section_idx,
|
||||
right_section_idx,
|
||||
)?;
|
||||
let all_left_relocs_match = left_reloc_diffs.iter().all(|d| d.kind == DataDiffKind::None);
|
||||
left_section_diff.data_diff = left_data_diff;
|
||||
right_section_diff.data_diff = right_data_diff;
|
||||
left_section_diff.reloc_diff = left_reloc_diffs;
|
||||
right_section_diff.reloc_diff = right_reloc_diffs;
|
||||
if all_left_relocs_match {
|
||||
// Use the highest match percent between two options:
|
||||
// - Left symbols matching right symbols by name
|
||||
// - Diff of the data itself
|
||||
// We only do this when all relocations on the left side match.
|
||||
if left_section_diff.match_percent.unwrap_or(-1.0) < match_percent {
|
||||
left_section_diff.match_percent = Some(match_percent);
|
||||
}
|
||||
}
|
||||
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 = §ion.data[range.clone()];
|
||||
|
||||
let data_diff = vec![DataDiff {
|
||||
data: data.to_vec(),
|
||||
kind: DataDiffKind::None,
|
||||
size: symbol.size as usize,
|
||||
}];
|
||||
|
||||
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..reloc.address + reloc_len as u64;
|
||||
reloc_diffs.push(DataRelocationDiff {
|
||||
reloc: reloc.clone(),
|
||||
kind: DataDiffKind::None,
|
||||
range,
|
||||
});
|
||||
}
|
||||
|
||||
let data_rows = build_data_diff_rows(&data_diff, &reloc_diffs, symbol.address);
|
||||
Ok(SymbolDiff {
|
||||
target_symbol: None,
|
||||
match_percent: None,
|
||||
diff_score: None,
|
||||
data_rows,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn diff_data_symbol(
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left_symbol_ref: SymbolRef,
|
||||
right_symbol_ref: SymbolRef,
|
||||
) -> Result<(ObjSymbolDiff, ObjSymbolDiff)> {
|
||||
let (left_section, left_symbol) = left_obj.section_symbol(left_symbol_ref);
|
||||
let (right_section, right_symbol) = right_obj.section_symbol(right_symbol_ref);
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_symbol_idx: usize,
|
||||
right_symbol_idx: usize,
|
||||
) -> Result<(SymbolDiff, SymbolDiff)> {
|
||||
let left_symbol = &left_obj.symbols[left_symbol_idx];
|
||||
let right_symbol = &right_obj.symbols[right_symbol_idx];
|
||||
|
||||
let left_section = left_section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
|
||||
let right_section = right_section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
|
||||
let left_section_idx =
|
||||
left_symbol.section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
|
||||
let right_section_idx =
|
||||
right_symbol.section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
|
||||
|
||||
let left_data = &left_section.data[left_symbol.section_address as usize
|
||||
..(left_symbol.section_address + left_symbol.size) as usize];
|
||||
let right_data = &right_section.data[right_symbol.section_address as usize
|
||||
..(right_symbol.section_address + right_symbol.size) as usize];
|
||||
let left_section = &left_obj.sections[left_section_idx];
|
||||
let right_section = &right_obj.sections[right_section_idx];
|
||||
|
||||
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
|
||||
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
|
||||
let left_start = left_symbol
|
||||
.address
|
||||
.checked_sub(left_section.address)
|
||||
.ok_or_else(|| anyhow!("Symbol address out of section bounds"))?;
|
||||
let right_start = right_symbol
|
||||
.address
|
||||
.checked_sub(right_section.address)
|
||||
.ok_or_else(|| anyhow!("Symbol address out of section bounds"))?;
|
||||
let left_end = left_start + left_symbol.size;
|
||||
if left_end > left_section.size {
|
||||
return Err(anyhow!(
|
||||
"Symbol {} size out of section bounds ({} > {})",
|
||||
left_symbol.name,
|
||||
left_end,
|
||||
left_section.size
|
||||
));
|
||||
}
|
||||
let right_end = right_start + right_symbol.size;
|
||||
if right_end > right_section.size {
|
||||
return Err(anyhow!(
|
||||
"Symbol {} size out of section bounds ({} > {})",
|
||||
right_symbol.name,
|
||||
right_end,
|
||||
right_section.size
|
||||
));
|
||||
}
|
||||
let left_range = left_start as usize..left_end as usize;
|
||||
let right_range = right_start as usize..right_end as usize;
|
||||
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,
|
||||
left_section_idx,
|
||||
right_section_idx,
|
||||
left_range,
|
||||
right_range,
|
||||
);
|
||||
|
||||
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;
|
||||
for (diff_kind, left_reloc, right_reloc) in reloc_diffs {
|
||||
let reloc_diff_len = match (left_reloc, right_reloc) {
|
||||
(None, None) => unreachable!(),
|
||||
(None, Some(right_reloc)) => {
|
||||
right_obj.arch.data_reloc_size(right_reloc.relocation.flags)
|
||||
}
|
||||
(Some(left_reloc), _) => left_obj.arch.data_reloc_size(left_reloc.relocation.flags),
|
||||
};
|
||||
total_reloc_bytes += reloc_diff_len;
|
||||
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..left_reloc.relocation.address + len as u64;
|
||||
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..right_reloc.relocation.address + len as u64;
|
||||
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;
|
||||
// Adjust the overall match ratio to include relocation differences.
|
||||
// We calculate it so that bytes that contain a relocation are counted twice: once for the
|
||||
// byte's raw value, and once for its relocation.
|
||||
// e.g. An 8 byte symbol that has 8 matching raw bytes and a single 4 byte relocation that
|
||||
// doesn't match would show as 66% (weighted average of 100% and 0%).
|
||||
match_ratio = ((bytes_match_ratio * (left_data.len() as f32))
|
||||
+ (relocs_match_ratio * total_reloc_bytes as f32))
|
||||
/ (left_data.len() + total_reloc_bytes) as f32;
|
||||
}
|
||||
}
|
||||
|
||||
left_reloc_diffs
|
||||
.sort_by(|a, b| a.range.start.cmp(&b.range.start).then(a.range.end.cmp(&b.range.end)));
|
||||
right_reloc_diffs
|
||||
.sort_by(|a, b| a.range.start.cmp(&b.range.start).then(a.range.end.cmp(&b.range.end)));
|
||||
|
||||
let match_percent = match_ratio * 100.0;
|
||||
let left_rows = build_data_diff_rows(&left_data_diff, &left_reloc_diffs, left_symbol.address);
|
||||
let right_rows =
|
||||
build_data_diff_rows(&right_data_diff, &right_reloc_diffs, right_symbol.address);
|
||||
|
||||
Ok((
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: left_symbol_ref,
|
||||
diff_symbol: Some(right_symbol_ref),
|
||||
instructions: vec![],
|
||||
SymbolDiff {
|
||||
target_symbol: Some(right_symbol_idx),
|
||||
match_percent: Some(match_percent),
|
||||
diff_score: None,
|
||||
data_rows: left_rows,
|
||||
..Default::default()
|
||||
},
|
||||
ObjSymbolDiff {
|
||||
symbol_ref: right_symbol_ref,
|
||||
diff_symbol: Some(left_symbol_ref),
|
||||
instructions: vec![],
|
||||
SymbolDiff {
|
||||
target_symbol: Some(left_symbol_idx),
|
||||
match_percent: Some(match_percent),
|
||||
diff_score: None,
|
||||
data_rows: right_rows,
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -174,49 +509,150 @@ pub fn diff_data_symbol(
|
||||
/// Compares a section of two object files.
|
||||
/// This essentially adds up the match percentage of each symbol in the section.
|
||||
pub fn diff_generic_section(
|
||||
left: &ObjSection,
|
||||
_right: &ObjSection,
|
||||
left_diff: &ObjSectionDiff,
|
||||
_right_diff: &ObjSectionDiff,
|
||||
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
||||
let match_percent = if left_diff.symbols.iter().all(|d| d.match_percent == Some(100.0)) {
|
||||
left_obj: &Object,
|
||||
_right_obj: &Object,
|
||||
left_diff: &ObjectDiff,
|
||||
_right_diff: &ObjectDiff,
|
||||
left_section_idx: usize,
|
||||
_right_section_idx: usize,
|
||||
) -> Result<(SectionDiff, SectionDiff)> {
|
||||
let match_percent = if symbols_matching_section(&left_obj.symbols, left_section_idx)
|
||||
.map(|(i, _)| &left_diff.symbols[i])
|
||||
.all(|d| d.match_percent == Some(100.0))
|
||||
{
|
||||
100.0 // Avoid fp precision issues
|
||||
} else {
|
||||
left.symbols
|
||||
.iter()
|
||||
.zip(left_diff.symbols.iter())
|
||||
.map(|(s, d)| d.match_percent.unwrap_or(0.0) * s.size as f32)
|
||||
.sum::<f32>()
|
||||
/ left.size as f32
|
||||
let (matched, total) = symbols_matching_section(&left_obj.symbols, left_section_idx)
|
||||
.map(|(i, s)| (s, &left_diff.symbols[i]))
|
||||
.fold((0.0, 0.0), |(matched, total), (s, d)| {
|
||||
(matched + d.match_percent.unwrap_or(0.0) * s.size as f32, total + s.size as f32)
|
||||
});
|
||||
if total == 0.0 { 100.0 } else { matched / total }
|
||||
};
|
||||
Ok((
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
SectionDiff { match_percent: Some(match_percent), data_diff: vec![], reloc_diff: vec![] },
|
||||
SectionDiff { match_percent: None, data_diff: vec![], reloc_diff: vec![] },
|
||||
))
|
||||
}
|
||||
|
||||
pub fn no_diff_bss_section() -> Result<SectionDiff> {
|
||||
Ok(SectionDiff { match_percent: Some(0.0), data_diff: vec![], reloc_diff: vec![] })
|
||||
}
|
||||
|
||||
/// Compare the addresses and sizes of each symbol in the BSS sections.
|
||||
pub fn diff_bss_section(
|
||||
left: &ObjSection,
|
||||
right: &ObjSection,
|
||||
left_diff: &ObjSectionDiff,
|
||||
right_diff: &ObjSectionDiff,
|
||||
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
||||
let left_sizes = left.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
|
||||
let right_sizes = right.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
|
||||
let ops = capture_diff_slices_deadline(Algorithm::Patience, &left_sizes, &right_sizes, None);
|
||||
left_obj: &Object,
|
||||
right_obj: &Object,
|
||||
left_diff: &ObjectDiff,
|
||||
right_diff: &ObjectDiff,
|
||||
left_section_idx: usize,
|
||||
right_section_idx: usize,
|
||||
) -> Result<(SectionDiff, SectionDiff)> {
|
||||
let left_section = &left_obj.sections[left_section_idx];
|
||||
let left_sizes = symbols_matching_section(&left_obj.symbols, left_section_idx)
|
||||
.filter_map(|(_, s)| s.address.checked_sub(left_section.address).map(|a| (a, s.size)))
|
||||
.collect::<Vec<_>>();
|
||||
let right_section = &right_obj.sections[right_section_idx];
|
||||
let right_sizes = symbols_matching_section(&right_obj.symbols, right_section_idx)
|
||||
.filter_map(|(_, s)| s.address.checked_sub(right_section.address).map(|a| (a, s.size)))
|
||||
.collect::<Vec<_>>();
|
||||
let ops = capture_diff_slices(Algorithm::Patience, &left_sizes, &right_sizes);
|
||||
let mut match_percent = get_diff_ratio(&ops, left_sizes.len(), right_sizes.len()) * 100.0;
|
||||
|
||||
// Use the highest match percent between two options:
|
||||
// - Left symbols matching right symbols by name
|
||||
// - Diff of the addresses and sizes of each symbol
|
||||
let (generic_diff, _) = diff_generic_section(left, right, left_diff, right_diff)?;
|
||||
let (generic_diff, _) = diff_generic_section(
|
||||
left_obj,
|
||||
right_obj,
|
||||
left_diff,
|
||||
right_diff,
|
||||
left_section_idx,
|
||||
right_section_idx,
|
||||
)?;
|
||||
if generic_diff.match_percent.unwrap_or(-1.0) > match_percent {
|
||||
match_percent = generic_diff.match_percent.unwrap();
|
||||
}
|
||||
|
||||
Ok((
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
SectionDiff { match_percent: Some(match_percent), data_diff: vec![], reloc_diff: vec![] },
|
||||
SectionDiff { match_percent: None, data_diff: vec![], reloc_diff: vec![] },
|
||||
))
|
||||
}
|
||||
|
||||
fn symbols_matching_section(
|
||||
symbols: &[Symbol],
|
||||
section_idx: usize,
|
||||
) -> impl Iterator<Item = (usize, &Symbol)> + '_ {
|
||||
symbols.iter().enumerate().filter(move |(_, s)| {
|
||||
s.section == Some(section_idx)
|
||||
&& s.kind != SymbolKind::Section
|
||||
&& s.size > 0
|
||||
&& !s.flags.contains(SymbolFlag::Ignored)
|
||||
})
|
||||
}
|
||||
|
||||
pub const BYTES_PER_ROW: usize = 16;
|
||||
|
||||
fn build_data_diff_row(
|
||||
data_diffs: &[DataDiff],
|
||||
reloc_diffs: &[DataRelocationDiff],
|
||||
symbol_address: u64,
|
||||
row_index: usize,
|
||||
) -> DataDiffRow {
|
||||
let row_start = row_index * BYTES_PER_ROW;
|
||||
let row_end = row_start + BYTES_PER_ROW;
|
||||
let mut row_diff = DataDiffRow {
|
||||
address: symbol_address + row_start as u64,
|
||||
segments: Vec::new(),
|
||||
relocations: Vec::new(),
|
||||
};
|
||||
|
||||
// Collect all segments that overlap with this row
|
||||
let mut current_offset = 0;
|
||||
for diff in data_diffs {
|
||||
let diff_end = current_offset + diff.size;
|
||||
if current_offset < row_end && diff_end > row_start {
|
||||
let start_in_diff = row_start.saturating_sub(current_offset);
|
||||
let end_in_diff = row_end.min(diff_end) - current_offset;
|
||||
if start_in_diff < end_in_diff {
|
||||
let data_slice = if diff.data.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
diff.data[start_in_diff..end_in_diff.min(diff.data.len())].to_vec()
|
||||
};
|
||||
row_diff.segments.push(DataDiff {
|
||||
data: data_slice,
|
||||
kind: diff.kind,
|
||||
size: end_in_diff - start_in_diff,
|
||||
});
|
||||
}
|
||||
}
|
||||
current_offset = diff_end;
|
||||
if current_offset >= row_start + BYTES_PER_ROW {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all relocations that overlap with this row
|
||||
let row_end_absolute = row_diff.address + BYTES_PER_ROW as u64;
|
||||
row_diff.relocations = reloc_diffs
|
||||
.iter()
|
||||
.filter(|rd| rd.range.start < row_end_absolute && rd.range.end > row_diff.address)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
row_diff
|
||||
}
|
||||
|
||||
fn build_data_diff_rows(
|
||||
segments: &[DataDiff],
|
||||
relocations: &[DataRelocationDiff],
|
||||
symbol_address: u64,
|
||||
) -> Vec<DataDiffRow> {
|
||||
let total_len = segments.iter().map(|s| s.size as u64).sum::<u64>();
|
||||
let num_rows = total_len.div_ceil(BYTES_PER_ROW as u64) as usize;
|
||||
(0..num_rows)
|
||||
.map(|row_index| build_data_diff_row(segments, relocations, symbol_address, row_index))
|
||||
.collect()
|
||||
}
|
||||
|
||||
49
objdiff-core/src/diff/demangler.rs
Normal file
49
objdiff-core/src/diff/demangler.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::diff::Demangler;
|
||||
|
||||
#[cfg(feature = "demangler")]
|
||||
impl Demangler {
|
||||
pub fn demangle(&self, name: &str) -> Option<String> {
|
||||
match self {
|
||||
Demangler::None => None,
|
||||
Demangler::Codewarrior => Self::demangle_codewarrior(name),
|
||||
Demangler::Msvc => Self::demangle_msvc(name),
|
||||
Demangler::Itanium => Self::demangle_itanium(name),
|
||||
Demangler::GnuLegacy => Self::demangle_gnu_legacy(name),
|
||||
Demangler::Auto => {
|
||||
// Try to guess
|
||||
if name.starts_with('?') {
|
||||
Self::demangle_msvc(name)
|
||||
} else {
|
||||
Self::demangle_codewarrior(name)
|
||||
.or_else(|| Self::demangle_gnu_legacy(name))
|
||||
.or_else(|| Self::demangle_itanium(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn demangle_codewarrior(name: &str) -> Option<String> {
|
||||
cwdemangle::demangle(name, &cwdemangle::DemangleOptions::default())
|
||||
}
|
||||
|
||||
fn demangle_msvc(name: &str) -> Option<String> {
|
||||
msvc_demangler::demangle(name, msvc_demangler::DemangleFlags::llvm()).ok()
|
||||
}
|
||||
|
||||
fn demangle_itanium(name: &str) -> Option<String> {
|
||||
let name = name.trim_start_matches('.');
|
||||
cpp_demangle::Symbol::new(name).ok().and_then(|s| s.demangle().ok())
|
||||
}
|
||||
|
||||
fn demangle_gnu_legacy(name: &str) -> Option<String> {
|
||||
let name = name.trim_start_matches('.');
|
||||
gnuv2_demangle::demangle(name, &gnuv2_demangle::DemangleConfig::new()).ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "demangler"))]
|
||||
impl Demangler {
|
||||
pub fn demangle(&self, _name: &str) -> Option<String> { None }
|
||||
}
|
||||
@@ -1,16 +1,32 @@
|
||||
use std::cmp::Ordering;
|
||||
use alloc::{
|
||||
borrow::Cow,
|
||||
collections::BTreeSet,
|
||||
format,
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
};
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::{
|
||||
diff::{ObjInsArgDiff, ObjInsDiff},
|
||||
obj::{ObjInsArg, ObjInsArgValue, ObjReloc, ObjSymbol},
|
||||
diff::{
|
||||
DataDiffKind, DataDiffRow, DiffObjConfig, InstructionDiffKind, InstructionDiffRow,
|
||||
ObjectDiff, SymbolDiff, data::resolve_relocation,
|
||||
},
|
||||
obj::{
|
||||
FlowAnalysisValue, InstructionArg, InstructionArgValue, Object, ParsedInstruction,
|
||||
ResolvedInstructionRef, ResolvedRelocation, SectionFlag, SectionKind, Symbol, SymbolFlag,
|
||||
SymbolKind,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DiffText<'a> {
|
||||
/// Basic text
|
||||
Basic(&'a str),
|
||||
/// Colored text
|
||||
BasicColor(&'a str, usize),
|
||||
/// Line number
|
||||
Line(u32),
|
||||
/// Instruction address
|
||||
@@ -18,86 +34,294 @@ pub enum DiffText<'a> {
|
||||
/// Instruction mnemonic
|
||||
Opcode(&'a str, u16),
|
||||
/// Instruction argument
|
||||
Argument(&'a ObjInsArgValue, Option<&'a ObjInsArgDiff>),
|
||||
Argument(InstructionArgValue<'a>),
|
||||
/// Branch destination
|
||||
BranchDest(u64, Option<&'a ObjInsArgDiff>),
|
||||
BranchDest(u64),
|
||||
/// Symbol name
|
||||
Symbol(&'a ObjSymbol),
|
||||
Symbol(&'a Symbol),
|
||||
/// Relocation addend
|
||||
Addend(i64),
|
||||
/// Number of spaces
|
||||
Spacing(usize),
|
||||
Spacing(u8),
|
||||
/// End of line
|
||||
Eol,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash)]
|
||||
pub enum DiffTextColor {
|
||||
#[default]
|
||||
Normal, // Grey
|
||||
Dim, // Dark grey
|
||||
Bright, // White
|
||||
DataFlow, // Light blue
|
||||
Replace, // Blue
|
||||
Delete, // Red
|
||||
Insert, // Green
|
||||
Rotating(u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DiffTextSegment<'a> {
|
||||
pub text: DiffText<'a>,
|
||||
pub color: DiffTextColor,
|
||||
pub pad_to: u8,
|
||||
}
|
||||
|
||||
impl<'a> DiffTextSegment<'a> {
|
||||
#[inline(always)]
|
||||
pub fn basic(text: &'a str, color: DiffTextColor) -> Self {
|
||||
Self { text: DiffText::Basic(text), color, pad_to: 0 }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn spacing(spaces: u8) -> Self {
|
||||
Self { text: DiffText::Spacing(spaces), color: DiffTextColor::Normal, pad_to: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
const EOL_SEGMENT: DiffTextSegment<'static> =
|
||||
DiffTextSegment { text: DiffText::Eol, color: DiffTextColor::Normal, pad_to: 0 };
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub enum HighlightKind {
|
||||
#[default]
|
||||
None,
|
||||
Opcode(u16),
|
||||
Arg(ObjInsArgValue),
|
||||
Argument(InstructionArgValue<'static>),
|
||||
Symbol(String),
|
||||
Address(u64),
|
||||
}
|
||||
|
||||
pub fn display_diff<E>(
|
||||
ins_diff: &ObjInsDiff,
|
||||
base_addr: u64,
|
||||
mut cb: impl FnMut(DiffText) -> Result<(), E>,
|
||||
) -> Result<(), E> {
|
||||
let Some(ins) = &ins_diff.ins else {
|
||||
cb(DiffText::Eol)?;
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum InstructionPart<'a> {
|
||||
Basic(Cow<'a, str>),
|
||||
Opcode(Cow<'a, str>, u16),
|
||||
Arg(InstructionArg<'a>),
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl<'a> InstructionPart<'a> {
|
||||
#[inline(always)]
|
||||
pub fn basic<T>(s: T) -> Self
|
||||
where T: Into<Cow<'a, str>> {
|
||||
InstructionPart::Basic(s.into())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn opcode<T>(s: T, o: u16) -> Self
|
||||
where T: Into<Cow<'a, str>> {
|
||||
InstructionPart::Opcode(s.into(), o)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn opaque<T>(s: T) -> Self
|
||||
where T: Into<Cow<'a, str>> {
|
||||
InstructionPart::Arg(InstructionArg::Value(InstructionArgValue::Opaque(s.into())))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn signed<T>(v: T) -> InstructionPart<'static>
|
||||
where T: Into<i64> {
|
||||
InstructionPart::Arg(InstructionArg::Value(InstructionArgValue::Signed(v.into())))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn unsigned<T>(v: T) -> InstructionPart<'static>
|
||||
where T: Into<u64> {
|
||||
InstructionPart::Arg(InstructionArg::Value(InstructionArgValue::Unsigned(v.into())))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn branch_dest<T>(v: T) -> InstructionPart<'static>
|
||||
where T: Into<u64> {
|
||||
InstructionPart::Arg(InstructionArg::BranchDest(v.into()))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reloc() -> InstructionPart<'static> { InstructionPart::Arg(InstructionArg::Reloc) }
|
||||
|
||||
#[inline(always)]
|
||||
pub fn separator() -> InstructionPart<'static> { InstructionPart::Separator }
|
||||
|
||||
pub fn into_static(self) -> InstructionPart<'static> {
|
||||
match self {
|
||||
InstructionPart::Basic(s) => InstructionPart::Basic(Cow::Owned(s.into_owned())),
|
||||
InstructionPart::Opcode(s, o) => InstructionPart::Opcode(Cow::Owned(s.into_owned()), o),
|
||||
InstructionPart::Arg(a) => InstructionPart::Arg(a.into_static()),
|
||||
InstructionPart::Separator => InstructionPart::Separator,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_row(
|
||||
obj: &Object,
|
||||
symbol_index: usize,
|
||||
ins_row: &InstructionDiffRow,
|
||||
diff_config: &DiffObjConfig,
|
||||
mut cb: impl FnMut(DiffTextSegment) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let Some(ins_ref) = ins_row.ins_ref else {
|
||||
cb(EOL_SEGMENT)?;
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(line) = ins.line {
|
||||
cb(DiffText::Line(line))?;
|
||||
let Some(resolved) = obj.resolve_instruction_ref(symbol_index, ins_ref) else {
|
||||
cb(DiffTextSegment::basic("<invalid>", DiffTextColor::Delete))?;
|
||||
cb(EOL_SEGMENT)?;
|
||||
return Ok(());
|
||||
};
|
||||
let base_color = match ins_row.kind {
|
||||
InstructionDiffKind::Replace => DiffTextColor::Replace,
|
||||
InstructionDiffKind::Delete => DiffTextColor::Delete,
|
||||
InstructionDiffKind::Insert => DiffTextColor::Insert,
|
||||
_ => DiffTextColor::Normal,
|
||||
};
|
||||
if let Some(line) = resolved.section.line_info.range(..=ins_ref.address).last().map(|(_, &b)| b)
|
||||
{
|
||||
cb(DiffTextSegment { text: DiffText::Line(line), color: DiffTextColor::Dim, pad_to: 5 })?;
|
||||
}
|
||||
cb(DiffText::Address(ins.address - base_addr))?;
|
||||
if let Some(branch) = &ins_diff.branch_from {
|
||||
cb(DiffText::BasicColor(" ~> ", branch.branch_idx))?;
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::Address(ins_ref.address.saturating_sub(resolved.symbol.address)),
|
||||
color: DiffTextColor::Dim,
|
||||
pad_to: 5,
|
||||
})?;
|
||||
if let Some(branch) = &ins_row.branch_from {
|
||||
cb(DiffTextSegment::basic(" ~> ", DiffTextColor::Rotating(branch.branch_idx as u8)))?;
|
||||
} else {
|
||||
cb(DiffText::Spacing(4))?;
|
||||
cb(DiffTextSegment::spacing(4))?;
|
||||
}
|
||||
cb(DiffText::Opcode(&ins.mnemonic, ins.op))?;
|
||||
for (i, arg) in ins.args.iter().enumerate() {
|
||||
if i == 0 {
|
||||
cb(DiffText::Spacing(1))?;
|
||||
let mut arg_idx = 0;
|
||||
let mut displayed_relocation = false;
|
||||
let analysis_result = if diff_config.show_data_flow {
|
||||
obj.get_flow_analysis_result(resolved.symbol)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
obj.arch.display_instruction(resolved, diff_config, &mut |part| match part {
|
||||
InstructionPart::Basic(text) => {
|
||||
if text.chars().all(|c| c == ' ') {
|
||||
cb(DiffTextSegment::spacing(text.len() as u8))
|
||||
} else {
|
||||
cb(DiffTextSegment::basic(&text, base_color))
|
||||
}
|
||||
}
|
||||
let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref());
|
||||
match arg {
|
||||
ObjInsArg::PlainText(s) => {
|
||||
cb(DiffText::Basic(s))?;
|
||||
InstructionPart::Opcode(mnemonic, opcode) => cb(DiffTextSegment {
|
||||
text: DiffText::Opcode(mnemonic.as_ref(), opcode),
|
||||
color: match ins_row.kind {
|
||||
InstructionDiffKind::OpMismatch => DiffTextColor::Replace,
|
||||
_ => base_color,
|
||||
},
|
||||
pad_to: 10,
|
||||
}),
|
||||
InstructionPart::Arg(arg) => {
|
||||
let diff_index = ins_row.arg_diff.get(arg_idx).copied().unwrap_or_default();
|
||||
arg_idx += 1;
|
||||
if arg == InstructionArg::Reloc {
|
||||
displayed_relocation = true;
|
||||
}
|
||||
ObjInsArg::Arg(v) => {
|
||||
cb(DiffText::Argument(v, diff))?;
|
||||
}
|
||||
ObjInsArg::Reloc => {
|
||||
display_reloc_name(ins.reloc.as_ref().unwrap(), &mut cb)?;
|
||||
}
|
||||
ObjInsArg::BranchDest(dest) => {
|
||||
if let Some(dest) = dest.checked_sub(base_addr) {
|
||||
cb(DiffText::BranchDest(dest, diff))?;
|
||||
} else {
|
||||
cb(DiffText::Basic("<unknown>"))?;
|
||||
let data_flow_value =
|
||||
analysis_result.and_then(|result|
|
||||
result.get_argument_value_at_address(
|
||||
ins_ref.address, (arg_idx - 1) as u8));
|
||||
match (arg, data_flow_value, resolved.ins_ref.branch_dest) {
|
||||
// If we have a flow analysis result, always use that over anything else.
|
||||
(InstructionArg::Value(_) | InstructionArg::Reloc, Some(FlowAnalysisValue::Text(text)), _) => {
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::Argument(InstructionArgValue::Opaque(Cow::Borrowed(text))),
|
||||
color: DiffTextColor::DataFlow,
|
||||
pad_to: 0,
|
||||
})
|
||||
},
|
||||
(InstructionArg::Value(value), None, _) => {
|
||||
let color = diff_index
|
||||
.get()
|
||||
.map_or(base_color, |i| DiffTextColor::Rotating(i as u8));
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::Argument(value),
|
||||
color,
|
||||
pad_to: 0,
|
||||
})
|
||||
},
|
||||
(InstructionArg::Reloc, _, None) => {
|
||||
let resolved = resolved.relocation.unwrap();
|
||||
let color = diff_index
|
||||
.get()
|
||||
.map_or(DiffTextColor::Bright, |i| DiffTextColor::Rotating(i as u8));
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::Symbol(resolved.symbol),
|
||||
color,
|
||||
pad_to: 0,
|
||||
})?;
|
||||
if resolved.relocation.addend != 0 {
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::Addend(resolved.relocation.addend),
|
||||
color,
|
||||
pad_to: 0,
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
(InstructionArg::BranchDest(dest), _, _) |
|
||||
// If the relocation was resolved to a branch destination, emit that instead.
|
||||
(InstructionArg::Reloc, _, Some(dest)) => {
|
||||
if let Some(addr) = dest.checked_sub(resolved.symbol.address) {
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::BranchDest(addr),
|
||||
color: diff_index
|
||||
.get()
|
||||
.map_or(base_color, |i| DiffTextColor::Rotating(i as u8)),
|
||||
pad_to: 0,
|
||||
})
|
||||
} else {
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::Argument(InstructionArgValue::Opaque(Cow::Borrowed(
|
||||
"<invalid>",
|
||||
))),
|
||||
color: diff_index
|
||||
.get()
|
||||
.map_or(base_color, |i| DiffTextColor::Rotating(i as u8)),
|
||||
pad_to: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
InstructionPart::Separator => {
|
||||
cb(DiffTextSegment::basic(diff_config.separator(), base_color))
|
||||
}
|
||||
})?;
|
||||
// Fallback for relocation that wasn't displayed
|
||||
if resolved.relocation.is_some() && !displayed_relocation {
|
||||
cb(DiffTextSegment::basic(" <", base_color))?;
|
||||
let resolved = resolved.relocation.unwrap();
|
||||
let diff_index = ins_row.arg_diff.get(arg_idx).copied().unwrap_or_default();
|
||||
let color =
|
||||
diff_index.get().map_or(DiffTextColor::Bright, |i| DiffTextColor::Rotating(i as u8));
|
||||
cb(DiffTextSegment { text: DiffText::Symbol(resolved.symbol), color, pad_to: 0 })?;
|
||||
if resolved.relocation.addend != 0 {
|
||||
cb(DiffTextSegment {
|
||||
text: DiffText::Addend(resolved.relocation.addend),
|
||||
color,
|
||||
pad_to: 0,
|
||||
})?;
|
||||
}
|
||||
cb(DiffTextSegment::basic(">", base_color))?;
|
||||
}
|
||||
if let Some(branch) = &ins_diff.branch_to {
|
||||
cb(DiffText::BasicColor(" ~>", branch.branch_idx))?;
|
||||
if let Some(branch) = &ins_row.branch_to {
|
||||
cb(DiffTextSegment::basic(" ~>", DiffTextColor::Rotating(branch.branch_idx as u8)))?;
|
||||
}
|
||||
cb(DiffText::Eol)?;
|
||||
cb(EOL_SEGMENT)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_reloc_name<E>(
|
||||
reloc: &ObjReloc,
|
||||
mut cb: impl FnMut(DiffText) -> Result<(), E>,
|
||||
) -> Result<(), E> {
|
||||
cb(DiffText::Symbol(&reloc.target))?;
|
||||
match reloc.target.addend.cmp(&0i64) {
|
||||
Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.target.addend))),
|
||||
Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.target.addend))),
|
||||
_ => Ok(()),
|
||||
impl PartialEq<HighlightKind> for HighlightKind {
|
||||
fn eq(&self, other: &HighlightKind) -> bool {
|
||||
match (self, other) {
|
||||
(HighlightKind::Opcode(a), HighlightKind::Opcode(b)) => a == b,
|
||||
(HighlightKind::Argument(a), HighlightKind::Argument(b)) => a.loose_eq(b),
|
||||
(HighlightKind::Symbol(a), HighlightKind::Symbol(b)) => a == b,
|
||||
(HighlightKind::Address(a), HighlightKind::Address(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,11 +329,9 @@ impl PartialEq<DiffText<'_>> for HighlightKind {
|
||||
fn eq(&self, other: &DiffText) -> bool {
|
||||
match (self, other) {
|
||||
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b,
|
||||
(HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b),
|
||||
(HighlightKind::Argument(a), DiffText::Argument(b)) => a.loose_eq(b),
|
||||
(HighlightKind::Symbol(a), DiffText::Symbol(b)) => a == &b.name,
|
||||
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b, _)) => {
|
||||
a == b
|
||||
}
|
||||
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -119,14 +341,537 @@ impl PartialEq<HighlightKind> for DiffText<'_> {
|
||||
fn eq(&self, other: &HighlightKind) -> bool { other.eq(self) }
|
||||
}
|
||||
|
||||
impl From<DiffText<'_>> for HighlightKind {
|
||||
fn from(value: DiffText<'_>) -> Self {
|
||||
impl From<&DiffText<'_>> for HighlightKind {
|
||||
fn from(value: &DiffText<'_>) -> Self {
|
||||
match value {
|
||||
DiffText::Opcode(_, op) => HighlightKind::Opcode(op),
|
||||
DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()),
|
||||
DiffText::Opcode(_, op) => HighlightKind::Opcode(*op),
|
||||
DiffText::Argument(arg) => HighlightKind::Argument(arg.to_static()),
|
||||
DiffText::Symbol(sym) => HighlightKind::Symbol(sym.name.to_string()),
|
||||
DiffText::Address(addr) | DiffText::BranchDest(addr, _) => HighlightKind::Address(addr),
|
||||
DiffText::Address(addr) | DiffText::BranchDest(addr) => HighlightKind::Address(*addr),
|
||||
_ => HighlightKind::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ContextItem {
|
||||
Copy { value: String, label: Option<String> },
|
||||
Navigate { label: String, symbol_index: usize, kind: SymbolNavigationKind },
|
||||
Separator,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
pub enum SymbolNavigationKind {
|
||||
#[default]
|
||||
Normal,
|
||||
Extab,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
pub enum HoverItemColor {
|
||||
#[default]
|
||||
Normal, // Gray
|
||||
Emphasized, // White
|
||||
Special, // Blue
|
||||
Delete, // Red
|
||||
Insert, // Green
|
||||
}
|
||||
|
||||
pub enum HoverItem {
|
||||
Text { label: String, value: String, color: HoverItemColor },
|
||||
Separator,
|
||||
}
|
||||
|
||||
pub fn symbol_context(obj: &Object, symbol_index: usize) -> Vec<ContextItem> {
|
||||
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 {
|
||||
out.push(ContextItem::Copy { value: name.clone(), label: None });
|
||||
}
|
||||
if symbol.section.is_some()
|
||||
&& let Some(address) = symbol.virtual_address
|
||||
{
|
||||
out.push(ContextItem::Copy {
|
||||
value: format!("{address:x}"),
|
||||
label: Some("virtual address".to_string()),
|
||||
});
|
||||
}
|
||||
out.append(&mut obj.arch.symbol_context(obj, symbol_index));
|
||||
out
|
||||
}
|
||||
|
||||
pub fn symbol_hover(
|
||||
obj: &Object,
|
||||
symbol_index: usize,
|
||||
addend: i64,
|
||||
override_color: Option<HoverItemColor>,
|
||||
) -> Vec<HoverItem> {
|
||||
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),
|
||||
_ => String::new(),
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
out.push(HoverItem::Text {
|
||||
label: "Name".into(),
|
||||
value: format!("{}{}", symbol.name, addend_str),
|
||||
color: override_color.clone().unwrap_or_default(),
|
||||
});
|
||||
if let Some(demangled_name) = &symbol.demangled_name {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Demangled".into(),
|
||||
value: demangled_name.into(),
|
||||
color: override_color.clone().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
if let Some(section) = symbol.section {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Section".into(),
|
||||
value: obj.sections[section].name.clone(),
|
||||
color: override_color.clone().unwrap_or_default(),
|
||||
});
|
||||
out.push(HoverItem::Text {
|
||||
label: "Address".into(),
|
||||
value: format!("{:x}{}", symbol.address, addend_str),
|
||||
color: override_color.clone().unwrap_or_default(),
|
||||
});
|
||||
if symbol.flags.contains(SymbolFlag::SizeInferred) {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Size".into(),
|
||||
value: format!("{:x} (inferred)", symbol.size),
|
||||
color: override_color.clone().unwrap_or_default(),
|
||||
});
|
||||
} else {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Size".into(),
|
||||
value: format!("{:x}", symbol.size),
|
||||
color: override_color.clone().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
if let Some(align) = symbol.align {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Alignment".into(),
|
||||
value: align.get().to_string(),
|
||||
color: override_color.clone().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
if let Some(address) = symbol.virtual_address {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Virtual address".into(),
|
||||
value: format!("{address:x}"),
|
||||
color: override_color.clone().unwrap_or(HoverItemColor::Special),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
out.push(HoverItem::Text {
|
||||
label: Default::default(),
|
||||
value: "Extern".into(),
|
||||
color: HoverItemColor::Emphasized,
|
||||
});
|
||||
}
|
||||
out.append(&mut obj.arch.symbol_hover(obj, symbol_index));
|
||||
out
|
||||
}
|
||||
|
||||
pub fn relocation_context(
|
||||
obj: &Object,
|
||||
reloc: ResolvedRelocation,
|
||||
ins: Option<ResolvedInstructionRef>,
|
||||
) -> Vec<ContextItem> {
|
||||
let mut out = Vec::new();
|
||||
out.append(&mut symbol_context(obj, reloc.relocation.target_symbol));
|
||||
if let Some(ins) = ins {
|
||||
let literals = display_ins_data_literals(obj, ins);
|
||||
if !literals.is_empty() {
|
||||
out.push(ContextItem::Separator);
|
||||
for (literal, label_override) in literals {
|
||||
out.push(ContextItem::Copy { value: literal, label: label_override });
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn data_row_hover(obj: &Object, diff_row: &DataDiffRow) -> Vec<HoverItem> {
|
||||
let mut out = Vec::new();
|
||||
let mut prev_reloc = None;
|
||||
let mut first = true;
|
||||
for reloc_diff in diff_row.relocations.iter() {
|
||||
let reloc = &reloc_diff.reloc;
|
||||
if prev_reloc == Some(reloc) {
|
||||
// Avoid showing consecutive duplicate relocations.
|
||||
// We do this because a single relocation can span across multiple diffs if the
|
||||
// bytes in the relocation changed (e.g. first byte is added, second is unchanged).
|
||||
continue;
|
||||
}
|
||||
prev_reloc = Some(reloc);
|
||||
|
||||
if first {
|
||||
first = false;
|
||||
} else {
|
||||
out.push(HoverItem::Separator);
|
||||
}
|
||||
|
||||
let reloc = resolve_relocation(&obj.symbols, reloc);
|
||||
let color = match reloc_diff.kind {
|
||||
DataDiffKind::None => HoverItemColor::Normal,
|
||||
DataDiffKind::Replace => HoverItemColor::Special,
|
||||
DataDiffKind::Delete => HoverItemColor::Delete,
|
||||
DataDiffKind::Insert => HoverItemColor::Insert,
|
||||
};
|
||||
out.append(&mut relocation_hover(obj, reloc, Some(color)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn data_row_context(obj: &Object, diff_row: &DataDiffRow) -> Vec<ContextItem> {
|
||||
let mut out = Vec::new();
|
||||
let mut prev_reloc = None;
|
||||
for reloc_diff in diff_row.relocations.iter() {
|
||||
let reloc = &reloc_diff.reloc;
|
||||
if prev_reloc == Some(reloc) {
|
||||
// Avoid showing consecutive duplicate relocations.
|
||||
// We do this because a single relocation can span across multiple diffs if the
|
||||
// bytes in the relocation changed (e.g. first byte is added, second is unchanged).
|
||||
continue;
|
||||
}
|
||||
prev_reloc = Some(reloc);
|
||||
|
||||
let reloc = resolve_relocation(&obj.symbols, reloc);
|
||||
out.append(&mut relocation_context(obj, reloc, None));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn relocation_hover(
|
||||
obj: &Object,
|
||||
reloc: ResolvedRelocation,
|
||||
override_color: Option<HoverItemColor>,
|
||||
) -> Vec<HoverItem> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(name) = obj.arch.reloc_name(reloc.relocation.flags) {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Relocation".into(),
|
||||
value: name.to_string(),
|
||||
color: override_color.clone().unwrap_or_default(),
|
||||
});
|
||||
} else {
|
||||
out.push(HoverItem::Text {
|
||||
label: "Relocation".into(),
|
||||
value: format!("<{:?}>", reloc.relocation.flags),
|
||||
color: override_color.clone().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
out.append(&mut symbol_hover(
|
||||
obj,
|
||||
reloc.relocation.target_symbol,
|
||||
reloc.relocation.addend,
|
||||
override_color,
|
||||
));
|
||||
out
|
||||
}
|
||||
|
||||
pub fn instruction_context(
|
||||
obj: &Object,
|
||||
resolved: ResolvedInstructionRef,
|
||||
ins: &ParsedInstruction,
|
||||
) -> Vec<ContextItem> {
|
||||
let mut out = Vec::new();
|
||||
let mut hex_string = String::new();
|
||||
for byte in resolved.code {
|
||||
hex_string.push_str(&format!("{byte:02x}"));
|
||||
}
|
||||
out.push(ContextItem::Copy { value: hex_string, label: Some("instruction bytes".to_string()) });
|
||||
out.append(&mut obj.arch.instruction_context(obj, resolved));
|
||||
if let Some(virtual_address) = resolved.symbol.virtual_address {
|
||||
let offset = resolved.ins_ref.address - resolved.symbol.address;
|
||||
out.push(ContextItem::Copy {
|
||||
value: format!("{:x}", virtual_address + offset),
|
||||
label: Some("virtual address".to_string()),
|
||||
});
|
||||
}
|
||||
for arg in &ins.args {
|
||||
if let InstructionArg::Value(arg) = arg {
|
||||
out.push(ContextItem::Copy { value: arg.to_string(), label: None });
|
||||
match arg {
|
||||
InstructionArgValue::Signed(v) => {
|
||||
out.push(ContextItem::Copy { value: v.to_string(), label: None });
|
||||
}
|
||||
InstructionArgValue::Unsigned(v) => {
|
||||
out.push(ContextItem::Copy { value: v.to_string(), label: None });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(reloc) = resolved.relocation {
|
||||
out.push(ContextItem::Separator);
|
||||
out.append(&mut relocation_context(obj, reloc, Some(resolved)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn instruction_hover(
|
||||
obj: &Object,
|
||||
resolved: ResolvedInstructionRef,
|
||||
ins: &ParsedInstruction,
|
||||
) -> Vec<HoverItem> {
|
||||
let mut out = Vec::new();
|
||||
out.push(HoverItem::Text {
|
||||
label: Default::default(),
|
||||
value: format!("{:02x?}", resolved.code),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
out.append(&mut obj.arch.instruction_hover(obj, resolved));
|
||||
if let Some(virtual_address) = resolved.symbol.virtual_address {
|
||||
let offset = resolved.ins_ref.address - resolved.symbol.address;
|
||||
out.push(HoverItem::Text {
|
||||
label: "Virtual address".into(),
|
||||
value: format!("{:x}", virtual_address + offset),
|
||||
color: HoverItemColor::Special,
|
||||
});
|
||||
}
|
||||
for arg in &ins.args {
|
||||
if let InstructionArg::Value(arg) = arg {
|
||||
match arg {
|
||||
InstructionArgValue::Signed(v) => {
|
||||
out.push(HoverItem::Text {
|
||||
label: Default::default(),
|
||||
value: format!("{arg} == {v}"),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
}
|
||||
InstructionArgValue::Unsigned(v) => {
|
||||
out.push(HoverItem::Text {
|
||||
label: Default::default(),
|
||||
value: format!("{arg} == {v}"),
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(reloc) = resolved.relocation {
|
||||
out.push(HoverItem::Separator);
|
||||
out.append(&mut relocation_hover(obj, reloc, None));
|
||||
let bytes = obj.symbol_data(reloc.relocation.target_symbol).unwrap_or(&[]);
|
||||
if let Some(ty) = obj.arch.guess_data_type(resolved, bytes) {
|
||||
let literals = display_ins_data_literals(obj, resolved);
|
||||
if !literals.is_empty() {
|
||||
out.push(HoverItem::Separator);
|
||||
for (literal, label_override) in literals {
|
||||
out.push(HoverItem::Text {
|
||||
label: label_override.unwrap_or_else(|| ty.to_string()),
|
||||
value: literal,
|
||||
color: HoverItemColor::Normal,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum SymbolFilter<'a> {
|
||||
None,
|
||||
Search(&'a Regex),
|
||||
Mapping(usize, Option<&'a Regex>),
|
||||
}
|
||||
|
||||
fn symbol_matches_filter(
|
||||
symbol: &Symbol,
|
||||
diff: &SymbolDiff,
|
||||
filter: SymbolFilter<'_>,
|
||||
show_hidden_symbols: bool,
|
||||
) -> bool {
|
||||
// Ignore absolute symbols
|
||||
if symbol.section.is_none() && !symbol.flags.contains(SymbolFlag::Common) {
|
||||
return false;
|
||||
}
|
||||
if !show_hidden_symbols
|
||||
&& (symbol.size == 0
|
||||
|| symbol.flags.contains(SymbolFlag::Hidden)
|
||||
|| symbol.flags.contains(SymbolFlag::Ignored))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
match filter {
|
||||
SymbolFilter::None => true,
|
||||
SymbolFilter::Search(regex) => {
|
||||
regex.is_match(&symbol.name)
|
||||
|| symbol.demangled_name.as_deref().is_some_and(|s| regex.is_match(s))
|
||||
}
|
||||
SymbolFilter::Mapping(symbol_ref, regex) => {
|
||||
diff.target_symbol == Some(symbol_ref)
|
||||
&& regex.is_none_or(|r| {
|
||||
r.is_match(&symbol.name)
|
||||
|| symbol.demangled_name.as_deref().is_some_and(|s| r.is_match(s))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct SectionDisplaySymbol {
|
||||
pub symbol: usize,
|
||||
pub is_mapping_symbol: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SectionDisplay {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
pub match_percent: Option<f32>,
|
||||
pub symbols: Vec<SectionDisplaySymbol>,
|
||||
pub kind: SectionKind,
|
||||
}
|
||||
|
||||
pub fn display_sections(
|
||||
obj: &Object,
|
||||
diff: &ObjectDiff,
|
||||
filter: SymbolFilter<'_>,
|
||||
show_hidden_symbols: bool,
|
||||
show_mapped_symbols: bool,
|
||||
reverse_fn_order: bool,
|
||||
) -> Vec<SectionDisplay> {
|
||||
let mut mapping = BTreeSet::new();
|
||||
let is_mapping_symbol = if let SymbolFilter::Mapping(_, _) = filter {
|
||||
for mapping_diff in &diff.mapping_symbols {
|
||||
let symbol = &obj.symbols[mapping_diff.symbol_index];
|
||||
if !symbol_matches_filter(
|
||||
symbol,
|
||||
&mapping_diff.symbol_diff,
|
||||
filter,
|
||||
show_hidden_symbols,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if !show_mapped_symbols {
|
||||
let symbol_diff = &diff.symbols[mapping_diff.symbol_index];
|
||||
if symbol_diff.target_symbol.is_some() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
mapping.insert((symbol.section, mapping_diff.symbol_index));
|
||||
}
|
||||
true
|
||||
} else {
|
||||
for (symbol_idx, (symbol, symbol_diff)) in obj.symbols.iter().zip(&diff.symbols).enumerate()
|
||||
{
|
||||
if !symbol_matches_filter(symbol, symbol_diff, filter, show_hidden_symbols) {
|
||||
continue;
|
||||
}
|
||||
mapping.insert((symbol.section, symbol_idx));
|
||||
}
|
||||
false
|
||||
};
|
||||
let num_sections = mapping.iter().map(|(section_idx, _)| *section_idx).dedup().count();
|
||||
let mut sections = Vec::with_capacity(num_sections);
|
||||
for (section_idx, group) in &mapping.iter().chunk_by(|(section_idx, _)| *section_idx) {
|
||||
let mut symbols = group
|
||||
.map(|&(_, symbol)| SectionDisplaySymbol { symbol, is_mapping_symbol })
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(section_idx) = section_idx {
|
||||
let section = &obj.sections[section_idx];
|
||||
if section.kind == SectionKind::Unknown {
|
||||
// Skip unknown and hidden sections
|
||||
continue;
|
||||
}
|
||||
let section_diff = &diff.sections[section_idx];
|
||||
let reverse_fn_order = section.kind == SectionKind::Code && reverse_fn_order;
|
||||
symbols.sort_by(|a, b| {
|
||||
let a = &obj.symbols[a.symbol];
|
||||
let b = &obj.symbols[b.symbol];
|
||||
section_symbol_sort(a, b)
|
||||
.then_with(|| {
|
||||
if reverse_fn_order {
|
||||
b.address.cmp(&a.address)
|
||||
} else {
|
||||
a.address.cmp(&b.address)
|
||||
}
|
||||
})
|
||||
.then_with(|| a.size.cmp(&b.size))
|
||||
});
|
||||
sections.push(SectionDisplay {
|
||||
id: section.id.clone(),
|
||||
name: if section.flags.contains(SectionFlag::Combined) {
|
||||
format!("{} [combined]", section.name)
|
||||
} else {
|
||||
section.name.clone()
|
||||
},
|
||||
size: section.size,
|
||||
match_percent: section_diff.match_percent,
|
||||
symbols,
|
||||
kind: section.kind,
|
||||
});
|
||||
} else {
|
||||
// Don't sort, preserve order of absolute symbols
|
||||
sections.push(SectionDisplay {
|
||||
id: ".comm".to_string(),
|
||||
name: ".comm".to_string(),
|
||||
size: 0,
|
||||
match_percent: None,
|
||||
symbols,
|
||||
kind: SectionKind::Common,
|
||||
});
|
||||
}
|
||||
}
|
||||
sections.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
sections
|
||||
}
|
||||
|
||||
fn section_symbol_sort(a: &Symbol, b: &Symbol) -> Ordering {
|
||||
if a.kind == SymbolKind::Section {
|
||||
if b.kind != SymbolKind::Section {
|
||||
return Ordering::Less;
|
||||
}
|
||||
} else if b.kind == SymbolKind::Section {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
Ordering::Equal
|
||||
}
|
||||
|
||||
pub fn display_ins_data_labels(obj: &Object, resolved: ResolvedInstructionRef) -> Vec<String> {
|
||||
let Some(reloc) = resolved.relocation else {
|
||||
return Vec::new();
|
||||
};
|
||||
if reloc.relocation.addend < 0 || reloc.relocation.addend as u64 >= reloc.symbol.size {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(data) = obj.symbol_data(reloc.relocation.target_symbol) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let bytes = &data[reloc.relocation.addend as usize..];
|
||||
obj.arch
|
||||
.guess_data_type(resolved, bytes)
|
||||
.map(|ty| ty.display_labels(obj.endianness, bytes))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn display_ins_data_literals(
|
||||
obj: &Object,
|
||||
resolved: ResolvedInstructionRef,
|
||||
) -> Vec<(String, Option<String>)> {
|
||||
let Some(reloc) = resolved.relocation else {
|
||||
return Vec::new();
|
||||
};
|
||||
if reloc.relocation.addend < 0 || reloc.relocation.addend as u64 >= reloc.symbol.size {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(data) = obj.symbol_data(reloc.relocation.target_symbol) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let bytes = &data[reloc.relocation.addend as usize..];
|
||||
obj.arch
|
||||
.guess_data_type(resolved, bytes)
|
||||
.map(|ty| ty.display_literals(obj.endianness, bytes))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
50
objdiff-core/src/jobs/check_update.rs
Normal file
50
objdiff-core/src/jobs/check_update.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::{sync::mpsc::Receiver, task::Waker};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use self_update::{
|
||||
cargo_crate_version,
|
||||
update::{Release, ReleaseUpdate},
|
||||
};
|
||||
|
||||
use crate::jobs::{Job, JobContext, JobResult, JobState, start_job, update_status};
|
||||
|
||||
pub struct CheckUpdateConfig {
|
||||
pub build_updater: fn() -> Result<Box<dyn ReleaseUpdate>>,
|
||||
pub bin_names: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct CheckUpdateResult {
|
||||
pub update_available: bool,
|
||||
pub latest_release: Release,
|
||||
pub found_binary: Option<String>,
|
||||
}
|
||||
|
||||
fn run_check_update(
|
||||
context: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
config: CheckUpdateConfig,
|
||||
) -> Result<Box<CheckUpdateResult>> {
|
||||
update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?;
|
||||
let updater = (config.build_updater)().context("Failed to create release updater")?;
|
||||
let latest_release = updater.get_latest_release()?;
|
||||
let update_available =
|
||||
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
|
||||
// Find the binary name in the release assets
|
||||
let mut found_binary = None;
|
||||
for bin_name in &config.bin_names {
|
||||
if latest_release.assets.iter().any(|a| &a.name == bin_name) {
|
||||
found_binary = Some(bin_name.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
|
||||
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
|
||||
}
|
||||
|
||||
pub fn start_check_update(waker: Waker, config: CheckUpdateConfig) -> JobState {
|
||||
start_job(waker, "Check for updates", Job::CheckUpdate, move |context, cancel| {
|
||||
run_check_update(&context, cancel, config)
|
||||
.map(|result| JobResult::CheckUpdate(Some(result)))
|
||||
})
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
use std::{fs, path::PathBuf, sync::mpsc::Receiver};
|
||||
use std::{fs, sync::mpsc::Receiver, task::Waker};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use const_format::formatcp;
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use typed_path::{Utf8PlatformPathBuf, Utf8UnixPathBuf};
|
||||
|
||||
use crate::{
|
||||
app::AppConfig,
|
||||
jobs::{
|
||||
objdiff::{run_make, BuildConfig, BuildStatus},
|
||||
start_job, update_status, Job, JobContext, JobResult, JobState,
|
||||
},
|
||||
build::{BuildConfig, BuildStatus, run_make},
|
||||
jobs::{Job, JobContext, JobResult, JobState, start_job, update_status},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateScratchConfig {
|
||||
pub build_config: BuildConfig,
|
||||
pub context_path: Option<PathBuf>,
|
||||
pub context_path: Option<Utf8UnixPathBuf>,
|
||||
pub build_context: bool,
|
||||
|
||||
// Scratch fields
|
||||
@@ -22,38 +19,8 @@ pub struct CreateScratchConfig {
|
||||
pub platform: String,
|
||||
pub compiler_flags: String,
|
||||
pub function_name: String,
|
||||
pub target_obj: PathBuf,
|
||||
}
|
||||
|
||||
impl CreateScratchConfig {
|
||||
pub(crate) fn from_config(config: &AppConfig, function_name: String) -> Result<Self> {
|
||||
let Some(selected_obj) = &config.selected_obj else {
|
||||
bail!("No object selected");
|
||||
};
|
||||
let Some(target_path) = &selected_obj.target_path else {
|
||||
bail!("No target path for {}", selected_obj.name);
|
||||
};
|
||||
let Some(scratch_config) = &selected_obj.scratch else {
|
||||
bail!("No scratch configuration for {}", selected_obj.name);
|
||||
};
|
||||
Ok(Self {
|
||||
build_config: BuildConfig::from_config(config),
|
||||
context_path: scratch_config.ctx_path.clone(),
|
||||
build_context: scratch_config.build_ctx,
|
||||
compiler: scratch_config.compiler.clone().unwrap_or_default(),
|
||||
platform: scratch_config.platform.clone().unwrap_or_default(),
|
||||
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
|
||||
function_name,
|
||||
target_obj: target_path.to_path_buf(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_available(config: &AppConfig) -> bool {
|
||||
let Some(selected_obj) = &config.selected_obj else {
|
||||
return false;
|
||||
};
|
||||
selected_obj.target_path.is_some() && selected_obj.scratch.is_some()
|
||||
}
|
||||
pub target_obj: Utf8PlatformPathBuf,
|
||||
pub preset_id: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
@@ -81,38 +48,40 @@ fn run_create_scratch(
|
||||
if let Some(context_path) = &config.context_path {
|
||||
if config.build_context {
|
||||
update_status(status, "Building context".to_string(), 0, 2, &cancel)?;
|
||||
match run_make(&config.build_config, context_path) {
|
||||
match run_make(&config.build_config, context_path.as_ref()) {
|
||||
BuildStatus { success: true, .. } => {}
|
||||
BuildStatus { success: false, stdout, stderr, .. } => {
|
||||
bail!("Failed to build context:\n{stdout}\n{stderr}")
|
||||
}
|
||||
}
|
||||
}
|
||||
let context_path = project_dir.join(context_path);
|
||||
let context_path = project_dir.join(context_path.with_platform_encoding());
|
||||
context = Some(
|
||||
fs::read_to_string(&context_path)
|
||||
.map_err(|e| anyhow!("Failed to read {}: {}", context_path.display(), e))?,
|
||||
.map_err(|e| anyhow!("Failed to read {}: {}", context_path, e))?,
|
||||
);
|
||||
}
|
||||
|
||||
update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
|
||||
let diff_flags = [format!("--disassemble={}", config.function_name)];
|
||||
let diff_flags = serde_json::to_string(&diff_flags).unwrap();
|
||||
let obj_path = project_dir.join(&config.target_obj);
|
||||
let file = reqwest::blocking::multipart::Part::file(&obj_path)
|
||||
.with_context(|| format!("Failed to open {}", obj_path.display()))?;
|
||||
let form = reqwest::blocking::multipart::Form::new()
|
||||
let diff_flags = serde_json::to_string(&diff_flags)?;
|
||||
let file = reqwest::blocking::multipart::Part::file(&config.target_obj)
|
||||
.with_context(|| format!("Failed to open {}", config.target_obj))?;
|
||||
let mut form = reqwest::blocking::multipart::Form::new()
|
||||
.text("compiler", config.compiler.clone())
|
||||
.text("platform", config.platform.clone())
|
||||
.text("compiler_flags", config.compiler_flags.clone())
|
||||
.text("diff_label", config.function_name.clone())
|
||||
.text("diff_flags", diff_flags)
|
||||
.text("context", context.unwrap_or_default())
|
||||
.text("source_code", "// Move related code from Context tab to here")
|
||||
.part("target_obj", file);
|
||||
.text("source_code", "// Move related code from Context tab to here");
|
||||
if let Some(preset) = config.preset_id {
|
||||
form = form.text("preset", preset.to_string());
|
||||
}
|
||||
form = form.part("target_obj", file);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.post(formatcp!("{API_HOST}/api/scratch"))
|
||||
.post(format!("{API_HOST}/api/scratch"))
|
||||
.multipart(form)
|
||||
.send()
|
||||
.map_err(|e| anyhow!("Failed to send request: {}", e))?;
|
||||
@@ -126,8 +95,8 @@ fn run_create_scratch(
|
||||
Ok(Box::from(CreateScratchResult { scratch_url }))
|
||||
}
|
||||
|
||||
pub fn start_create_scratch(ctx: &egui::Context, config: CreateScratchConfig) -> JobState {
|
||||
start_job(ctx, "Create scratch", Job::CreateScratch, move |context, cancel| {
|
||||
pub fn start_create_scratch(waker: Waker, config: CreateScratchConfig) -> JobState {
|
||||
start_job(waker, "Create scratch", Job::CreateScratch, move |context, cancel| {
|
||||
run_create_scratch(&context, cancel, config)
|
||||
.map(|result| JobResult::CreateScratch(Some(result)))
|
||||
})
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::{
|
||||
sync::{
|
||||
Arc, RwLock,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc::{Receiver, Sender, TryRecvError},
|
||||
Arc, RwLock,
|
||||
},
|
||||
task::Waker,
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
@@ -53,7 +54,6 @@ impl JobQueue {
|
||||
}
|
||||
|
||||
/// Returns whether any job is running.
|
||||
#[allow(dead_code)]
|
||||
pub fn any_running(&self) -> bool {
|
||||
self.jobs.iter().any(|job| {
|
||||
if let Some(handle) = &job.handle {
|
||||
@@ -85,20 +85,64 @@ impl JobQueue {
|
||||
/// Clears all finished jobs.
|
||||
pub fn clear_finished(&mut self) {
|
||||
self.jobs.retain(|job| {
|
||||
!(job.should_remove
|
||||
&& job.handle.is_none()
|
||||
&& job.context.status.read().unwrap().error.is_none())
|
||||
!(job.handle.is_none() && job.context.status.read().unwrap().error.is_none())
|
||||
});
|
||||
}
|
||||
|
||||
/// Clears all errored jobs.
|
||||
pub fn clear_errored(&mut self) {
|
||||
self.jobs.retain(|job| job.context.status.read().unwrap().error.is_none());
|
||||
}
|
||||
|
||||
/// Removes a job from the queue given its ID.
|
||||
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
|
||||
|
||||
/// Collects the results of all finished jobs and handles any errors.
|
||||
pub fn collect_results(&mut self) {
|
||||
let mut results = vec![];
|
||||
for (job, result) in self.iter_finished() {
|
||||
match result {
|
||||
Ok(result) => {
|
||||
match result {
|
||||
JobResult::None => {
|
||||
// Job context contains the error
|
||||
}
|
||||
_ => results.push(result),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
|
||||
anyhow::Error::msg(*msg)
|
||||
} else if let Some(msg) = err.downcast_ref::<String>() {
|
||||
anyhow::Error::msg(msg.clone())
|
||||
} else {
|
||||
anyhow::Error::msg("Thread panicked")
|
||||
};
|
||||
let result = job.context.status.write();
|
||||
if let Ok(mut guard) = result {
|
||||
guard.error = Some(err);
|
||||
} else {
|
||||
drop(result);
|
||||
job.context.status = Arc::new(RwLock::new(JobStatus {
|
||||
title: "Error".to_string(),
|
||||
progress_percent: 0.0,
|
||||
progress_items: None,
|
||||
status: String::new(),
|
||||
error: Some(err),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.results.append(&mut results);
|
||||
self.clear_finished();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobContext {
|
||||
pub status: Arc<RwLock<JobStatus>>,
|
||||
pub egui: egui::Context,
|
||||
pub waker: Waker,
|
||||
}
|
||||
|
||||
pub struct JobState {
|
||||
@@ -107,7 +151,6 @@ pub struct JobState {
|
||||
pub handle: Option<JoinHandle<JobResult>>,
|
||||
pub context: JobContext,
|
||||
pub cancel: Sender<()>,
|
||||
pub should_remove: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -127,15 +170,8 @@ 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(
|
||||
ctx: &egui::Context,
|
||||
waker: Waker,
|
||||
title: &str,
|
||||
kind: Job,
|
||||
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
|
||||
@@ -147,23 +183,20 @@ fn start_job(
|
||||
status: String::new(),
|
||||
error: None,
|
||||
}));
|
||||
let context = JobContext { status: status.clone(), egui: ctx.clone() };
|
||||
let context_inner = JobContext { status: status.clone(), egui: ctx.clone() };
|
||||
let context = JobContext { status: status.clone(), waker: waker.clone() };
|
||||
let context_inner = JobContext { status: status.clone(), waker };
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let handle = std::thread::spawn(move || {
|
||||
return match run(context_inner, rx) {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
if let Ok(mut w) = status.write() {
|
||||
w.error = Some(e);
|
||||
}
|
||||
JobResult::None
|
||||
let handle = std::thread::spawn(move || match run(context_inner, rx) {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
if let Ok(mut w) = status.write() {
|
||||
w.error = Some(e);
|
||||
}
|
||||
};
|
||||
JobResult::None
|
||||
}
|
||||
});
|
||||
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
||||
log::info!("Started job {}", id);
|
||||
JobState { id, kind, handle: Some(handle), context, cancel: tx, should_remove: true }
|
||||
JobState { id, kind, handle: Some(handle), context, cancel: tx }
|
||||
}
|
||||
|
||||
fn update_status(
|
||||
@@ -184,6 +217,13 @@ fn update_status(
|
||||
w.status = str;
|
||||
}
|
||||
drop(w);
|
||||
context.egui.request_repaint();
|
||||
context.waker.wake_by_ref();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_cancel(rx: &Receiver<()>) -> bool {
|
||||
match rx.try_recv() {
|
||||
Ok(_) | Err(TryRecvError::Disconnected) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
188
objdiff-core/src/jobs/objdiff.rs
Normal file
188
objdiff-core/src/jobs/objdiff.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use std::{sync::mpsc::Receiver, task::Waker};
|
||||
|
||||
use anyhow::{Error, Result, bail};
|
||||
use time::OffsetDateTime;
|
||||
use typed_path::Utf8PlatformPathBuf;
|
||||
|
||||
use crate::{
|
||||
build::{BuildConfig, BuildStatus, run_make},
|
||||
diff::{DiffObjConfig, DiffSide, MappingConfig, ObjectDiff, diff_objs},
|
||||
jobs::{Job, JobContext, JobResult, JobState, start_job, update_status},
|
||||
obj::{Object, read},
|
||||
};
|
||||
|
||||
pub struct ObjDiffConfig {
|
||||
pub build_config: BuildConfig,
|
||||
pub build_base: bool,
|
||||
pub build_target: bool,
|
||||
pub target_path: Option<Utf8PlatformPathBuf>,
|
||||
pub base_path: Option<Utf8PlatformPathBuf>,
|
||||
pub diff_obj_config: DiffObjConfig,
|
||||
pub mapping_config: MappingConfig,
|
||||
}
|
||||
|
||||
pub struct ObjDiffResult {
|
||||
pub first_status: BuildStatus,
|
||||
pub second_status: BuildStatus,
|
||||
pub first_obj: Option<(Object, ObjectDiff)>,
|
||||
pub second_obj: Option<(Object, ObjectDiff)>,
|
||||
pub time: OffsetDateTime,
|
||||
}
|
||||
|
||||
fn run_build(
|
||||
context: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
config: ObjDiffConfig,
|
||||
) -> Result<Box<ObjDiffResult>> {
|
||||
let mut target_path_rel = None;
|
||||
let mut base_path_rel = None;
|
||||
if config.build_target || config.build_base {
|
||||
let project_dir = config
|
||||
.build_config
|
||||
.project_dir
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::msg("Missing project dir"))?;
|
||||
if let Some(target_path) = &config.target_path {
|
||||
target_path_rel = match target_path.strip_prefix(project_dir) {
|
||||
Ok(p) => Some(p.with_unix_encoding()),
|
||||
Err(_) => {
|
||||
bail!("Target path '{}' doesn't begin with '{}'", target_path, project_dir);
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(base_path) = &config.base_path {
|
||||
base_path_rel = match base_path.strip_prefix(project_dir) {
|
||||
Ok(p) => Some(p.with_unix_encoding()),
|
||||
Err(_) => {
|
||||
bail!("Base path '{}' doesn't begin with '{}'", base_path, project_dir);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
let mut total = 1;
|
||||
if config.build_target && target_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if config.build_base && base_path_rel.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if config.target_path.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
if config.base_path.is_some() {
|
||||
total += 1;
|
||||
}
|
||||
|
||||
let mut step_idx = 0;
|
||||
let mut first_status = match target_path_rel {
|
||||
Some(target_path_rel) if config.build_target => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Building target {target_path_rel}"),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
run_make(&config.build_config, target_path_rel.as_ref())
|
||||
}
|
||||
_ => BuildStatus::default(),
|
||||
};
|
||||
|
||||
let mut second_status = match base_path_rel {
|
||||
Some(base_path_rel) if config.build_base => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Building base {base_path_rel}"),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
run_make(&config.build_config, base_path_rel.as_ref())
|
||||
}
|
||||
_ => BuildStatus::default(),
|
||||
};
|
||||
|
||||
let time = OffsetDateTime::now_utc();
|
||||
|
||||
let first_obj = match &config.target_path {
|
||||
Some(target_path) if first_status.success => {
|
||||
update_status(
|
||||
context,
|
||||
format!("Loading target {target_path}"),
|
||||
step_idx,
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
step_idx += 1;
|
||||
match read::read(target_path.as_ref(), &config.diff_obj_config, DiffSide::Target) {
|
||||
Ok(obj) => Some(obj),
|
||||
Err(e) => {
|
||||
first_status = BuildStatus {
|
||||
success: false,
|
||||
stdout: format!("Loading object '{target_path}'"),
|
||||
stderr: format!("{e:#}"),
|
||||
..Default::default()
|
||||
};
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
step_idx += 1;
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let second_obj = match &config.base_path {
|
||||
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, DiffSide::Base) {
|
||||
Ok(obj) => Some(obj),
|
||||
Err(e) => {
|
||||
second_status = BuildStatus {
|
||||
success: false,
|
||||
stdout: format!("Loading object '{base_path}'"),
|
||||
stderr: format!("{e:#}"),
|
||||
..Default::default()
|
||||
};
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
step_idx += 1;
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
|
||||
step_idx += 1;
|
||||
let result = diff_objs(
|
||||
first_obj.as_ref(),
|
||||
second_obj.as_ref(),
|
||||
None,
|
||||
&config.diff_obj_config,
|
||||
&config.mapping_config,
|
||||
)?;
|
||||
|
||||
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
|
||||
Ok(Box::new(ObjDiffResult {
|
||||
first_status,
|
||||
second_status,
|
||||
first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))),
|
||||
second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))),
|
||||
time,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn start_build(waker: Waker, config: ObjDiffConfig) -> JobState {
|
||||
start_job(waker, "Build", Job::ObjDiff, move |context, cancel| {
|
||||
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
|
||||
})
|
||||
}
|
||||
@@ -3,14 +3,19 @@ use std::{
|
||||
fs::File,
|
||||
path::PathBuf,
|
||||
sync::mpsc::Receiver,
|
||||
task::Waker,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
pub use self_update; // Re-export self_update crate
|
||||
use self_update::update::ReleaseUpdate;
|
||||
|
||||
use crate::{
|
||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||
update::build_updater,
|
||||
};
|
||||
use crate::jobs::{Job, JobContext, JobResult, JobState, start_job, update_status};
|
||||
|
||||
pub struct UpdateConfig {
|
||||
pub build_updater: fn() -> Result<Box<dyn ReleaseUpdate>>,
|
||||
pub bin_name: String,
|
||||
}
|
||||
|
||||
pub struct UpdateResult {
|
||||
pub exe_path: PathBuf,
|
||||
@@ -19,16 +24,15 @@ pub struct UpdateResult {
|
||||
fn run_update(
|
||||
status: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
bin_name: String,
|
||||
config: UpdateConfig,
|
||||
) -> Result<Box<UpdateResult>> {
|
||||
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
|
||||
let updater = build_updater().context("Failed to create release updater")?;
|
||||
let updater = (config.build_updater)().context("Failed to create release updater")?;
|
||||
let latest_release = updater.get_latest_release()?;
|
||||
let asset = latest_release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|a| a.name == bin_name)
|
||||
.ok_or_else(|| anyhow::Error::msg(format!("No release asset for {bin_name}")))?;
|
||||
let asset =
|
||||
latest_release.assets.iter().find(|a| a.name == config.bin_name).ok_or_else(|| {
|
||||
anyhow::Error::msg(format!("No release asset for {}", config.bin_name))
|
||||
})?;
|
||||
|
||||
update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?;
|
||||
let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?;
|
||||
@@ -36,7 +40,7 @@ fn run_update(
|
||||
let tmp_file = File::create(&tmp_path)?;
|
||||
self_update::Download::from_url(&asset.download_url)
|
||||
.set_header(reqwest::header::ACCEPT, "application/octet-stream".parse()?)
|
||||
.download_to(&tmp_file)?;
|
||||
.download_to(tmp_file)?;
|
||||
|
||||
update_status(status, "Extracting release".to_string(), 2, 3, &cancel)?;
|
||||
let tmp_file = tmp_dir.path().join("replacement_tmp");
|
||||
@@ -47,17 +51,16 @@ fn run_update(
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::{fs, os::unix::fs::PermissionsExt};
|
||||
let mut perms = fs::metadata(&target_file)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&target_file, perms)?;
|
||||
fs::set_permissions(&target_file, fs::Permissions::from_mode(0o755))?;
|
||||
}
|
||||
tmp_dir.close()?;
|
||||
|
||||
update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
|
||||
Ok(Box::from(UpdateResult { exe_path: target_file }))
|
||||
}
|
||||
|
||||
pub fn start_update(ctx: &egui::Context, bin_name: String) -> JobState {
|
||||
start_job(ctx, "Update app", Job::Update, move |context, cancel| {
|
||||
run_update(&context, cancel, bin_name).map(JobResult::Update)
|
||||
pub fn start_update(waker: Waker, config: UpdateConfig) -> JobState {
|
||||
start_job(waker, "Update app", Job::Update, move |context, cancel| {
|
||||
run_update(&context, cancel, config).map(JobResult::Update)
|
||||
})
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod arch;
|
||||
#[cfg(feature = "bindings")]
|
||||
pub mod bindings;
|
||||
#[cfg(feature = "build")]
|
||||
pub mod build;
|
||||
#[cfg(feature = "config")]
|
||||
pub mod config;
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod diff;
|
||||
#[cfg(feature = "build")]
|
||||
pub mod jobs;
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod obj;
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod util;
|
||||
|
||||
109
objdiff-core/src/obj/dwarf2.rs
Normal file
109
objdiff-core/src/obj/dwarf2.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use alloc::{borrow::Cow, vec::Vec};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use object::{Object as _, ObjectSection as _};
|
||||
use typed_arena::Arena;
|
||||
|
||||
use crate::obj::{Section, SectionKind};
|
||||
|
||||
/// Parse line information from DWARF 2+ sections.
|
||||
pub(crate) fn parse_line_info_dwarf2(
|
||||
obj_file: &object::File,
|
||||
sections: &mut [Section],
|
||||
) -> Result<()> {
|
||||
let arena_data = Arena::new();
|
||||
let arena_relocations = Arena::new();
|
||||
let endian = match obj_file.endianness() {
|
||||
object::Endianness::Little => gimli::RunTimeEndian::Little,
|
||||
object::Endianness::Big => gimli::RunTimeEndian::Big,
|
||||
};
|
||||
let dwarf = gimli::Dwarf::load(|id: gimli::SectionId| -> Result<_> {
|
||||
load_file_section(id, obj_file, endian, &arena_data, &arena_relocations)
|
||||
})
|
||||
.context("loading DWARF sections")?;
|
||||
|
||||
let mut iter = dwarf.units();
|
||||
if let Some(header) = iter.next().map_err(|e| gimli_error(e, "iterating over DWARF units"))? {
|
||||
let unit = dwarf.unit(header).map_err(|e| gimli_error(e, "loading DWARF unit"))?;
|
||||
if let Some(program) = unit.line_program.clone() {
|
||||
let mut text_sections = sections.iter_mut().filter(|s| s.kind == SectionKind::Code);
|
||||
let mut lines = text_sections.next().map(|section| &mut section.line_info);
|
||||
|
||||
let mut rows = program.rows();
|
||||
while let Some((_header, row)) =
|
||||
rows.next_row().map_err(|e| gimli_error(e, "loading program row"))?
|
||||
{
|
||||
if let (Some(line), Some(lines)) = (row.line(), &mut lines) {
|
||||
lines.insert(row.address(), line.get() as u32);
|
||||
}
|
||||
if row.end_sequence() {
|
||||
// The next row is the start of a new sequence, which means we must
|
||||
// advance to the next .text section.
|
||||
lines = text_sections.next().map(|section| &mut section.line_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if iter.next().map_err(|e| gimli_error(e, "checking for next unit"))?.is_some() {
|
||||
log::warn!("Multiple units found in DWARF data, only processing the first");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct RelocationMap(object::read::RelocationMap);
|
||||
|
||||
impl RelocationMap {
|
||||
fn add(&mut self, file: &object::File, section: &object::Section) {
|
||||
for (offset, relocation) in section.relocations() {
|
||||
if let Err(e) = self.0.add(file, offset, relocation) {
|
||||
log::error!(
|
||||
"Relocation error for section {} at offset 0x{:08x}: {}",
|
||||
section.name().unwrap(),
|
||||
offset,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl gimli::read::Relocate for &'_ RelocationMap {
|
||||
fn relocate_address(&self, offset: usize, value: u64) -> gimli::Result<u64> {
|
||||
Ok(self.0.relocate(offset as u64, value))
|
||||
}
|
||||
|
||||
fn relocate_offset(&self, offset: usize, value: usize) -> gimli::Result<usize> {
|
||||
<usize as gimli::ReaderOffset>::from_u64(self.0.relocate(offset as u64, value as u64))
|
||||
}
|
||||
}
|
||||
|
||||
type Relocate<'a, R> = gimli::RelocateReader<R, &'a RelocationMap>;
|
||||
|
||||
fn load_file_section<'input, 'arena, Endian: gimli::Endianity>(
|
||||
id: gimli::SectionId,
|
||||
file: &object::File<'input>,
|
||||
endian: Endian,
|
||||
arena_data: &'arena Arena<Cow<'input, [u8]>>,
|
||||
arena_relocations: &'arena Arena<RelocationMap>,
|
||||
) -> Result<Relocate<'arena, gimli::EndianSlice<'arena, Endian>>> {
|
||||
let mut relocations = RelocationMap::default();
|
||||
let data = match file.section_by_name(id.name()) {
|
||||
Some(ref section) => {
|
||||
relocations.add(file, section);
|
||||
section.uncompressed_data()?
|
||||
}
|
||||
// Use a non-zero capacity so that `ReaderOffsetId`s are unique.
|
||||
None => Cow::Owned(Vec::with_capacity(1)),
|
||||
};
|
||||
let data_ref = arena_data.alloc(data);
|
||||
let section = gimli::EndianSlice::new(data_ref, endian);
|
||||
let relocations = arena_relocations.alloc(relocations);
|
||||
Ok(Relocate::new(section, relocations))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn gimli_error(e: gimli::Error, context: &str) -> anyhow::Error {
|
||||
anyhow::anyhow!("gimli error {context}: {e:?}")
|
||||
}
|
||||
401
objdiff-core/src/obj/mdebug.rs
Normal file
401
objdiff-core/src/obj/mdebug.rs
Normal file
@@ -0,0 +1,401 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use anyhow::{Context, Result, bail, ensure};
|
||||
use object::{Endianness, Object, ObjectSection};
|
||||
|
||||
use super::{Section, SectionKind};
|
||||
|
||||
const HDRR_SIZE: usize = 0x60;
|
||||
const FDR_SIZE: usize = 0x48;
|
||||
const PDR_SIZE: usize = 0x34;
|
||||
const SYMR_SIZE: usize = 0x0c;
|
||||
|
||||
const ST_PROC: u8 = 6;
|
||||
const ST_STATICPROC: u8 = 14;
|
||||
const ST_END: u8 = 8;
|
||||
|
||||
pub(super) fn parse_line_info_mdebug(
|
||||
obj_file: &object::File,
|
||||
sections: &mut [Section],
|
||||
) -> Result<()> {
|
||||
let Some(section) = obj_file.section_by_name(".mdebug") else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let data = section.data().context("failed to read .mdebug contents")?;
|
||||
if data.len() < HDRR_SIZE {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let section_file_offset = section.file_range().map(|(offset, _)| offset as usize);
|
||||
|
||||
let endianness = obj_file.endianness();
|
||||
let header = Header::parse(data, endianness)?;
|
||||
|
||||
let symbols_data = slice_at(
|
||||
data,
|
||||
header.cb_sym_offset,
|
||||
header.isym_max.checked_mul(SYMR_SIZE as u32).context("symbol table size overflow")?,
|
||||
section_file_offset,
|
||||
)?;
|
||||
let symbols = parse_symbols(symbols_data, endianness)?;
|
||||
|
||||
let fdr_data = slice_at(
|
||||
data,
|
||||
header.cb_fd_offset,
|
||||
header
|
||||
.ifd_max
|
||||
.checked_mul(FDR_SIZE as u32)
|
||||
.context("file descriptor table size overflow")?,
|
||||
section_file_offset,
|
||||
)?;
|
||||
|
||||
for fdr_index in 0..header.ifd_max as usize {
|
||||
let fdr_offset = fdr_index * FDR_SIZE;
|
||||
let fdr = FileDescriptor::parse(&fdr_data[fdr_offset..fdr_offset + FDR_SIZE], endianness)?;
|
||||
if fdr.cpd == 0 || fdr.csym == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sym_base = fdr.isym_base as usize;
|
||||
let sym_end = sym_base + fdr.csym as usize;
|
||||
if sym_end > symbols.len() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(line_file_offset) = header.cb_line_offset.checked_add(fdr.cb_line_offset) else {
|
||||
continue;
|
||||
};
|
||||
let Some(line_file_base) =
|
||||
resolve_offset(line_file_offset, data.len(), section_file_offset)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(line_file_end) = line_file_base.checked_add(fdr.cb_line as usize) else {
|
||||
continue;
|
||||
};
|
||||
if line_file_end > data.len() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for local_proc_index in 0..fdr.cpd as usize {
|
||||
let pdr_index = fdr.ipd_first as usize + local_proc_index;
|
||||
let pdr_offset = header
|
||||
.cb_pd_offset
|
||||
.checked_add((pdr_index as u32) * PDR_SIZE as u32)
|
||||
.context("procedure descriptor offset overflow")?;
|
||||
let pdr_data = match slice_at(data, pdr_offset, PDR_SIZE as u32, section_file_offset) {
|
||||
Ok(data) => data,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let pdr = ProcDescriptor::parse(pdr_data, endianness)?;
|
||||
if pdr.isym as usize >= fdr.csym as usize {
|
||||
continue;
|
||||
}
|
||||
let global_sym_index = sym_base + pdr.isym as usize;
|
||||
let Some(start_symbol) = symbols.get(global_sym_index) else {
|
||||
continue;
|
||||
};
|
||||
if start_symbol.st != ST_PROC && start_symbol.st != ST_STATICPROC {
|
||||
continue;
|
||||
}
|
||||
|
||||
let local_index = pdr.isym as u32;
|
||||
let mut end_address = None;
|
||||
for sym in &symbols[global_sym_index..sym_end] {
|
||||
if sym.st == ST_END && sym.index == local_index {
|
||||
end_address = Some(sym.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let Some(end_address) = end_address else {
|
||||
continue;
|
||||
};
|
||||
let Some(size) = end_address.checked_sub(start_symbol.value) else {
|
||||
continue;
|
||||
};
|
||||
if size == 0 || size % 4 != 0 {
|
||||
continue;
|
||||
}
|
||||
let word_count = (size / 4) as usize;
|
||||
if word_count == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(mut cursor) = line_file_base.checked_add(pdr.cb_line_offset as usize) else {
|
||||
continue;
|
||||
};
|
||||
if cursor >= line_file_end {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut line_number = pdr.ln_low as i32;
|
||||
let mut lines = Vec::with_capacity(word_count);
|
||||
while lines.len() < word_count && cursor < line_file_end {
|
||||
let b0 = data[cursor];
|
||||
cursor += 1;
|
||||
let count = (b0 & 0x0f) as usize + 1;
|
||||
let delta = decode_delta(endianness, b0 >> 4, data, &mut cursor, line_file_end)?;
|
||||
line_number = line_number.wrapping_add(delta as i32);
|
||||
for _ in 0..count {
|
||||
if lines.len() == word_count {
|
||||
break;
|
||||
}
|
||||
lines.push(line_number);
|
||||
}
|
||||
}
|
||||
|
||||
if lines.len() != word_count {
|
||||
continue;
|
||||
}
|
||||
|
||||
assign_lines(sections, fdr.adr as u64 + pdr.addr as u64, &lines);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn assign_lines(sections: &mut [Section], base_address: u64, lines: &[i32]) {
|
||||
let mut address = base_address;
|
||||
for &line in lines {
|
||||
if line >= 0
|
||||
&& let Some(section) = find_code_section(sections, address)
|
||||
{
|
||||
section.line_info.insert(address, line as u32);
|
||||
}
|
||||
address = address.wrapping_add(4);
|
||||
}
|
||||
}
|
||||
|
||||
fn find_code_section(sections: &mut [Section], address: u64) -> Option<&mut Section> {
|
||||
sections.iter_mut().find(|section| {
|
||||
section.kind == SectionKind::Code
|
||||
&& address >= section.address
|
||||
&& address < section.address + section.size
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_delta(
|
||||
endianness: Endianness,
|
||||
nibble: u8,
|
||||
data: &[u8],
|
||||
cursor: &mut usize,
|
||||
end: usize,
|
||||
) -> Result<i32> {
|
||||
if nibble == 8 {
|
||||
ensure!(*cursor + 2 <= end, "extended delta out of range");
|
||||
let bytes: [u8; 2] = data[*cursor..*cursor + 2].try_into().unwrap();
|
||||
*cursor += 2;
|
||||
Ok(match endianness {
|
||||
Endianness::Big => i16::from_be_bytes(bytes) as i32,
|
||||
Endianness::Little => i16::from_le_bytes(bytes) as i32,
|
||||
})
|
||||
} else {
|
||||
let mut value = (nibble & 0x0f) as i32;
|
||||
if value & 0x8 != 0 {
|
||||
value -= 0x10;
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn slice_at(
|
||||
data: &[u8],
|
||||
offset: u32,
|
||||
size: u32,
|
||||
section_file_offset: Option<usize>,
|
||||
) -> Result<&[u8]> {
|
||||
let size = size as usize;
|
||||
if size == 0 {
|
||||
ensure!(
|
||||
resolve_offset(offset, data.len(), section_file_offset).is_some(),
|
||||
"offset outside of .mdebug section"
|
||||
);
|
||||
return Ok(&data[0..0]);
|
||||
}
|
||||
let Some(offset) = resolve_offset(offset, data.len(), section_file_offset) else {
|
||||
bail!("offset outside of .mdebug section");
|
||||
};
|
||||
let end = offset.checked_add(size).context("range overflow")?;
|
||||
ensure!(end <= data.len(), "range exceeds .mdebug size");
|
||||
Ok(&data[offset..end])
|
||||
}
|
||||
|
||||
fn resolve_offset(
|
||||
offset: u32,
|
||||
data_len: usize,
|
||||
section_file_offset: Option<usize>,
|
||||
) -> Option<usize> {
|
||||
let offset = offset as usize;
|
||||
if offset <= data_len {
|
||||
Some(offset)
|
||||
} else if let Some(file_offset) = section_file_offset {
|
||||
offset.checked_sub(file_offset).filter(|rel| *rel <= data_len)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Header {
|
||||
cb_line_offset: u32,
|
||||
cb_pd_offset: u32,
|
||||
cb_sym_offset: u32,
|
||||
cb_fd_offset: u32,
|
||||
isym_max: u32,
|
||||
ifd_max: u32,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
fn parse(data: &[u8], endianness: Endianness) -> Result<Self> {
|
||||
ensure!(HDRR_SIZE <= data.len(), ".mdebug header truncated");
|
||||
let mut cursor = 0;
|
||||
let magic = read_u16(data, &mut cursor, endianness)?;
|
||||
let _vstamp = read_u16(data, &mut cursor, endianness)?;
|
||||
ensure!(magic == 0x7009, "unexpected .mdebug magic: {magic:#x}");
|
||||
let _iline_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_line = read_u32(data, &mut cursor, endianness)?;
|
||||
let cb_line_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _idn_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_dn_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _ipd_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let cb_pd_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let isym_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let cb_sym_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iopt_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_opt_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iaux_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_aux_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iss_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_ss_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iss_ext_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_ss_ext_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let ifd_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let cb_fd_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _crfd = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_rfd_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iext_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_ext_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
|
||||
Ok(Header { cb_line_offset, cb_pd_offset, cb_sym_offset, cb_fd_offset, isym_max, ifd_max })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct FileDescriptor {
|
||||
adr: u32,
|
||||
isym_base: u32,
|
||||
csym: u32,
|
||||
ipd_first: u16,
|
||||
cpd: u16,
|
||||
cb_line_offset: u32,
|
||||
cb_line: u32,
|
||||
}
|
||||
|
||||
impl FileDescriptor {
|
||||
fn parse(data: &[u8], endianness: Endianness) -> Result<Self> {
|
||||
ensure!(data.len() >= FDR_SIZE, "FDR truncated");
|
||||
let mut cursor = 0;
|
||||
let adr = read_u32(data, &mut cursor, endianness)?;
|
||||
let _rss = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iss_base = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_ss = read_u32(data, &mut cursor, endianness)?;
|
||||
let isym_base = read_u32(data, &mut cursor, endianness)?;
|
||||
let csym = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iline_base = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cline = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iopt_base = read_u32(data, &mut cursor, endianness)?;
|
||||
let _copt = read_u32(data, &mut cursor, endianness)?;
|
||||
let ipd_first = read_u16(data, &mut cursor, endianness)?;
|
||||
let cpd = read_u16(data, &mut cursor, endianness)?;
|
||||
let _iaux_base = read_u32(data, &mut cursor, endianness)?;
|
||||
let _caux = read_u32(data, &mut cursor, endianness)?;
|
||||
let _rfd_base = read_u32(data, &mut cursor, endianness)?;
|
||||
let _crfd = read_u32(data, &mut cursor, endianness)?;
|
||||
let _bits = read_u32(data, &mut cursor, endianness)?;
|
||||
let cb_line_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let cb_line = read_u32(data, &mut cursor, endianness)?;
|
||||
|
||||
Ok(FileDescriptor { adr, isym_base, csym, ipd_first, cpd, cb_line_offset, cb_line })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ProcDescriptor {
|
||||
addr: u32,
|
||||
isym: u32,
|
||||
ln_low: i32,
|
||||
cb_line_offset: u32,
|
||||
}
|
||||
|
||||
impl ProcDescriptor {
|
||||
fn parse(data: &[u8], endianness: Endianness) -> Result<Self> {
|
||||
ensure!(data.len() >= PDR_SIZE, "PDR truncated");
|
||||
let mut cursor = 0;
|
||||
let addr = read_u32(data, &mut cursor, endianness)?;
|
||||
let isym = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iline = read_u32(data, &mut cursor, endianness)?;
|
||||
let _regmask = read_u32(data, &mut cursor, endianness)?;
|
||||
let _regoffset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iopt = read_u32(data, &mut cursor, endianness)?;
|
||||
let _fregmask = read_u32(data, &mut cursor, endianness)?;
|
||||
let _fregoffset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _frameoffset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _framereg = read_u16(data, &mut cursor, endianness)?;
|
||||
let _pcreg = read_u16(data, &mut cursor, endianness)?;
|
||||
let ln_low = read_i32(data, &mut cursor, endianness)?;
|
||||
let _ln_high = read_i32(data, &mut cursor, endianness)?;
|
||||
let cb_line_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
|
||||
Ok(ProcDescriptor { addr, isym, ln_low, cb_line_offset })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct SymbolEntry {
|
||||
value: u32,
|
||||
st: u8,
|
||||
index: u32,
|
||||
}
|
||||
|
||||
fn parse_symbols(data: &[u8], endianness: Endianness) -> Result<Vec<SymbolEntry>> {
|
||||
ensure!(data.len().is_multiple_of(SYMR_SIZE), "symbol table misaligned");
|
||||
let mut symbols = Vec::with_capacity(data.len() / SYMR_SIZE);
|
||||
let mut cursor = 0;
|
||||
while cursor + SYMR_SIZE <= data.len() {
|
||||
let _iss = read_u32(data, &mut cursor, endianness)?;
|
||||
let value = read_u32(data, &mut cursor, endianness)?;
|
||||
let bits = read_u32(data, &mut cursor, endianness)?;
|
||||
let (st, index) = match endianness {
|
||||
Endianness::Big => (((bits >> 26) & 0x3f) as u8, bits & 0x000f_ffff),
|
||||
Endianness::Little => (((bits & 0x3f) as u8), (bits >> 12) & 0x000f_ffff),
|
||||
};
|
||||
symbols.push(SymbolEntry { value, st, index });
|
||||
}
|
||||
Ok(symbols)
|
||||
}
|
||||
|
||||
fn read_u16(data: &[u8], cursor: &mut usize, endianness: Endianness) -> Result<u16> {
|
||||
ensure!(*cursor + 2 <= data.len(), "unexpected EOF while reading u16");
|
||||
let bytes: [u8; 2] = data[*cursor..*cursor + 2].try_into().unwrap();
|
||||
*cursor += 2;
|
||||
Ok(match endianness {
|
||||
Endianness::Big => u16::from_be_bytes(bytes),
|
||||
Endianness::Little => u16::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_u32(data: &[u8], cursor: &mut usize, endianness: Endianness) -> Result<u32> {
|
||||
ensure!(*cursor + 4 <= data.len(), "unexpected EOF while reading u32");
|
||||
let bytes: [u8; 4] = data[*cursor..*cursor + 4].try_into().unwrap();
|
||||
*cursor += 4;
|
||||
Ok(match endianness {
|
||||
Endianness::Big => u32::from_be_bytes(bytes),
|
||||
Endianness::Little => u32::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_i32(data: &[u8], cursor: &mut usize, endianness: Endianness) -> Result<i32> {
|
||||
Ok(read_u32(data, cursor, endianness)? as i32)
|
||||
}
|
||||
@@ -1,174 +1,441 @@
|
||||
#[cfg(feature = "dwarf")]
|
||||
mod dwarf2;
|
||||
mod mdebug;
|
||||
pub mod read;
|
||||
pub mod split_meta;
|
||||
|
||||
use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
|
||||
use alloc::{
|
||||
borrow::Cow,
|
||||
boxed::Box,
|
||||
collections::BTreeMap,
|
||||
string::{String, ToString},
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
use core::{
|
||||
fmt,
|
||||
num::{NonZeroU32, NonZeroU64},
|
||||
};
|
||||
|
||||
use cwextab::*;
|
||||
use filetime::FileTime;
|
||||
use flagset::{flags, FlagSet};
|
||||
use object::RelocationFlags;
|
||||
use split_meta::SplitMeta;
|
||||
use flagset::{FlagSet, flags};
|
||||
|
||||
use crate::{arch::ObjArch, util::ReallySigned};
|
||||
use crate::{
|
||||
arch::{Arch, ArchDummy},
|
||||
obj::split_meta::SplitMeta,
|
||||
util::ReallySigned,
|
||||
};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum ObjSectionKind {
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)]
|
||||
pub enum SectionKind {
|
||||
#[default]
|
||||
Unknown = -1,
|
||||
Code,
|
||||
Data,
|
||||
Bss,
|
||||
Common,
|
||||
}
|
||||
|
||||
flags! {
|
||||
pub enum ObjSymbolFlags: u8 {
|
||||
#[derive(Hash)]
|
||||
pub enum SymbolFlag: u8 {
|
||||
Global,
|
||||
Local,
|
||||
Weak,
|
||||
Common,
|
||||
Hidden,
|
||||
/// Has extra data associated with the symbol
|
||||
/// (e.g. exception table entry)
|
||||
HasExtra,
|
||||
/// Symbol size was missing and was inferred
|
||||
SizeInferred,
|
||||
/// Symbol should be ignored by any diffing
|
||||
Ignored,
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub struct ObjSymbolFlagSet(pub FlagSet<ObjSymbolFlags>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjSection {
|
||||
pub type SymbolFlagSet = FlagSet<SymbolFlag>;
|
||||
|
||||
flags! {
|
||||
#[derive(Hash)]
|
||||
pub enum SectionFlag: u8 {
|
||||
/// Section combined from multiple input sections
|
||||
Combined,
|
||||
}
|
||||
}
|
||||
|
||||
pub type SectionFlagSet = FlagSet<SectionFlag>;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Section {
|
||||
/// Unique section ID
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub kind: ObjSectionKind,
|
||||
pub address: u64,
|
||||
pub size: u64,
|
||||
pub data: Vec<u8>,
|
||||
pub orig_index: usize,
|
||||
pub symbols: Vec<ObjSymbol>,
|
||||
pub relocations: Vec<ObjReloc>,
|
||||
pub virtual_address: Option<u64>,
|
||||
pub kind: SectionKind,
|
||||
pub data: SectionData,
|
||||
pub flags: SectionFlagSet,
|
||||
pub align: Option<NonZeroU64>,
|
||||
pub relocations: Vec<Relocation>,
|
||||
/// Line number info (.line or .debug_line section)
|
||||
pub line_info: BTreeMap<u64, u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum ObjInsArgValue {
|
||||
Signed(i64),
|
||||
Unsigned(u64),
|
||||
Opaque(Cow<'static, str>),
|
||||
}
|
||||
|
||||
impl ObjInsArgValue {
|
||||
pub fn loose_eq(&self, other: &ObjInsArgValue) -> bool {
|
||||
match (self, other) {
|
||||
(ObjInsArgValue::Signed(a), ObjInsArgValue::Signed(b)) => a == b,
|
||||
(ObjInsArgValue::Unsigned(a), ObjInsArgValue::Unsigned(b)) => a == b,
|
||||
(ObjInsArgValue::Signed(a), ObjInsArgValue::Unsigned(b))
|
||||
| (ObjInsArgValue::Unsigned(b), ObjInsArgValue::Signed(a)) => *a as u64 == *b,
|
||||
(ObjInsArgValue::Opaque(a), ObjInsArgValue::Opaque(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ObjInsArgValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ObjInsArgValue::Signed(v) => write!(f, "{:#x}", ReallySigned(*v)),
|
||||
ObjInsArgValue::Unsigned(v) => write!(f, "{:#x}", v),
|
||||
ObjInsArgValue::Opaque(v) => write!(f, "{}", v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum ObjInsArg {
|
||||
PlainText(Cow<'static, str>),
|
||||
Arg(ObjInsArgValue),
|
||||
Reloc,
|
||||
BranchDest(u64),
|
||||
}
|
||||
|
||||
impl ObjInsArg {
|
||||
pub fn loose_eq(&self, other: &ObjInsArg) -> bool {
|
||||
match (self, other) {
|
||||
(ObjInsArg::Arg(a), ObjInsArg::Arg(b)) => a.loose_eq(b),
|
||||
(ObjInsArg::Reloc, ObjInsArg::Reloc) => true,
|
||||
(ObjInsArg::BranchDest(a), ObjInsArg::BranchDest(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjIns {
|
||||
pub address: u64,
|
||||
pub size: u8,
|
||||
pub op: u16,
|
||||
pub mnemonic: String,
|
||||
pub args: Vec<ObjInsArg>,
|
||||
pub reloc: Option<ObjReloc>,
|
||||
pub branch_dest: Option<u64>,
|
||||
/// Line number
|
||||
pub line: Option<u32>,
|
||||
/// Formatted instruction
|
||||
pub formatted: String,
|
||||
/// Original (unsimplified) instruction
|
||||
pub orig: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjSymbol {
|
||||
pub name: String,
|
||||
pub demangled_name: Option<String>,
|
||||
pub has_extab: bool,
|
||||
pub extab_name: Option<String>,
|
||||
pub extabindex_name: Option<String>,
|
||||
pub address: u64,
|
||||
pub section_address: u64,
|
||||
pub size: u64,
|
||||
pub size_known: bool,
|
||||
pub flags: ObjSymbolFlagSet,
|
||||
pub addend: i64,
|
||||
/// Original virtual address (from .note.split section)
|
||||
pub virtual_address: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjExtab {
|
||||
pub func: ObjSymbol,
|
||||
pub data: ExceptionTableData,
|
||||
pub dtors: Vec<ObjSymbol>,
|
||||
#[derive(Clone, Default)]
|
||||
#[repr(transparent)]
|
||||
pub struct SectionData(pub Vec<u8>);
|
||||
|
||||
impl core::ops::Deref for SectionData {
|
||||
type Target = Vec<u8>;
|
||||
|
||||
fn deref(&self) -> &Self::Target { &self.0 }
|
||||
}
|
||||
|
||||
pub struct ObjInfo {
|
||||
pub arch: Box<dyn ObjArch>,
|
||||
pub path: Option<PathBuf>,
|
||||
pub timestamp: Option<FileTime>,
|
||||
pub sections: Vec<ObjSection>,
|
||||
/// Common BSS symbols
|
||||
pub common: Vec<ObjSymbol>,
|
||||
/// Exception tables
|
||||
pub extab: Option<Vec<ObjExtab>>,
|
||||
/// Split object metadata (.note.split section)
|
||||
pub split_meta: Option<SplitMeta>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjReloc {
|
||||
pub flags: RelocationFlags,
|
||||
pub address: u64,
|
||||
pub target: ObjSymbol,
|
||||
pub target_section: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct SymbolRef {
|
||||
pub section_idx: usize,
|
||||
pub symbol_idx: usize,
|
||||
}
|
||||
|
||||
impl ObjInfo {
|
||||
pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (Option<&ObjSection>, &ObjSymbol) {
|
||||
if symbol_ref.section_idx == self.sections.len() {
|
||||
let symbol = &self.common[symbol_ref.symbol_idx];
|
||||
return (None, symbol);
|
||||
}
|
||||
let section = &self.sections[symbol_ref.section_idx];
|
||||
let symbol = §ion.symbols[symbol_ref.symbol_idx];
|
||||
(Some(section), symbol)
|
||||
impl fmt::Debug for SectionData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("SectionData").field(&self.0.len()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Section {
|
||||
pub fn data_range(&self, address: u64, size: usize) -> Option<&[u8]> {
|
||||
let offset = address.checked_sub(self.address)?;
|
||||
self.data.get(offset as usize..offset as usize + size)
|
||||
}
|
||||
|
||||
// The alignment to use when "Combine data/text sections" is enabled.
|
||||
pub fn combined_alignment(&self) -> u64 {
|
||||
const MIN_ALIGNMENT: u64 = 4;
|
||||
self.align.map_or(MIN_ALIGNMENT, |align| align.get().max(MIN_ALIGNMENT))
|
||||
}
|
||||
|
||||
pub fn relocation_at(&self, address: u64, size: u8) -> Option<&Relocation> {
|
||||
match self.relocations.binary_search_by_key(&address, |r| r.address) {
|
||||
Ok(mut i) => {
|
||||
// Find the first relocation at the address
|
||||
while i
|
||||
.checked_sub(1)
|
||||
.and_then(|n| self.relocations.get(n))
|
||||
.is_some_and(|r| r.address == address)
|
||||
{
|
||||
i -= 1;
|
||||
}
|
||||
self.relocations.get(i)
|
||||
}
|
||||
Err(i) => self.relocations.get(i).filter(|r| r.address < address + size as u64),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_relocation_at<'obj>(
|
||||
&'obj self,
|
||||
obj: &'obj Object,
|
||||
address: u64,
|
||||
size: u8,
|
||||
) -> Option<ResolvedRelocation<'obj>> {
|
||||
self.relocation_at(address, size).and_then(|relocation| {
|
||||
let symbol = obj.symbols.get(relocation.target_symbol)?;
|
||||
Some(ResolvedRelocation { relocation, symbol })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum InstructionArgValue<'a> {
|
||||
Signed(i64),
|
||||
Unsigned(u64),
|
||||
Opaque(Cow<'a, str>),
|
||||
}
|
||||
|
||||
impl InstructionArgValue<'_> {
|
||||
pub fn loose_eq(&self, other: &InstructionArgValue) -> bool {
|
||||
match (self, other) {
|
||||
(InstructionArgValue::Signed(a), InstructionArgValue::Signed(b)) => a == b,
|
||||
(InstructionArgValue::Unsigned(a), InstructionArgValue::Unsigned(b)) => a == b,
|
||||
(InstructionArgValue::Signed(a), InstructionArgValue::Unsigned(b))
|
||||
| (InstructionArgValue::Unsigned(b), InstructionArgValue::Signed(a)) => *a as u64 == *b,
|
||||
(InstructionArgValue::Opaque(a), InstructionArgValue::Opaque(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_static(&self) -> InstructionArgValue<'static> {
|
||||
match self {
|
||||
InstructionArgValue::Signed(v) => InstructionArgValue::Signed(*v),
|
||||
InstructionArgValue::Unsigned(v) => InstructionArgValue::Unsigned(*v),
|
||||
InstructionArgValue::Opaque(v) => InstructionArgValue::Opaque(v.to_string().into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_static(self) -> InstructionArgValue<'static> {
|
||||
match self {
|
||||
InstructionArgValue::Signed(v) => InstructionArgValue::Signed(v),
|
||||
InstructionArgValue::Unsigned(v) => InstructionArgValue::Unsigned(v),
|
||||
InstructionArgValue::Opaque(v) => {
|
||||
InstructionArgValue::Opaque(Cow::Owned(v.into_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for InstructionArgValue<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
InstructionArgValue::Signed(v) => write!(f, "{:#x}", ReallySigned(*v)),
|
||||
InstructionArgValue::Unsigned(v) => write!(f, "{v:#x}"),
|
||||
InstructionArgValue::Opaque(v) => write!(f, "{v}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum InstructionArg<'a> {
|
||||
Value(InstructionArgValue<'a>),
|
||||
Reloc,
|
||||
BranchDest(u64),
|
||||
}
|
||||
|
||||
impl InstructionArg<'_> {
|
||||
pub fn loose_eq(&self, other: &InstructionArg) -> bool {
|
||||
match (self, other) {
|
||||
(InstructionArg::Value(a), InstructionArg::Value(b)) => a.loose_eq(b),
|
||||
(InstructionArg::Reloc, InstructionArg::Reloc) => true,
|
||||
(InstructionArg::BranchDest(a), InstructionArg::BranchDest(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_static(&self) -> InstructionArg<'static> {
|
||||
match self {
|
||||
InstructionArg::Value(v) => InstructionArg::Value(v.to_static()),
|
||||
InstructionArg::Reloc => InstructionArg::Reloc,
|
||||
InstructionArg::BranchDest(v) => InstructionArg::BranchDest(*v),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_static(self) -> InstructionArg<'static> {
|
||||
match self {
|
||||
InstructionArg::Value(v) => InstructionArg::Value(v.into_static()),
|
||||
InstructionArg::Reloc => InstructionArg::Reloc,
|
||||
InstructionArg::BranchDest(v) => InstructionArg::BranchDest(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct InstructionRef {
|
||||
pub address: u64,
|
||||
pub size: u8,
|
||||
pub opcode: u16,
|
||||
pub branch_dest: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedInstruction {
|
||||
pub ins_ref: InstructionRef,
|
||||
pub mnemonic: Cow<'static, str>,
|
||||
pub args: Vec<InstructionArg<'static>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub enum SymbolKind {
|
||||
#[default]
|
||||
Unknown,
|
||||
Function,
|
||||
Object,
|
||||
Section,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FlowAnalysisValue {
|
||||
Text(String),
|
||||
}
|
||||
|
||||
pub trait FlowAnalysisResult: core::fmt::Debug + Send {
|
||||
fn get_argument_value_at_address(
|
||||
&self,
|
||||
address: u64,
|
||||
argument: u8,
|
||||
) -> Option<&FlowAnalysisValue>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub struct Symbol {
|
||||
pub name: String,
|
||||
pub demangled_name: Option<String>,
|
||||
pub address: u64,
|
||||
pub size: u64,
|
||||
pub kind: SymbolKind,
|
||||
pub section: Option<usize>,
|
||||
pub flags: SymbolFlagSet,
|
||||
/// Alignment (from Metrowerks .comment section)
|
||||
pub align: Option<NonZeroU32>,
|
||||
/// Original virtual address (from .note.split section)
|
||||
pub virtual_address: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Object {
|
||||
pub arch: Box<dyn Arch>,
|
||||
pub endianness: object::Endianness,
|
||||
pub symbols: Vec<Symbol>,
|
||||
pub sections: Vec<Section>,
|
||||
/// Split object metadata (.note.split section)
|
||||
pub split_meta: Option<SplitMeta>,
|
||||
#[cfg(feature = "std")]
|
||||
pub path: Option<std::path::PathBuf>,
|
||||
#[cfg(feature = "std")]
|
||||
pub timestamp: Option<filetime::FileTime>,
|
||||
pub flow_analysis_results: BTreeMap<u64, Box<dyn FlowAnalysisResult>>,
|
||||
}
|
||||
|
||||
impl Default for Object {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
arch: ArchDummy::new(),
|
||||
endianness: object::Endianness::Little,
|
||||
symbols: vec![],
|
||||
sections: vec![],
|
||||
split_meta: None,
|
||||
#[cfg(feature = "std")]
|
||||
path: None,
|
||||
#[cfg(feature = "std")]
|
||||
timestamp: None,
|
||||
flow_analysis_results: BTreeMap::<u64, Box<dyn FlowAnalysisResult>>::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Object {
|
||||
pub fn resolve_instruction_ref(
|
||||
&self,
|
||||
symbol_index: usize,
|
||||
ins_ref: InstructionRef,
|
||||
) -> Option<ResolvedInstructionRef<'_>> {
|
||||
let symbol = self.symbols.get(symbol_index)?;
|
||||
let section_index = symbol.section?;
|
||||
let section = self.sections.get(section_index)?;
|
||||
let offset = ins_ref.address.checked_sub(section.address)?;
|
||||
let code = section.data.get(offset as usize..offset as usize + ins_ref.size as usize)?;
|
||||
let relocation = section.resolve_relocation_at(self, ins_ref.address, ins_ref.size);
|
||||
Some(ResolvedInstructionRef {
|
||||
ins_ref,
|
||||
symbol_index,
|
||||
symbol,
|
||||
section,
|
||||
section_index,
|
||||
code,
|
||||
relocation,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn symbol_data(&self, symbol_index: usize) -> Option<&[u8]> {
|
||||
let symbol = self.symbols.get(symbol_index)?;
|
||||
let section_index = symbol.section?;
|
||||
let section = self.sections.get(section_index)?;
|
||||
let offset = symbol.address.checked_sub(section.address)?;
|
||||
section.data.get(offset as usize..offset as usize + symbol.size as usize)
|
||||
}
|
||||
|
||||
pub fn symbol_by_name(&self, name: &str) -> Option<usize> {
|
||||
self.symbols.iter().position(|symbol| symbol.section.is_some() && symbol.name == name)
|
||||
}
|
||||
|
||||
pub fn get_flow_analysis_result(&self, symbol: &Symbol) -> Option<&dyn FlowAnalysisResult> {
|
||||
let key = symbol.section.unwrap_or_default() as u64 * 1024 * 1024 * 1024 + symbol.address;
|
||||
self.flow_analysis_results.get(&key).map(|result| result.as_ref())
|
||||
}
|
||||
|
||||
pub fn add_flow_analysis_result(
|
||||
&mut self,
|
||||
symbol: &Symbol,
|
||||
result: Box<dyn FlowAnalysisResult>,
|
||||
) {
|
||||
let key = symbol.section.unwrap_or_default() as u64 * 1024 * 1024 * 1024 + symbol.address;
|
||||
self.flow_analysis_results.insert(key, result);
|
||||
}
|
||||
|
||||
pub fn has_flow_analysis_result(&self) -> bool { !self.flow_analysis_results.is_empty() }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Relocation {
|
||||
pub flags: RelocationFlags,
|
||||
pub address: u64,
|
||||
pub target_symbol: usize,
|
||||
pub addend: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum RelocationFlags {
|
||||
Elf(u32),
|
||||
Coff(u16),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ResolvedRelocation<'a> {
|
||||
pub relocation: &'a Relocation,
|
||||
pub symbol: &'a Symbol,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ResolvedSymbol<'obj> {
|
||||
pub obj: &'obj Object,
|
||||
pub symbol_index: usize,
|
||||
pub symbol: &'obj Symbol,
|
||||
pub section_index: usize,
|
||||
pub section: &'obj Section,
|
||||
pub data: &'obj [u8],
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ResolvedInstructionRef<'obj> {
|
||||
pub ins_ref: InstructionRef,
|
||||
pub symbol_index: usize,
|
||||
pub symbol: &'obj Symbol,
|
||||
pub section_index: usize,
|
||||
pub section: &'obj Section,
|
||||
pub code: &'obj [u8],
|
||||
pub relocation: Option<ResolvedRelocation<'obj>>,
|
||||
}
|
||||
|
||||
static DUMMY_SYMBOL: Symbol = Symbol {
|
||||
name: String::new(),
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: SymbolKind::Unknown,
|
||||
section: None,
|
||||
flags: SymbolFlagSet::empty(),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
};
|
||||
|
||||
static DUMMY_SECTION: Section = Section {
|
||||
id: String::new(),
|
||||
name: String::new(),
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: SectionKind::Unknown,
|
||||
data: SectionData(Vec::new()),
|
||||
flags: SectionFlagSet::empty(),
|
||||
align: None,
|
||||
relocations: Vec::new(),
|
||||
line_info: BTreeMap::new(),
|
||||
virtual_address: None,
|
||||
};
|
||||
|
||||
impl Default for ResolvedInstructionRef<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ins_ref: InstructionRef::default(),
|
||||
symbol_index: 0,
|
||||
symbol: &DUMMY_SYMBOL,
|
||||
section_index: 0,
|
||||
section: &DUMMY_SECTION,
|
||||
code: &[],
|
||||
relocation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,165 @@
|
||||
---
|
||||
source: objdiff-core/src/obj/read.rs
|
||||
expression: "(sections, symbols)"
|
||||
---
|
||||
(
|
||||
[
|
||||
Section {
|
||||
id: ".text-0",
|
||||
name: ".text",
|
||||
address: 0,
|
||||
size: 8,
|
||||
kind: Code,
|
||||
data: SectionData(
|
||||
8,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
align: None,
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
0,
|
||||
),
|
||||
address: 0,
|
||||
target_symbol: 0,
|
||||
addend: 4,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
0,
|
||||
),
|
||||
address: 2,
|
||||
target_symbol: 1,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
0,
|
||||
),
|
||||
address: 4,
|
||||
target_symbol: 0,
|
||||
addend: 10,
|
||||
},
|
||||
],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".data-combined",
|
||||
name: ".data",
|
||||
address: 0,
|
||||
size: 12,
|
||||
kind: Data,
|
||||
data: SectionData(
|
||||
12,
|
||||
),
|
||||
flags: FlagSet(Combined),
|
||||
align: None,
|
||||
relocations: [
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
0,
|
||||
),
|
||||
address: 0,
|
||||
target_symbol: 2,
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
flags: Elf(
|
||||
0,
|
||||
),
|
||||
address: 4,
|
||||
target_symbol: 2,
|
||||
addend: 0,
|
||||
},
|
||||
],
|
||||
line_info: {
|
||||
0: 1,
|
||||
8: 2,
|
||||
},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".data-1",
|
||||
name: ".data",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
align: None,
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
Section {
|
||||
id: ".data-2",
|
||||
name: ".data",
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Unknown,
|
||||
data: SectionData(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
align: None,
|
||||
relocations: [],
|
||||
line_info: {},
|
||||
virtual_address: None,
|
||||
},
|
||||
],
|
||||
[
|
||||
Symbol {
|
||||
name: ".data",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Section,
|
||||
section: Some(
|
||||
1,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "symbol",
|
||||
demangled_name: None,
|
||||
address: 4,
|
||||
size: 4,
|
||||
kind: Object,
|
||||
section: Some(
|
||||
1,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: "function",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 8,
|
||||
kind: Function,
|
||||
section: Some(
|
||||
0,
|
||||
),
|
||||
flags: FlagSet(),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
Symbol {
|
||||
name: ".data",
|
||||
demangled_name: None,
|
||||
address: 0,
|
||||
size: 0,
|
||||
kind: Unknown,
|
||||
section: None,
|
||||
flags: FlagSet(),
|
||||
align: None,
|
||||
virtual_address: None,
|
||||
},
|
||||
],
|
||||
)
|
||||
@@ -1,6 +1,11 @@
|
||||
use std::{io, io::Write};
|
||||
use alloc::{string::String, vec, vec::Vec};
|
||||
|
||||
use object::{elf::SHT_NOTE, Endian, ObjectSection};
|
||||
use anyhow::{Result, anyhow};
|
||||
use object::{Endian, ObjectSection, elf::SHT_NOTE};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use crate::util::align_data_to_4;
|
||||
use crate::util::align_size_to_4;
|
||||
|
||||
pub const SPLITMETA_SECTION: &str = ".note.split";
|
||||
pub const SHT_SPLITMETA: u32 = SHT_NOTE;
|
||||
@@ -27,10 +32,10 @@ const NT_SPLIT_MODULE_ID: u32 = u32::from_be_bytes(*b"MODI");
|
||||
const NT_SPLIT_VIRTUAL_ADDRESSES: u32 = u32::from_be_bytes(*b"VIRT");
|
||||
|
||||
impl SplitMeta {
|
||||
pub fn from_section<E>(section: object::Section, e: E, is_64: bool) -> io::Result<Self>
|
||||
pub fn from_section<E>(section: object::Section, e: E, is_64: bool) -> Result<Self>
|
||||
where E: Endian {
|
||||
let mut result = SplitMeta::default();
|
||||
let data = section.uncompressed_data().map_err(object_io_error)?;
|
||||
let data = section.uncompressed_data().map_err(object_error)?;
|
||||
let mut iter = NoteIterator::new(data.as_ref(), section.align(), e, is_64)?;
|
||||
while let Some(note) = iter.next(e)? {
|
||||
if note.name != ELF_NOTE_SPLIT {
|
||||
@@ -38,20 +43,19 @@ impl SplitMeta {
|
||||
}
|
||||
match note.n_type {
|
||||
NT_SPLIT_GENERATOR => {
|
||||
let string = String::from_utf8(note.desc.to_vec())
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
let string =
|
||||
String::from_utf8(note.desc.to_vec()).map_err(anyhow::Error::new)?;
|
||||
result.generator = Some(string);
|
||||
}
|
||||
NT_SPLIT_MODULE_NAME => {
|
||||
let string = String::from_utf8(note.desc.to_vec())
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
let string =
|
||||
String::from_utf8(note.desc.to_vec()).map_err(anyhow::Error::new)?;
|
||||
result.module_name = Some(string);
|
||||
}
|
||||
NT_SPLIT_MODULE_ID => {
|
||||
result.module_id =
|
||||
Some(e.read_u32_bytes(note.desc.try_into().map_err(|_| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "Invalid module ID size")
|
||||
})?));
|
||||
result.module_id = Some(e.read_u32_bytes(
|
||||
note.desc.try_into().map_err(|_| anyhow!("Invalid module ID size"))?,
|
||||
));
|
||||
}
|
||||
NT_SPLIT_VIRTUAL_ADDRESSES => {
|
||||
let vec = if is_64 {
|
||||
@@ -79,10 +83,11 @@ impl SplitMeta {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn to_writer<E, W>(&self, writer: &mut W, e: E, is_64: bool) -> io::Result<()>
|
||||
#[cfg(feature = "std")]
|
||||
pub fn to_writer<E, W>(&self, writer: &mut W, e: E, is_64: bool) -> std::io::Result<()>
|
||||
where
|
||||
E: Endian,
|
||||
W: Write + ?Sized,
|
||||
W: std::io::Write + ?Sized,
|
||||
{
|
||||
if let Some(generator) = &self.generator {
|
||||
write_note_header(writer, e, NT_SPLIT_GENERATOR, generator.len())?;
|
||||
@@ -137,10 +142,9 @@ impl SplitMeta {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an object::read::Error to an io::Error.
|
||||
fn object_io_error(err: object::read::Error) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidData, err)
|
||||
}
|
||||
/// Convert an object::read::Error to a String.
|
||||
#[inline]
|
||||
fn object_error(err: object::read::Error) -> anyhow::Error { anyhow::Error::new(err) }
|
||||
|
||||
/// An ELF note entry.
|
||||
struct Note<'data> {
|
||||
@@ -161,27 +165,27 @@ where E: Endian
|
||||
impl<'data, E> NoteIterator<'data, E>
|
||||
where E: Endian
|
||||
{
|
||||
fn new(data: &'data [u8], align: u64, e: E, is_64: bool) -> io::Result<Self> {
|
||||
fn new(data: &'data [u8], align: u64, e: E, is_64: bool) -> Result<Self> {
|
||||
Ok(if is_64 {
|
||||
NoteIterator::B64(
|
||||
object::read::elf::NoteIterator::new(e, align, data).map_err(object_io_error)?,
|
||||
object::read::elf::NoteIterator::new(e, align, data).map_err(object_error)?,
|
||||
)
|
||||
} else {
|
||||
NoteIterator::B32(
|
||||
object::read::elf::NoteIterator::new(e, align as u32, data)
|
||||
.map_err(object_io_error)?,
|
||||
.map_err(object_error)?,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn next(&mut self, e: E) -> io::Result<Option<Note<'data>>> {
|
||||
fn next(&mut self, e: E) -> Result<Option<Note<'data>>> {
|
||||
match self {
|
||||
NoteIterator::B32(iter) => Ok(iter.next().map_err(object_io_error)?.map(|note| Note {
|
||||
NoteIterator::B32(iter) => Ok(iter.next().map_err(object_error)?.map(|note| Note {
|
||||
n_type: note.n_type(e),
|
||||
name: note.name(),
|
||||
desc: note.desc(),
|
||||
})),
|
||||
NoteIterator::B64(iter) => Ok(iter.next().map_err(object_io_error)?.map(|note| Note {
|
||||
NoteIterator::B64(iter) => Ok(iter.next().map_err(object_error)?.map(|note| Note {
|
||||
n_type: note.n_type(e),
|
||||
name: note.name(),
|
||||
desc: note.desc(),
|
||||
@@ -190,16 +194,6 @@ where E: Endian
|
||||
}
|
||||
}
|
||||
|
||||
fn align_size_to_4(size: usize) -> usize { (size + 3) & !3 }
|
||||
|
||||
fn align_data_to_4<W: Write + ?Sized>(writer: &mut W, len: usize) -> io::Result<()> {
|
||||
const ALIGN_BYTES: &[u8] = &[0; 4];
|
||||
if len % 4 != 0 {
|
||||
writer.write_all(&ALIGN_BYTES[..4 - len % 4])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ELF note format:
|
||||
// Name Size | 4 bytes (integer)
|
||||
// Desc Size | 4 bytes (integer)
|
||||
@@ -208,10 +202,11 @@ fn align_data_to_4<W: Write + ?Sized>(writer: &mut W, len: usize) -> io::Result<
|
||||
// Desc | variable size, padded to a 4 byte boundary
|
||||
const NOTE_HEADER_SIZE: usize = 12 + ((ELF_NOTE_SPLIT.len() + 4) & !3);
|
||||
|
||||
fn write_note_header<E, W>(writer: &mut W, e: E, kind: u32, desc_len: usize) -> io::Result<()>
|
||||
#[cfg(feature = "std")]
|
||||
fn write_note_header<E, W>(writer: &mut W, e: E, kind: u32, desc_len: usize) -> std::io::Result<()>
|
||||
where
|
||||
E: Endian,
|
||||
W: Write + ?Sized,
|
||||
W: std::io::Write + ?Sized,
|
||||
{
|
||||
writer.write_all(&e.write_u32_bytes(ELF_NOTE_SPLIT.len() as u32 + 1))?; // Name Size
|
||||
writer.write_all(&e.write_u32_bytes(desc_len as u32))?; // Desc Size
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
use std::{
|
||||
fmt::{LowerHex, UpperHex},
|
||||
io::Read,
|
||||
};
|
||||
use alloc::{format, vec::Vec};
|
||||
use core::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use byteorder::{NativeEndian, ReadBytesExt};
|
||||
use anyhow::{Result, ensure};
|
||||
use num_traits::PrimInt;
|
||||
use object::{Endian, Object};
|
||||
|
||||
// https://stackoverflow.com/questions/44711012/how-do-i-format-a-signed-integer-to-a-sign-aware-hexadecimal-representation
|
||||
pub struct ReallySigned<N: PrimInt>(pub(crate) N);
|
||||
pub struct ReallySigned<N: PrimInt>(pub N);
|
||||
|
||||
impl<N: PrimInt> LowerHex for ReallySigned<N> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
impl<N: PrimInt> fmt::LowerHex for ReallySigned<N> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let num = self.0.to_i64().unwrap();
|
||||
let prefix = if f.alternate() { "0x" } else { "" };
|
||||
let bare_hex = format!("{:x}", num.abs());
|
||||
@@ -20,8 +17,8 @@ impl<N: PrimInt> LowerHex for ReallySigned<N> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: PrimInt> UpperHex for ReallySigned<N> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
impl<N: PrimInt> fmt::UpperHex for ReallySigned<N> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let num = self.0.to_i64().unwrap();
|
||||
let prefix = if f.alternate() { "0x" } else { "" };
|
||||
let bare_hex = format!("{:X}", num.abs());
|
||||
@@ -29,10 +26,66 @@ impl<N: PrimInt> UpperHex for ReallySigned<N> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_u32<R: Read>(obj_file: &object::File, reader: &mut R) -> Result<u32> {
|
||||
Ok(obj_file.endianness().read_u32(reader.read_u32::<NativeEndian>()?))
|
||||
pub fn read_u32(obj_file: &object::File, reader: &mut &[u8]) -> Result<u32> {
|
||||
ensure!(reader.len() >= 4, "Not enough bytes to read u32");
|
||||
let value = u32::from_ne_bytes(reader[..4].try_into()?);
|
||||
*reader = &reader[4..];
|
||||
Ok(obj_file.endianness().read_u32(value))
|
||||
}
|
||||
|
||||
pub fn read_u16<R: Read>(obj_file: &object::File, reader: &mut R) -> Result<u16> {
|
||||
Ok(obj_file.endianness().read_u16(reader.read_u16::<NativeEndian>()?))
|
||||
pub fn read_u16(obj_file: &object::File, reader: &mut &[u8]) -> Result<u16> {
|
||||
ensure!(reader.len() >= 2, "Not enough bytes to read u16");
|
||||
let value = u16::from_ne_bytes(reader[..2].try_into()?);
|
||||
*reader = &reader[2..];
|
||||
Ok(obj_file.endianness().read_u16(value))
|
||||
}
|
||||
|
||||
pub fn align_size_to_4(size: usize) -> usize { (size + 3) & !3 }
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn align_data_to_4<W: std::io::Write + ?Sized>(
|
||||
writer: &mut W,
|
||||
len: usize,
|
||||
) -> std::io::Result<()> {
|
||||
const ALIGN_BYTES: &[u8] = &[0; 4];
|
||||
if !len.is_multiple_of(4) {
|
||||
writer.write_all(&ALIGN_BYTES[..4 - len % 4])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn align_u64_to(len: u64, align: u64) -> u64 { len + ((align - (len % align)) % align) }
|
||||
|
||||
pub fn align_data_slice_to(data: &mut Vec<u8>, align: u64) {
|
||||
data.resize(align_u64_to(data.len() as u64, align) as usize, 0);
|
||||
}
|
||||
|
||||
// Float where we specifically care about comparing the raw bits rather than
|
||||
// caring about IEEE semantics.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct RawFloat(pub f32);
|
||||
impl PartialEq for RawFloat {
|
||||
fn eq(&self, other: &Self) -> bool { self.0.to_bits() == other.0.to_bits() }
|
||||
}
|
||||
impl Eq for RawFloat {}
|
||||
impl Ord for RawFloat {
|
||||
fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.0.to_bits().cmp(&other.0.to_bits()) }
|
||||
}
|
||||
impl PartialOrd for RawFloat {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> { Some(self.cmp(other)) }
|
||||
}
|
||||
|
||||
// Double where we specifically care about comparing the raw bits rather than
|
||||
// caring about IEEE semantics.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct RawDouble(pub f64);
|
||||
impl PartialEq for RawDouble {
|
||||
fn eq(&self, other: &Self) -> bool { self.0.to_bits() == other.0.to_bits() }
|
||||
}
|
||||
impl Eq for RawDouble {}
|
||||
impl Ord for RawDouble {
|
||||
fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.0.to_bits().cmp(&other.0.to_bits()) }
|
||||
}
|
||||
impl PartialOrd for RawDouble {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> { Some(self.cmp(other)) }
|
||||
}
|
||||
|
||||
75
objdiff-core/tests/arch_arm.rs
Normal file
75
objdiff-core/tests/arch_arm.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use objdiff_core::{diff, obj};
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
#[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,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
let symbol_idx =
|
||||
obj.symbols.iter().position(|s| s.name == "_ZN13LinkStateItem12OnStateLeaveEi").unwrap();
|
||||
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[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, diff::DiffSide::Base)
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
let symbol_idx = obj
|
||||
.symbols
|
||||
.iter()
|
||||
.position(|s| s.name == "THUMB_BRANCH_ServerDisplay_UncategorizedMove")
|
||||
.unwrap();
|
||||
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[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,
|
||||
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);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "arm")]
|
||||
fn trim_trailing_hword() {
|
||||
let diff_config = diff::DiffObjConfig::default();
|
||||
let obj = obj::read::parse(
|
||||
include_object!("data/arm/issue_253.o"),
|
||||
&diff_config,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
let symbol_idx = obj.symbols.iter().position(|s| s.name == "sub_8030F20").unwrap();
|
||||
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
82
objdiff-core/tests/arch_mips.rs
Normal file
82
objdiff-core/tests/arch_mips.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use objdiff_core::{diff, obj};
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
#[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, 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();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[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,
|
||||
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,
|
||||
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 =
|
||||
obj_le.symbols.iter().position(|s| s.name == "func_00000000__FPcPc").unwrap();
|
||||
let (left_diff, right_diff) =
|
||||
diff::code::diff_code(&obj_be, &obj_le, left_symbol_idx, right_symbol_idx, &diff_config)
|
||||
.unwrap();
|
||||
// Although the objects differ in endianness, the instructions should match.
|
||||
assert_eq!(left_diff.instruction_rows[0].kind, diff::InstructionDiffKind::None);
|
||||
assert_eq!(right_diff.instruction_rows[0].kind, diff::InstructionDiffKind::None);
|
||||
assert_eq!(left_diff.instruction_rows[1].kind, diff::InstructionDiffKind::None);
|
||||
assert_eq!(right_diff.instruction_rows[1].kind, diff::InstructionDiffKind::None);
|
||||
assert_eq!(left_diff.instruction_rows[2].kind, diff::InstructionDiffKind::None);
|
||||
assert_eq!(right_diff.instruction_rows[2].kind, diff::InstructionDiffKind::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[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,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(obj.symbols);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "mips")]
|
||||
fn ido_mdebug_line_numbers() {
|
||||
let diff_config = diff::DiffObjConfig::default();
|
||||
let obj = obj::read::parse(
|
||||
include_object!("data/mips/ido_lines_example.o"),
|
||||
&diff_config,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let text_section = obj.sections.iter().find(|s| s.name == ".text").unwrap();
|
||||
assert_eq!(text_section.line_info.get(&0), Some(&6));
|
||||
assert_eq!(text_section.line_info.get(&12), Some(&7));
|
||||
assert_eq!(text_section.line_info.get(&56), Some(&9));
|
||||
assert_eq!(text_section.line_info.len(), 66);
|
||||
}
|
||||
124
objdiff-core/tests/arch_ppc.rs
Normal file
124
objdiff-core/tests/arch_ppc.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use objdiff_core::{
|
||||
diff::{self, display},
|
||||
obj,
|
||||
obj::SectionKind,
|
||||
};
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
#[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, diff::DiffSide::Base)
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
let symbol_idx =
|
||||
obj.symbols.iter().position(|s| s.name == "Type2Text__10SObjectTagFUi").unwrap();
|
||||
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[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,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
let line_infos = obj
|
||||
.sections
|
||||
.iter()
|
||||
.filter(|s| s.kind == SectionKind::Code)
|
||||
.map(|s| s.line_info.clone())
|
||||
.collect::<Vec<_>>();
|
||||
insta::assert_debug_snapshot!(line_infos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[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,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ppc")]
|
||||
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,
|
||||
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();
|
||||
|
||||
let target_diff = diff.left.as_ref().unwrap();
|
||||
let base_diff = diff.right.as_ref().unwrap();
|
||||
let sections_display = display::display_sections(
|
||||
&target_obj,
|
||||
target_diff,
|
||||
display::SymbolFilter::None,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
insta::assert_debug_snapshot!(sections_display);
|
||||
|
||||
let target_symbol_idx = target_obj
|
||||
.symbols
|
||||
.iter()
|
||||
.position(|s| s.name == "WeaponHurts__20CDamageVulnerabilityCFRC11CWeaponModei")
|
||||
.unwrap();
|
||||
let target_symbol_diff = &target_diff.symbols[target_symbol_idx];
|
||||
let base_symbol_idx = base_obj
|
||||
.symbols
|
||||
.iter()
|
||||
.position(|s| s.name == "WeaponHurts__20CDamageVulnerabilityCFRC11CWeaponModei")
|
||||
.unwrap();
|
||||
let base_symbol_diff = &base_diff.symbols[base_symbol_idx];
|
||||
assert_eq!(target_symbol_diff.target_symbol, Some(base_symbol_idx));
|
||||
assert_eq!(base_symbol_diff.target_symbol, Some(target_symbol_idx));
|
||||
insta::assert_debug_snapshot!((target_symbol_diff, base_symbol_diff));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[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,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
let symbol_idx =
|
||||
obj.symbols.iter().position(|s| s.name == "?FloatingPointExample@@YAXXZ").unwrap();
|
||||
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
124
objdiff-core/tests/arch_x86.rs
Normal file
124
objdiff-core/tests/arch_x86.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use objdiff_core::{diff, diff::display::SymbolFilter, obj};
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
#[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,
|
||||
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();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "x86")]
|
||||
fn read_x86_combine_sections() {
|
||||
let diff_config = diff::DiffObjConfig {
|
||||
combine_data_sections: true,
|
||||
combine_text_sections: true,
|
||||
..Default::default()
|
||||
};
|
||||
let obj =
|
||||
obj::read::parse(include_object!("data/x86/rtest.obj"), &diff_config, diff::DiffSide::Base)
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(obj.sections);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[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,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
let symbol_idx =
|
||||
obj.symbols.iter().position(|s| s.name == "?Dot@Vector@@QEAAMPEAU1@@Z").unwrap();
|
||||
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[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,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
let obj_diff =
|
||||
diff::diff_objs(Some(&obj), None, None, &diff_config, &diff::MappingConfig::default())
|
||||
.unwrap()
|
||||
.left
|
||||
.unwrap();
|
||||
let section_display =
|
||||
diff::display::display_sections(&obj, &obj_diff, SymbolFilter::None, false, false, false);
|
||||
insta::assert_debug_snapshot!(section_display);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[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,
|
||||
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();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
// Inferred size of functions should ignore symbols with specific prefixes
|
||||
#[test]
|
||||
#[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,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "x86")]
|
||||
fn read_x86_indirect_table() {
|
||||
let diff_config = diff::DiffObjConfig::default();
|
||||
let obj = obj::read::parse(
|
||||
include_object!("data/x86/indirect_table.obj"),
|
||||
&diff_config,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(obj);
|
||||
let symbol_idx = obj.symbols.iter().position(|s| s.name == "?process@@YAHHHH@Z").unwrap();
|
||||
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
52
objdiff-core/tests/common.rs
Normal file
52
objdiff-core/tests/common.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use objdiff_core::{
|
||||
diff::{DiffObjConfig, SymbolDiff, display::DiffTextSegment},
|
||||
obj::Object,
|
||||
};
|
||||
|
||||
pub fn display_diff(
|
||||
obj: &Object,
|
||||
diff: &SymbolDiff,
|
||||
symbol_idx: usize,
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> String {
|
||||
let mut output = String::new();
|
||||
for row in &diff.instruction_rows {
|
||||
output.push('[');
|
||||
let mut separator = false;
|
||||
objdiff_core::diff::display::display_row(obj, symbol_idx, row, diff_config, |segment| {
|
||||
if separator {
|
||||
output.push_str(", ");
|
||||
} else {
|
||||
separator = true;
|
||||
}
|
||||
let DiffTextSegment { text, color, pad_to } = segment;
|
||||
output.push_str(&format!("({text:?}, {color:?}, {pad_to:?})"));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
output.push_str("]\n");
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct AlignedAs<Align, Bytes: ?Sized> {
|
||||
pub _align: [Align; 0],
|
||||
pub bytes: Bytes,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! include_bytes_align_as {
|
||||
($align_ty:ty, $path:literal) => {{
|
||||
static ALIGNED: &common::AlignedAs<$align_ty, [u8]> =
|
||||
&common::AlignedAs { _align: [], bytes: *include_bytes!($path) };
|
||||
&ALIGNED.bytes
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! include_object {
|
||||
($path:literal) => {
|
||||
include_bytes_align_as!(u64, $path)
|
||||
};
|
||||
}
|
||||
BIN
objdiff-core/tests/data/arm/LinkStateItem.o
Normal file
BIN
objdiff-core/tests/data/arm/LinkStateItem.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/arm/enemy300.o
Normal file
BIN
objdiff-core/tests/data/arm/enemy300.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/arm/issue_253.o
Normal file
BIN
objdiff-core/tests/data/arm/issue_253.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/arm/thumb.o
Normal file
BIN
objdiff-core/tests/data/arm/thumb.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/mips/code_be.o
Normal file
BIN
objdiff-core/tests/data/mips/code_be.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/mips/code_le.o
Normal file
BIN
objdiff-core/tests/data/mips/code_le.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/mips/ido_lines_example.o
Normal file
BIN
objdiff-core/tests/data/mips/ido_lines_example.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/mips/main.c.o
Normal file
BIN
objdiff-core/tests/data/mips/main.c.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/mips/vw_main.c.o
Normal file
BIN
objdiff-core/tests/data/mips/vw_main.c.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/ppc/CDamageVulnerability_base.o
Normal file
BIN
objdiff-core/tests/data/ppc/CDamageVulnerability_base.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/ppc/CDamageVulnerability_target.o
Normal file
BIN
objdiff-core/tests/data/ppc/CDamageVulnerability_target.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/ppc/IObj.o
Normal file
BIN
objdiff-core/tests/data/ppc/IObj.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/ppc/NMWException.o
Normal file
BIN
objdiff-core/tests/data/ppc/NMWException.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/ppc/m_Do_hostIO.o
Normal file
BIN
objdiff-core/tests/data/ppc/m_Do_hostIO.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/ppc/vmx128.obj
Normal file
BIN
objdiff-core/tests/data/ppc/vmx128.obj
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/x86/basenode.obj
Normal file
BIN
objdiff-core/tests/data/x86/basenode.obj
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/x86/indirect_table.obj
Normal file
BIN
objdiff-core/tests/data/x86/indirect_table.obj
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/x86/jumptable.o
Normal file
BIN
objdiff-core/tests/data/x86/jumptable.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/x86/local_labels.obj
Normal file
BIN
objdiff-core/tests/data/x86/local_labels.obj
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/x86/rtest.obj
Normal file
BIN
objdiff-core/tests/data/x86/rtest.obj
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/x86/staticdebug.obj
Normal file
BIN
objdiff-core/tests/data/x86/staticdebug.obj
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/x86_64/vs2022.o
Normal file
BIN
objdiff-core/tests/data/x86_64/vs2022.o
Normal file
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_arm.rs
|
||||
expression: output
|
||||
---
|
||||
[(Line(90), Dim, 5), (Address(0), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r12")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(8), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(90), Dim, 5), (Address(4), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bx", 32777), Normal, 10), (Argument(Opaque("r12")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(90), Dim, 5), (Address(8), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Symbol(Symbol { name: "esEnemyDraw", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_arm.rs
|
||||
expression: diff.instruction_rows
|
||||
---
|
||||
[
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 76,
|
||||
size: 4,
|
||||
opcode: 32792,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 80,
|
||||
size: 4,
|
||||
opcode: 32777,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 84,
|
||||
size: 4,
|
||||
opcode: 65534,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
]
|
||||
1876
objdiff-core/tests/snapshots/arch_arm__read_arm-2.snap
Normal file
1876
objdiff-core/tests/snapshots/arch_arm__read_arm-2.snap
Normal file
File diff suppressed because it is too large
Load Diff
112
objdiff-core/tests/snapshots/arch_arm__read_arm-3.snap
Normal file
112
objdiff-core/tests/snapshots/arch_arm__read_arm-3.snap
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_arm.rs
|
||||
expression: output
|
||||
---
|
||||
[(Address(0), Dim, 5), (Spacing(4), Normal, 0), (Opcode("stmdb", 32883), Normal, 10), (Argument(Opaque("sp")), Normal, 0), (Basic("!"), Normal, 0), (Basic(", "), Normal, 0), (Basic("{"), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("lr")), Normal, 0), (Basic("}"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(4), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(8), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r1")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(12), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateBase12OnStateLeaveEi", demangled_name: Some("LinkStateBase::OnStateLeave(int)"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(16), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(20)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(20), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32784), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(10)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(24), Dim, 5), (Spacing(4), Normal, 0), (Opcode("addls", 32769), Normal, 10), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("lsl")), Normal, 0), (Spacing(1), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(2)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(28), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(32), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(36), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(40), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(44), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(192), Normal, 0), (Basic(" ~>"), Rotating(1), 0), (Eol, Normal, 0)]
|
||||
[(Address(48), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(124), Normal, 0), (Basic(" ~>"), Rotating(2), 0), (Eol, Normal, 0)]
|
||||
[(Address(52), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(56), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(140), Normal, 0), (Basic(" ~>"), Rotating(3), 0), (Eol, Normal, 0)]
|
||||
[(Address(60), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(76), Normal, 0), (Basic(" ~>"), Rotating(4), 0), (Eol, Normal, 0)]
|
||||
[(Address(64), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(152), Normal, 0), (Basic(" ~>"), Rotating(5), 0), (Eol, Normal, 0)]
|
||||
[(Address(68), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(164), Normal, 0), (Basic(" ~>"), Rotating(6), 0), (Eol, Normal, 0)]
|
||||
[(Address(72), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(164), Normal, 0), (Basic(" ~>"), Rotating(6), 0), (Eol, Normal, 0)]
|
||||
[(Address(76), Dim, 5), (Basic(" ~> "), Rotating(4), 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(336)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(420), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(80), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(84), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN18UnkStruct_027e103c19func_ov000_020cf01cEv", demangled_name: Some("UnkStruct_027e103c::func_ov000_020cf01c()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(88), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldrb", 32793), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(224)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(92), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32784), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(96), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bne", 32772), Normal, 10), (BranchDest(108), Normal, 0), (Basic(" ~>"), Rotating(7), 0), (Eol, Normal, 0)]
|
||||
[(Address(100), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (BranchDest(424), Normal, 0), (Basic(" ~>"), Rotating(8), 0), (Eol, Normal, 0)]
|
||||
[(Address(104), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN12EquipBombchu19func_ov014_0213ec64Ev", demangled_name: Some("EquipBombchu::func_ov014_0213ec64()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(108), Dim, 5), (Basic(" ~> "), Rotating(7), 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(308)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(424), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(112), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(116), Dim, 5), (Spacing(4), Normal, 0), (Opcode("blx", 32776), Normal, 10), (Symbol(Symbol { name: "_Z19func_ov014_0211fd04Pi", demangled_name: Some("func_ov014_0211fd04(int*)"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(120), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(124), Dim, 5), (Basic(" ~> "), Rotating(2), 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(128), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(132), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateItem13StopUsingBombEi", demangled_name: Some("LinkStateItem::StopUsingBomb(int)"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(136), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(140), Dim, 5), (Basic(" ~> "), Rotating(3), 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(144), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateItem13StopUsingRopeEv", demangled_name: Some("LinkStateItem::StopUsingRope()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(148), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(152), Dim, 5), (Basic(" ~> "), Rotating(5), 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(156), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateItem15StopUsingHammerEv", demangled_name: Some("LinkStateItem::StopUsingHammer()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(160), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(164), Dim, 5), (Basic(" ~> "), Rotating(6), 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(248)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(420), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(168), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(172), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(176), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r1")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(180), Dim, 5), (Spacing(4), Normal, 0), (Opcode("strb", 32885), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(42)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(184), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN18UnkStruct_027e103c19func_ov000_020cf9dcEii", demangled_name: Some("UnkStruct_027e103c::func_ov000_020cf9dc(int, int)"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(188), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
|
||||
[(Address(192), Dim, 5), (Basic(" ~> "), Rotating(1), 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(196), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateItem14StopUsingScoopEv", demangled_name: Some("LinkStateItem::StopUsingScoop()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(200), Dim, 5), (Basic(" ~> "), Rotating(0), 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(20)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(204), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mvn", 32819), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(208), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32784), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(212), Dim, 5), (Spacing(4), Normal, 0), (Opcode("beq", 32772), Normal, 10), (BranchDest(236), Normal, 0), (Basic(" ~>"), Rotating(9), 0), (Eol, Normal, 0)]
|
||||
[(Address(216), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(220), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateBase12GetEquipItemEi", demangled_name: Some("LinkStateBase::GetEquipItem(int)"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(224), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(228), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(28)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(232), Dim, 5), (Spacing(4), Normal, 0), (Opcode("blx", 32776), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(236), Dim, 5), (Basic(" ~> "), Rotating(9), 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(20)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(240), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32784), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(9)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(244), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bgt", 32772), Normal, 10), (BranchDest(288), Normal, 0), (Basic(" ~>"), Rotating(10), 0), (Eol, Normal, 0)]
|
||||
[(Address(248), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bge", 32772), Normal, 10), (BranchDest(296), Normal, 0), (Basic(" ~>"), Rotating(11), 0), (Eol, Normal, 0)]
|
||||
[(Address(252), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32784), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(256), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bgt", 32772), Normal, 10), (BranchDest(308), Normal, 0), (Basic(" ~>"), Rotating(12), 0), (Eol, Normal, 0)]
|
||||
[(Address(260), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mvn", 32819), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(264), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32784), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(268), Dim, 5), (Spacing(4), Normal, 0), (Opcode("blt", 32772), Normal, 10), (BranchDest(308), Normal, 0), (Basic(" ~>"), Rotating(12), 0), (Eol, Normal, 0)]
|
||||
[(Address(272), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmpne", 32784), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(276), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmpne", 32784), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(280), Dim, 5), (Spacing(4), Normal, 0), (Opcode("beq", 32772), Normal, 10), (BranchDest(340), Normal, 0), (Basic(" ~>"), Rotating(13), 0), (Eol, Normal, 0)]
|
||||
[(Address(284), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(308), Normal, 0), (Basic(" ~>"), Rotating(12), 0), (Eol, Normal, 0)]
|
||||
[(Address(288), Dim, 5), (Basic(" ~> "), Rotating(10), 0), (Opcode("cmp", 32784), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(10)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(292), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bne", 32772), Normal, 10), (BranchDest(308), Normal, 0), (Basic(" ~>"), Rotating(12), 0), (Eol, Normal, 0)]
|
||||
[(Address(296), Dim, 5), (Basic(" ~> "), Rotating(11), 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(300), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateBase18EquipItem_vfunc_28Ev", demangled_name: Some("LinkStateBase::EquipItem_vfunc_28()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(304), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32772), Normal, 10), (BranchDest(340), Normal, 0), (Basic(" ~>"), Rotating(13), 0), (Eol, Normal, 0)]
|
||||
[(Address(308), Dim, 5), (Basic(" ~> "), Rotating(12), 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(312), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateBase18EquipItem_vfunc_28Ev", demangled_name: Some("LinkStateBase::EquipItem_vfunc_28()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(316), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32784), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(4)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(320), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmpne", 32784), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(2)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(324), Dim, 5), (Spacing(4), Normal, 0), (Opcode("beq", 32772), Normal, 10), (BranchDest(340), Normal, 0), (Basic(" ~>"), Rotating(13), 0), (Eol, Normal, 0)]
|
||||
[(Address(328), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN13LinkStateItem16GetLinkStateMoveEv", demangled_name: Some("LinkStateItem::GetLinkStateMove()"), address: 488, size: 16, kind: Function, section: Some(0), flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(332), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(1)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(336), Dim, 5), (Spacing(4), Normal, 0), (Opcode("strb", 32885), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(20)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(340), Dim, 5), (Basic(" ~> "), Rotating(13), 0), (Opcode("mvn", 32819), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(344), Dim, 5), (Spacing(4), Normal, 0), (Opcode("add", 32769), Normal, 10), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(80)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(348), Dim, 5), (Spacing(4), Normal, 0), (Opcode("add", 32769), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(88)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(352), Dim, 5), (Spacing(4), Normal, 0), (Opcode("str", 32884), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(24)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(356), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32784), Normal, 10), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(360), Dim, 5), (Spacing(4), Normal, 0), (Opcode("beq", 32772), Normal, 10), (BranchDest(384), Normal, 0), (Basic(" ~>"), Rotating(14), 0), (Eol, Normal, 0)]
|
||||
[(Address(364), Dim, 5), (Basic(" ~> "), Rotating(15), 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(368), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_Z19func_ov000_020b7e6cPi", demangled_name: Some("func_ov000_020b7e6c(int*)"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(372), Dim, 5), (Spacing(4), Normal, 0), (Opcode("add", 32769), Normal, 10), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(4)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(376), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32784), Normal, 10), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(380), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bne", 32772), Normal, 10), (BranchDest(364), Normal, 0), (Basic(" ~>"), Rotating(15), 0), (Eol, Normal, 0)]
|
||||
[(Address(384), Dim, 5), (Basic(" ~> "), Rotating(14), 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(36)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(428), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(388), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(392), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldrb", 32793), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(128)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(396), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32784), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(400), Dim, 5), (Spacing(4), Normal, 0), (Opcode("beq", 32772), Normal, 10), (BranchDest(408), Normal, 0), (Basic(" ~>"), Rotating(16), 0), (Eol, Normal, 0)]
|
||||
[(Address(404), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32775), Normal, 10), (Symbol(Symbol { name: "_ZN13PlayerControl13StopFollowingEv", demangled_name: Some("PlayerControl::StopFollowing()"), address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Addend(-8), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(408), Dim, 5), (Basic(" ~> "), Rotating(16), 0), (Opcode("mov", 32811), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(412), Dim, 5), (Spacing(4), Normal, 0), (Opcode("strb", 32885), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(38)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(416), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldmia", 32791), Normal, 10), (Argument(Opaque("sp")), Normal, 0), (Basic("!"), Normal, 0), (Basic(", "), Normal, 0), (Basic("{"), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic("}"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(420), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Symbol(Symbol { name: "data_027e103c", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(424), Dim, 5), (Basic(" ~> "), Rotating(8), 0), (Opcode(".word", 65534), Normal, 10), (Symbol(Symbol { name: "data_027e1098", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(428), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Symbol(Symbol { name: "gPlayerControl", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
1971
objdiff-core/tests/snapshots/arch_arm__read_arm.snap
Normal file
1971
objdiff-core/tests/snapshots/arch_arm__read_arm.snap
Normal file
File diff suppressed because it is too large
Load Diff
1608
objdiff-core/tests/snapshots/arch_arm__read_thumb-2.snap
Normal file
1608
objdiff-core/tests/snapshots/arch_arm__read_thumb-2.snap
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user