Compare commits

..

No commits in common. "main" and "v2.3.4" have entirely different histories.
main ... v2.3.4

180 changed files with 12304 additions and 71800 deletions

View File

@ -1,4 +1,5 @@
# 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"]
[target.x86_64-pc-windows-msvc]
linker = "rust-lld"
[target.aarch64-pc-windows-msvc]
linker = "rust-lld"

View File

@ -1,2 +0,0 @@
[*.md]
trim_trailing_whitespace = false

View File

@ -4,16 +4,13 @@ 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:
@ -36,9 +33,9 @@ jobs:
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo check
run: cargo check --all-targets --all-features --workspace
run: cargo check --all-features --all-targets
- name: Cargo clippy
run: cargo clippy --all-targets --all-features --workspace
run: cargo clippy --all-features --all-targets
fmt:
name: Format
@ -68,12 +65,13 @@ jobs:
continue-on-error: ${{ matrix.checks == 'advisories' }}
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
- uses: EmbarkStudios/cargo-deny-action@v1
with:
command: check ${{ matrix.checks }}
test:
name: Test
if: 'false' # No tests yet
strategy:
matrix:
platform: [ ubuntu-latest, windows-latest, macos-latest ]
@ -92,7 +90,7 @@ jobs:
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo test
run: cargo test --release --all-features --workspace
run: cargo test --release --all-features
build-cli:
name: Build objdiff-cli
@ -146,14 +144,13 @@ 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: |
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
python3 -m venv .venv
. .venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
@ -164,15 +161,15 @@ jobs:
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: |
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
${{ 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
if-no-files-found: error
build-gui:
@ -183,26 +180,21 @@ jobs:
matrix:
include:
- platform: ubuntu-latest
target: x86_64-unknown-linux-gnu.2.31
target_base: x86_64-unknown-linux-gnu
target: 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 }}
@ -214,71 +206,38 @@ 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_base || matrix.target }}
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 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: |
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
${{ 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
if-no-files-found: error
build-wasm:
name: Build objdiff-wasm
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: 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
release:
name: Release
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: [ build-cli, build-gui, build-wasm ]
needs: [ build-cli, build-gui ]
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check git tag against package versions
- name: Check git tag against Cargo version
shell: bash
run: |
set -eou pipefail
@ -290,24 +249,9 @@ jobs:
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
@ -337,53 +281,3 @@ jobs:
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"

2
.gitignore vendored
View File

@ -18,4 +18,4 @@ android.keystore
*.frag
*.vert
*.metal
.vscode/
.vscode/launch.json

View File

@ -1,36 +0,0 @@
# 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

View File

@ -1,44 +0,0 @@
# 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.

4189
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,23 +3,8 @@ members = [
"objdiff-cli",
"objdiff-core",
"objdiff-gui",
"objdiff-wasm",
]
default-members = [
"objdiff-cli",
"objdiff-core",
"objdiff-gui",
# Exclude objdiff-wasm by default
]
resolver = "3"
[workspace.package]
version = "3.3.0"
authors = ["Luke Street <luke@street.dev>"]
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
rust-version = "1.88"
resolver = "2"
[profile.release-lto]
inherits = "release"
@ -27,6 +12,10 @@ lto = "fat"
strip = "debuginfo"
codegen-units = 1
[profile.release-min]
inherits = "release-lto"
opt-level = "z"
[workspace.package]
version = "2.3.4"
authors = ["Luke Street <luke@street.dev>"]
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
rust-version = "1.74"

144
README.md
View File

@ -7,23 +7,19 @@ A local diffing tool for decompilation projects. Inspired by [decomp.me](https:/
Features:
- 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)
- Compare entire object files: functions and data.
- Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC)
- Automatic rebuild on source file changes.
- Project integration via [configuration file](#configuration).
- Search and filter all of a project's objects and quickly switch.
- Click to highlight all instances of values and registers.
Supports:
- ARM (GBA, DS, 3DS)
- ARM64 (Switch)
- PowerPC 750CL (GameCube, Wii)
- MIPS (N64, PS1, PS2, PSP)
- PowerPC (GameCube, Wii, PS3, Xbox 360)
- SuperH (Saturn, Dreamcast)
- x86, x86_64 (PC)
- x86 (COFF only at the moment)
- ARM (GBA, DS, 3DS)
See [Usage](#usage) for more information.
@ -42,7 +38,7 @@ 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).
CLI binaries can be found on the [releases page](https://github.com/encounter/objdiff/releases).
## Screenshots
@ -51,30 +47,33 @@ CLI binaries are available on the [releases page](https://github.com/encounter/o
## Usage
objdiff compares two relocatable object files (`.o`). Here's how it works:
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.
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.
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:
2. **Load the project** in objdiff.
- Target build directory: `build/asm`
- Base build directory: `build/src`
- Object: `MetroTRK/mslsupp.o`
3. **Select an object** from the sidebar to begin diffing.
objdiff will then execute the build system from the project directory to build both objects:
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
```sh
$ make build/asm/MetroTRK/mslsupp.o # Only if "Build target object" is enabled
$ make build/src/MetroTRK/mslsupp.o
```
The configuration file allows complete flexibility in project structure - your build directories can have any layout as long as the paths are specified correctly.
The objects will then be compared and the results will be displayed in the UI.
See [Configuration](#configuration) for setup details.
See [Configuration](#configuration) for more information.
## Configuration
Projects can add an `objdiff.json` file to configure the tool automatically. The configuration file must be located in
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
the root project directory.
If your project has a generator script (e.g. `configure.py`), it's highly recommended to generate the objdiff configuration
If your project has a generator script (e.g. `configure.py`), it's 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
@ -89,31 +88,22 @@ 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",
@ -127,70 +117,60 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
### Schema
> [!NOTE]
> View [config.schema.json](config.schema.json) for all available options. Below is a summary of the most important options.
View [config.schema.json](config.schema.json) for all available options. The below list is a summary of the most important options.
#### Build Configuration
`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`.
**`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`.
`custom_args` _(optional)_: Additional arguments to pass to the build command prior to the object path.
**`custom_args`** _(optional)_
Additional arguments to pass to the build command prior to the object path.
`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.
**`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.
`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_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.
`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.
#### File Watching
`units` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
**`watch_patterns`** _(optional, default: listed above)_
A list of glob patterns to watch for changes ([supported syntax](https://docs.rs/globset/latest/globset/#syntax)). When these files change, objdiff automatically rebuilds and re-compares objects.
**`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`.
> `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".
## Building
Install Rust via [rustup](https://rustup.rs).
```shell
git clone https://github.com/encounter/objdiff.git
cd objdiff
cargo run --release
$ git clone https://github.com/encounter/objdiff.git
$ cd objdiff
$ cargo run --release
```
Or install directly with cargo:
Or using `cargo install`.
```shell
cargo install --locked --git https://github.com/encounter/objdiff.git objdiff-gui objdiff-cli
$ cargo install --locked --git https://github.com/encounter/objdiff.git objdiff-gui objdiff-cli
```
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
```
The binaries will be installed to `~/.cargo/bin` as `objdiff` and `objdiff-cli`.
## License

View File

@ -15,7 +15,7 @@
},
"custom_make": {
"type": "string",
"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`.",
"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`.",
"examples": [
"make",
"ninja"
@ -41,55 +41,39 @@
},
"build_target": {
"type": "boolean",
"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.",
"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.",
"default": false
},
"build_base": {
"type": "boolean",
"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.",
"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.",
"default": true
},
"watch_patterns": {
"type": "array",
"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",
"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",
"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.",
@ -111,25 +95,6 @@
"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": {
@ -138,7 +103,7 @@
"properties": {
"name": {
"type": "string",
"description": "The display name in the UI. Defaults to the object's path."
"description": "The name of the object in the UI. If not specified, the object's path will be used."
},
"path": {
"type": "string",
@ -147,11 +112,11 @@
},
"target_path": {
"type": "string",
"description": "Path to the \"target\" or \"expected\" object (the intended result)."
"description": "Path to the target object from the project root.\nRequired if path is not specified."
},
"base_path": {
"type": "string",
"description": "Path to the \"base\" or \"current\" object (built from current source code).\nOmit if there is no source object yet."
"description": "Path to the base object from the project root.\nRequired if path is not specified."
},
"reverse_fn_order": {
"type": "boolean",
@ -175,20 +140,6 @@
"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"
}
]
}
}
}
},
@ -224,10 +175,6 @@
"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": [
@ -240,7 +187,7 @@
"properties": {
"complete": {
"type": "boolean",
"description": "Marks the object as \"complete\" (linked) when `true` or \"incomplete\" when `false`."
"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\"."
},
"reverse_fn_order": {
"type": "boolean",
@ -260,7 +207,7 @@
},
"auto_generated": {
"type": "boolean",
"description": "Hides the object from the sidebar but includes it in progress reports."
"description": "Hides the object from the object list by default, but still includes it in reports."
}
}
},

View File

@ -73,8 +73,6 @@ ignore = [
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate is an indirect dependency" },
{ id = "RUSTSEC-2025-0052", reason = "Unmaintained async-std crate is an indirect dependency" },
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
@ -99,12 +97,12 @@ allow = [
"BSL-1.0",
"CC0-1.0",
"MPL-2.0",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
"0BSD",
"OFL-1.1",
"Ubuntu-font-1.0",
"CDLA-Permissive-2.0",
"LicenseRef-UFL-1.0",
"OpenSSL",
]
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
@ -241,10 +239,7 @@ allow-git = []
[sources.allow-org]
# github.com organizations to allow git sources for
github = [
"encounter",
"gimli-rs", # gimli
]
github = ["notify-rs"]
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for

View File

@ -14,13 +14,13 @@ publish = false
[dependencies]
anyhow = "1.0"
argp = "0.4"
crossterm = "0.29"
argp = "0.3"
crossterm = "0.28"
enable-ansi-support = "0.2"
memmap2 = "0.9"
objdiff-core = { path = "../objdiff-core", features = ["all"] }
prost = "0.14"
ratatui = "0.29"
prost = "0.13"
ratatui = "0.28"
rayon = "1.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@ -28,7 +28,6 @@ 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.11"
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1"

View File

@ -4,7 +4,7 @@
//! For now, this only adds a --version/-V option which causes early-exit.
use std::ffi::OsStr;
use argp::{EarlyExit, FromArgs, TopLevelCommand, parser::ParseGlobalOptions};
use argp::{parser::ParseGlobalOptions, EarlyExit, FromArgs, TopLevelCommand};
struct ArgsOrVersion<T>(T)
where T: FromArgs;

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,2 @@
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(())
}

View File

@ -1,25 +1,28 @@
use std::{collections::HashSet, fs::File, io::Read, time::Instant};
use std::{
collections::HashSet,
fs::File,
io::Read,
path::{Path, PathBuf},
time::Instant,
};
use anyhow::{Context, Result, bail};
use anyhow::{bail, Context, Result};
use argp::FromArgs;
use objdiff_core::{
bindings::report::{
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, REPORT_VERSION,
Report, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report,
ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
REPORT_VERSION,
},
config::{ProjectObject, ProjectOptions, apply_project_options, path::platform_path},
diff,
obj::{self, SectionKind, SymbolFlag, SymbolKind},
config::ProjectObject,
diff, obj,
obj::{ObjSectionKind, ObjSymbolFlags},
};
use prost::Message;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
use tracing::{info, warn};
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
use crate::{
cmd::{apply_config_args, diff::ObjectConfig},
util::output::{OutputFormat, write_output},
};
use crate::util::output::{write_output, OutputFormat};
#[derive(FromArgs, PartialEq, Debug)]
/// Generate a progress report for a project.
@ -40,36 +43,33 @@ pub enum SubCommand {
/// Generate a progress report for a project.
#[argp(subcommand, name = "generate")]
pub struct GenerateArgs {
#[argp(option, short = 'p', from_str_fn(platform_path))]
#[argp(option, short = 'p')]
/// Project directory
project: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'o', from_str_fn(platform_path))]
project: Option<PathBuf>,
#[argp(option, short = 'o')]
/// Output file
output: Option<Utf8PlatformPathBuf>,
output: Option<PathBuf>,
#[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, from_str_fn(platform_path))]
#[argp(positional)]
/// Previous report file
previous: Utf8PlatformPathBuf,
#[argp(positional, from_str_fn(platform_path))]
previous: PathBuf,
#[argp(positional)]
/// Current report file
current: Utf8PlatformPathBuf,
#[argp(option, short = 'o', from_str_fn(platform_path))]
current: PathBuf,
#[argp(option, short = 'o')]
/// Output file
output: Option<Utf8PlatformPathBuf>,
output: Option<PathBuf>,
#[argp(option, short = 'f')]
/// Output format (json, json-pretty, proto) (default: json)
format: Option<String>,
@ -83,46 +83,18 @@ pub fn run(args: Args) -> Result<()> {
}
fn generate(args: GenerateArgs) -> Result<()> {
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 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_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
info!("Loading project {}", project_dir.display());
let project = match objdiff_core::config::try_project_config(project_dir.as_ref()) {
let mut project = match objdiff_core::config::try_project_config(project_dir) {
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)",
objects.len(),
project.units().len(),
if args.deduplicate { 1 } else { rayon::current_num_threads() }
);
@ -131,29 +103,31 @@ 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, 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))?
{
for object in project.units.as_deref_mut().unwrap_or_default() {
if let Some(unit) = report_object(
object,
project_dir,
project.target_dir.as_deref(),
project.base_dir.as_deref(),
Some(&mut existing_functions),
)? {
units.push(unit);
}
}
} else {
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)
let vec = project
.units
.as_deref_mut()
.unwrap_or_default()
.par_iter_mut()
.map(|object| {
report_object(
object,
project_dir,
project.target_dir.as_deref(),
project.base_dir.as_deref(),
None,
)
})
.collect::<Result<Vec<Option<ReportUnit>>>>()?;
units = vec.into_iter().flatten().collect();
@ -176,70 +150,56 @@ 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: &ObjectConfig,
diff_config: &diff::DiffObjConfig,
object: &mut ProjectObject,
project_dir: &Path,
target_dir: Option<&Path>,
base_dir: Option<&Path>,
mut existing_functions: Option<&mut HashSet<String>>,
) -> Result<Option<ReportUnit>> {
object.resolve_paths(project_dir, target_dir, base_dir);
match (&object.target_path, &object.base_path) {
(None, Some(_)) if !object.complete.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 mapping_config = diff::MappingConfig::default();
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
let target = object
.target_path
.as_ref()
.map(|p| {
obj::read::read(p.as_ref(), diff_config, diff::DiffSide::Target)
.with_context(|| format!("Failed to open {p}"))
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
})
.transpose()?;
let base = object
.base_path
.as_ref()
.map(|p| {
obj::read::read(p.as_ref(), diff_config, diff::DiffSide::Base)
.with_context(|| format!("Failed to open {p}"))
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
})
.transpose()?;
let result =
diff::diff_objs(target.as_ref(), base.as_ref(), None, diff_config, &mapping_config)?;
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
let metadata = ReportUnitMetadata {
complete: object.metadata.complete,
complete: object.complete(),
module_name: target
.as_ref()
.and_then(|o| o.split_meta.as_ref())
.and_then(|m| m.module_name.clone()),
module_id: target.as_ref().and_then(|o| o.split_meta.as_ref()).and_then(|m| m.module_id),
source_path: 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,
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),
};
let mut measures = Measures { total_units: 1, ..Default::default() };
let mut sections = vec![];
@ -247,16 +207,15 @@ fn report_object(
let obj = target.as_ref().or(base.as_ref()).unwrap();
let obj_diff = result.left.as_ref().or(result.right.as_ref()).unwrap();
for ((section_idx, section), section_diff) in
obj.sections.iter().enumerate().zip(&obj_diff.sections)
{
if section.kind == SectionKind::Unknown {
continue;
}
for (section, section_diff) in obj.sections.iter().zip(&obj_diff.sections) {
let section_match_percent = section_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object,
// assume complete means 100% match
if object.complete.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(),
@ -266,40 +225,39 @@ fn report_object(
demangled_name: None,
virtual_address: section.virtual_address,
}),
address: None,
});
match section.kind {
SectionKind::Data | SectionKind::Bss => {
ObjSectionKind::Data | ObjSectionKind::Bss => {
measures.total_data += section.size;
if section_match_percent == 100.0 {
measures.matched_data += section.size;
}
continue;
}
_ => {}
ObjSectionKind::Code => (),
}
for (symbol, symbol_diff) in 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
{
for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.symbols) {
if symbol.size == 0 || symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
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;
if let Some(existing_functions) = &mut existing_functions {
if (symbol.flags.0.contains(ObjSymbolFlags::Global)
|| symbol.flags.0.contains(ObjSymbolFlags::Weak))
&& !existing_functions.insert(symbol.name.clone())
{
continue;
}
}
let match_percent = symbol_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object,
// assume complete means 100% match
if object.complete.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;
@ -314,7 +272,6 @@ 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;
@ -322,16 +279,6 @@ 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;
@ -340,7 +287,7 @@ fn report_object(
measures.calc_fuzzy_match_percent();
measures.calc_matched_percent();
Ok(Some(ReportUnit {
name: object.name.clone(),
name: object.name().to_string(),
measures: Some(measures),
sections,
functions,
@ -350,7 +297,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 == "-" && args.current == "-" {
let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") {
// Special case for comparing two reports from stdin
let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?;
@ -465,14 +412,15 @@ fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
.collect()
}
fn read_report(path: &Utf8PlatformPath) -> Result<Report> {
if path == Utf8PlatformPath::new("-") {
fn read_report(path: &Path) -> Result<Report> {
if path == Path::new("-") {
let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?;
return Report::parse(&data).with_context(|| "Failed to load report from stdin");
}
let file = File::open(path).with_context(|| format!("Failed to open {path}"))?;
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}"))
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()))
}

View File

@ -1,9 +1,6 @@
#![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.
@ -17,7 +14,7 @@ use anyhow::{Error, Result};
use argp::{FromArgValue, FromArgs};
use enable_ansi_support::enable_ansi_support;
use supports_color::Stream;
use tracing_subscriber::{EnvFilter, filter::LevelFilter};
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
enum LogLevel {

View File

@ -5,7 +5,7 @@ use std::{
path::Path,
};
use anyhow::{Context, Result, bail};
use anyhow::{bail, Context, Result};
use tracing::info;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
@ -34,12 +34,9 @@ impl OutputFormat {
}
}
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()) {
pub fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
where T: serde::Serialize + prost::Message {
match output {
Some(output) if output != Path::new("-") => {
info!("Writing to {}", output.display());
let file = File::options()

View File

@ -3,7 +3,7 @@ use std::{io::stdout, panic};
use crossterm::{
cursor::Show,
event::DisableMouseCapture,
terminal::{LeaveAlternateScreen, disable_raw_mode},
terminal::{disable_raw_mode, LeaveAlternateScreen},
};
pub fn crossterm_panic_handler() {

View File

@ -1,654 +0,0 @@
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') => {
result.redraw = true;
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,
};
result.redraw = true;
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]))
}

View File

@ -1,25 +0,0 @@
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<()>;
}

View File

@ -12,201 +12,70 @@ A local diffing tool for decompilation projects.
"""
documentation = "https://docs.rs/objdiff-core"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
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",
]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
any-arch = ["config", "dep:bimap", "dep:strum", "dep:similar", "dep:flagset", "dep:log", "dep:memmap2", "dep:byteorder", "dep:num-traits"] # Implicit, used to check if any arch is enabled
config = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"]
dwarf = ["dep:gimli"]
mips = ["any-arch", "dep:rabbitizer"]
ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"]
x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"]
arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"]
bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"]
wasm = ["bindings", "any-arch", "dep:console_error_panic_hook", "dep:console_log", "dep:wasm-bindgen", "dep:tsify-next", "dep:log"]
[package.metadata.docs.rs]
features = ["all"]
[dependencies]
anyhow = { version = "1.0", default-features = false }
anyhow = "1.0"
bimap = { version = "0.6", features = ["serde"], optional = true }
byteorder = { version = "1.5", optional = true }
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 }
flagset = { version = "0.4", optional = true }
log = { version = "0.4", 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.11", 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.11", default-features = false, optional = true }
num-traits = { version = "0.2", optional = true }
object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false }
pbjson = { version = "0.7", optional = true }
prost = { version = "0.13", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
similar = { version = "2.6", default-features = false, optional = true }
strum = { version = "0.26", features = ["derive"], optional = true }
wasm-bindgen = { version = "0.2", optional = true }
tsify-next = { version = "0.5", default-features = false, features = ["js"], optional = true }
console_log = { version = "1.0", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
# config
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 }
globset = { version = "0.4", features = ["serde1"], optional = true }
semver = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
# dwarf
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 }
gimli = { version = "0.31", default-features = false, features = ["read-all"], optional = true }
# ppc
cwextab = { version = "1.1", optional = true }
powerpc = { version = "0.4", optional = true }
rlwinmdec = { version = "1.1", optional = true }
cwdemangle = { version = "1.0", optional = true }
cwextab = { version = "1.0.2", optional = true }
ppc750cl = { version = "0.3", optional = true }
# mips
rabbitizer = { version = "2.0.0-alpha.4", default-features = false, features = ["all_extensions"], optional = true }
rabbitizer = { version = "1.12", optional = true }
# x86
iced-x86 = { version = "1.21", default-features = false, features = ["decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums", "no_std"], optional = true }
cpp_demangle = { version = "0.4", optional = true }
iced-x86 = { version = "1.21", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
msvc-demangler = { version = "0.10", optional = true }
# arm
unarm = { version = "1.9", 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.1.0", optional = true }
notify-debouncer-full = { version = "0.5.0", optional = true }
shell-escape = { version = "0.1", optional = true }
tempfile = { version = "3.20", optional = true }
time = { version = "0.3", optional = true }
encoding_rs = { version = "0.8.35", optional = true }
# demangler
cpp_demangle = { version = "0.4", optional = true, default-features = false, features = ["alloc"] }
cwdemangle = { version = "1.0", optional = true }
gnuv2_demangle = { version = "0.1.0", 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 }
unarm = { version = "1.6", optional = true }
arm-attr = { version = "0.1", optional = true }
[build-dependencies]
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"
prost-build = { version = "0.13", optional = true }
pbjson-build = { version = "0.7", optional = true }

View File

@ -5,12 +5,10 @@ objdiff-core contains the core functionality of [objdiff](https://github.com/enc
## 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.
- **`mips`**: Enables the MIPS backend powered by [rabbitizer](https://github.com/Decompollaborate/rabbitizer). (Note: C library with Rust bindings)
- **`ppc`**: Enables the PowerPC backend powered by [ppc750cl](https://github.com/encounter/ppc750cl).
- **`x86`**: Enables the x86 backend powered by [iced-x86](https://crates.io/crates/iced-x86).
- **`arm`**: Enables the ARM backend powered by [unarm](https://github.com/AetiasHax/unarm).
- **`bindings`**: Enables serialization and deserialization of objdiff data structures.

View File

@ -1,12 +1,6 @@
#[cfg(feature = "any-arch")]
mod config_gen;
fn main() -> Result<(), Box<dyn std::error::Error>> {
fn main() {
#[cfg(feature = "bindings")]
compile_protos();
#[cfg(feature = "any-arch")]
config_gen::generate_diff_config();
Ok(())
}
#[cfg(feature = "bindings")]
@ -19,15 +13,7 @@ fn compile_protos() {
.map(|m| m.modified().unwrap())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
let mut run_protoc = false;
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<_>>();
let proto_files = vec![root.join("diff.proto"), root.join("report.proto")];
for proto_file in &proto_files {
println!("cargo:rerun-if-changed={}", proto_file.display());
let mtime = match std::fs::metadata(proto_file) {
@ -63,14 +49,11 @@ fn compile_protos() {
}
}
#[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");
}
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");
}

View File

@ -1,333 +0,0 @@
{
"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": "combineDataSections",
"type": "boolean",
"default": false,
"name": "Combine data sections",
"description": "Combines data sections with equal names."
},
{
"id": "combineTextSections",
"type": "boolean",
"default": false,
"name": "Combine text sections",
"description": "Combines all text sections into one."
},
{
"id": "arm.archVersion",
"type": "choice",
"default": "auto",
"name": "Architecture version",
"description": "ARM architecture version to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "v4t",
"name": "ARMv4T (GBA)"
},
{
"value": "v5te",
"name": "ARMv5TE (DS)"
},
{
"value": "v6k",
"name": "ARMv6K (3DS)"
}
]
},
{
"id": "arm.unifiedSyntax",
"type": "boolean",
"default": false,
"name": "Unified syntax",
"description": "Disassemble as unified assembly language (UAL)."
},
{
"id": "arm.avRegisters",
"type": "boolean",
"default": false,
"name": "Use A/V registers",
"description": "Display R0-R3 as A1-A4 and R4-R11 as V1-V8."
},
{
"id": "arm.r9Usage",
"type": "choice",
"default": "generalPurpose",
"name": "Display R9 as",
"items": [
{
"value": "generalPurpose",
"name": "R9 or V6",
"description": "Use R9 as a general-purpose register."
},
{
"value": "sb",
"name": "SB (static base)",
"description": "Used for position-independent data (PID)."
},
{
"value": "tr",
"name": "TR (TLS register)",
"description": "Used for thread-local storage."
}
]
},
{
"id": "arm.slUsage",
"type": "boolean",
"default": false,
"name": "Display R10 as SL",
"description": "Used for explicit stack limits."
},
{
"id": "arm.fpUsage",
"type": "boolean",
"default": false,
"name": "Display R11 as FP",
"description": "Used for frame pointers."
},
{
"id": "arm.ipUsage",
"type": "boolean",
"default": false,
"name": "Display R12 as IP",
"description": "Used for interworking and long branches."
},
{
"id": "mips.abi",
"type": "choice",
"default": "auto",
"name": "ABI",
"description": "MIPS ABI to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "o32",
"name": "O32"
},
{
"value": "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",
"spaceBetweenArgs",
"combineDataSections",
"combineTextSections"
]
},
{
"id": "arm",
"name": "ARM",
"properties": [
"arm.archVersion",
"arm.unifiedSyntax",
"arm.avRegisters",
"arm.r9Usage",
"arm.slUsage",
"arm.fpUsage",
"arm.ipUsage"
]
},
{
"id": "mips",
"name": "MIPS",
"properties": ["mips.abi", "mips.instrCategory", "mips.registerPrefix"]
},
{
"id": "ppc",
"name": "PowerPC",
"properties": ["ppc.calculatePoolRelocations", "analyzeDataFlow"]
},
{
"id": "x86",
"name": "x86",
"properties": ["x86.formatter"]
}
]
}

View File

@ -1,500 +0,0 @@
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();
}

View File

@ -1,59 +0,0 @@
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;
}

View File

@ -0,0 +1,163 @@
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;
}

View File

@ -2,18 +2,6 @@ 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
@ -50,6 +38,18 @@ message Measures {
uint32 complete_units = 16;
}
// 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;
}
message ReportCategory {
// The ID of the category
string id = 1;
@ -99,8 +99,6 @@ 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
@ -110,3 +108,57 @@ 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;
}

View File

@ -1,34 +1,40 @@
use alloc::{collections::BTreeMap, format, string::ToString, vec::Vec};
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::{args, arm, thumb};
use crate::{
arch::{Arch, OPCODE_DATA, OPCODE_INVALID, RelocationOverride, RelocationOverrideTarget},
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, display::InstructionPart},
obj::{
InstructionRef, Relocation, RelocationFlags, ResolvedInstructionRef, ResolvedRelocation,
Section, SectionKind, Symbol, SymbolFlag, SymbolFlagSet, SymbolKind,
},
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
};
#[derive(Debug)]
pub struct ArchArm {
use anyhow::{bail, Result};
use arm_attr::{enums::CpuArch, tag::Tag, BuildAttrs};
use object::{
elf::{self, SHT_ARM_ATTRIBUTES},
Endian, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, SectionIndex,
SectionKind, Symbol, SymbolKind,
};
use unarm::{
args::{Argument, OffsetImm, OffsetReg, Register},
parse::{ArmVersion, ParseMode, Parser},
DisplayOptions, ParseFlags, ParsedIns, RegNames,
};
use crate::{
arch::{ObjArch, ProcessCodeResult},
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig},
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
};
pub struct ObjArchArm {
/// Maps section index, to list of disasm modes (arm, thumb or data) sorted by address
disasm_modes: BTreeMap<usize, Vec<DisasmMode>>,
detected_version: Option<unarm::ArmVersion>,
disasm_modes: HashMap<SectionIndex, Vec<DisasmMode>>,
detected_version: Option<ArmVersion>,
endianness: object::Endianness,
}
impl ArchArm {
pub fn new(file: &object::File) -> Result<Self> {
impl ObjArchArm {
pub fn new(file: &File) -> Result<Self> {
let endianness = file.endianness();
match 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();
File::Elf32(_) => {
let disasm_modes = Self::elf_get_mapping_symbols(file);
let detected_version = Self::elf_detect_arm_version(file)?;
Ok(Self { disasm_modes, detected_version, endianness })
}
@ -36,11 +42,10 @@ impl ArchArm {
}
}
fn elf_detect_arm_version(file: &object::File) -> Result<Option<unarm::ArmVersion>> {
fn elf_detect_arm_version(file: &File) -> Result<Option<ArmVersion>> {
// Check ARM attributes
if let Some(arm_attrs) = file.sections().find(|s| {
s.kind() == object::SectionKind::Elf(elf::SHT_ARM_ATTRIBUTES)
&& s.name() == Ok(".ARM.attributes")
s.kind() == SectionKind::Elf(SHT_ARM_ATTRIBUTES) && s.name() == Ok(".ARM.attributes")
}) {
let attr_data = arm_attrs.uncompressed_data()?;
let build_attrs = BuildAttrs::new(&attr_data, match file.endianness() {
@ -54,12 +59,16 @@ impl ArchArm {
}
// 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(unarm::ArmVersion::V4T)),
Some(CpuArch::V5TE) => return Ok(Some(unarm::ArmVersion::V5Te)),
Some(CpuArch::V6K) => return Ok(Some(unarm::ArmVersion::V6K)),
Some(CpuArch::V4T) => return Ok(Some(ArmVersion::V4T)),
Some(CpuArch::V5TE) => return Ok(Some(ArmVersion::V5Te)),
Some(CpuArch::V6K) => return Ok(Some(ArmVersion::V6K)),
Some(arch) => bail!("ARM arch {} not supported", arch),
None => {}
};
@ -69,459 +78,248 @@ impl ArchArm {
Ok(None)
}
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)
fn elf_get_mapping_symbols(file: &File) -> HashMap<SectionIndex, Vec<DisasmMode>> {
file.sections()
.filter(|s| s.kind() == SectionKind::Text)
.map(|s| {
let index = s.index();
let mut mapping_symbols: Vec<_> = file
.symbols()
.filter(|s| s.section_index().map(|i| i == index).unwrap_or(false))
.filter_map(|s| DisasmMode::from_symbol(&s))
.collect();
mapping_symbols.sort_unstable_by_key(|x| x.address);
(index, mapping_symbols)
(s.index(), mapping_symbols)
})
.collect()
}
fn parse_flags(&self, diff_config: &DiffObjConfig) -> unarm::ParseFlags {
unarm::ParseFlags {
ual: diff_config.arm_unified_syntax,
version: match diff_config.arm_arch_version {
ArmArchVersion::Auto => self.detected_version.unwrap_or(unarm::ArmVersion::V5Te),
ArmArchVersion::V4t => unarm::ArmVersion::V4T,
ArmArchVersion::V5te => unarm::ArmVersion::V5Te,
ArmArchVersion::V6k => unarm::ArmVersion::V6K,
},
}
}
fn display_options(&self, diff_config: &DiffObjConfig) -> unarm::DisplayOptions {
unarm::DisplayOptions {
reg_names: unarm::RegNames {
av_registers: diff_config.arm_av_registers,
r9_use: match diff_config.arm_r9_usage {
ArmR9Usage::GeneralPurpose => unarm::R9Use::GeneralPurpose,
ArmR9Usage::Sb => unarm::R9Use::Pid,
ArmR9Usage::Tr => unarm::R9Use::Tls,
},
explicit_stack_limit: diff_config.arm_sl_usage,
frame_pointer: diff_config.arm_fp_usage,
ip: diff_config.arm_ip_usage,
},
}
}
fn parse_ins_ref(
&self,
ins_ref: InstructionRef,
code: &[u8],
diff_config: &DiffObjConfig,
) -> Result<(unarm::Ins, unarm::ParsedIns)> {
if ins_ref.opcode == thumb::Opcode::BlH as u16 && ins_ref.size == 4 {
// Special case: combined thumb BL instruction
let parse_flags = self.parse_flags(diff_config);
let first_ins = thumb::Ins {
code: match self.endianness {
object::Endianness::Little => u16::from_le_bytes([code[0], code[1]]),
object::Endianness::Big => u16::from_be_bytes([code[0], code[1]]),
} as u32,
op: thumb::Opcode::BlH,
};
let second_ins = thumb::Ins::new(
match self.endianness {
object::Endianness::Little => u16::from_le_bytes([code[2], code[3]]),
object::Endianness::Big => u16::from_be_bytes([code[2], code[3]]),
} as u32,
&parse_flags,
);
let first_parsed = first_ins.parse(&parse_flags);
let second_parsed = second_ins.parse(&parse_flags);
return Ok((
unarm::Ins::Thumb(first_ins),
first_parsed.combine_thumb_bl(&second_parsed),
));
}
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 (ins, parsed_ins) = if ins_ref.opcode == OPCODE_DATA {
let mut args = args::Arguments::default();
args[0] = args::Argument::UImm(code);
let mnemonic = if ins_ref.size == 4 { ".word" } else { ".hword" };
(unarm::Ins::Data, unarm::ParsedIns { mnemonic, args })
} else if ins_ref.opcode & (1 << 15) != 0 {
let ins = arm::Ins { code, op: arm::Opcode::from(ins_ref.opcode as u8) };
let parsed = ins.parse(&self.parse_flags(diff_config));
(unarm::Ins::Arm(ins), parsed)
} else {
let ins = thumb::Ins { code, op: thumb::Opcode::from(ins_ref.opcode as u8) };
let parsed = ins.parse(&self.parse_flags(diff_config));
(unarm::Ins::Thumb(ins), parsed)
};
Ok((ins, parsed_ins))
}
}
impl Arch for ArchArm {
fn post_init(&mut self, sections: &[Section], symbols: &[Symbol]) {
self.disasm_modes = Self::get_mapping_symbols(sections, symbols);
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 scan_instructions_internal(
fn process_code(
&self,
address: u64,
code: &[u8],
section_index: usize,
_relocations: &[Relocation],
diff_config: &DiffObjConfig,
) -> Result<Vec<InstructionRef>> {
relocations: &[ObjReloc],
line_info: &BTreeMap<u64, u32>,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult> {
let start_addr = address as u32;
let end_addr = start_addr + code.len() as u32;
// Mapping symbols decide what kind of data comes after it. $a for ARM code, $t for Thumb code and $d for data.
let fallback_mappings =
[DisasmMode { address: start_addr, mapping: unarm::ParseMode::Arm }];
let fallback_mappings = [DisasmMode { address: start_addr, mapping: ParseMode::Arm }];
let mapping_symbols = self
.disasm_modes
.get(&section_index)
.get(&SectionIndex(section_index))
.map(|x| x.as_slice())
.unwrap_or(&fallback_mappings);
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 first_mapping_idx =
match mapping_symbols.binary_search_by_key(&start_addr, |x| x.address) {
Ok(idx) => idx,
Err(idx) => idx - 1,
};
let first_mapping = mapping_symbols[first_mapping_idx].mapping;
let mut mappings_iter = mapping_symbols
.iter()
.copied()
.skip(first_mapping_idx + 1)
.take_while(|x| x.address < end_addr);
let mut mappings_iter =
mapping_symbols.iter().skip(first_mapping_idx + 1).take_while(|x| x.address < end_addr);
let mut next_mapping = mappings_iter.next();
let ins_count = code.len() / mode.instruction_size(start_addr);
let mut ops = Vec::<InstructionRef>::with_capacity(ins_count);
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 parse_flags = self.parse_flags(diff_config);
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 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 parse_flags = ParseFlags { ual: config.arm_unified_syntax, version };
let mut ins_size = mode.instruction_size(address);
let data = &code[(address - start_addr) as usize..];
if data.len() < 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 code = match (self.endianness, ins_size) {
(object::Endianness::Little, 2) => u16::from_le_bytes([data[0], data[1]]) as u32,
(object::Endianness::Little, 4) => {
u32::from_le_bytes([data[0], data[1], data[2], data[3]])
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();
}
(object::Endianness::Big, 2) => u16::from_be_bytes([data[0], data[1]]) as u32,
(object::Endianness::Big, 4) => {
u32::from_be_bytes([data[0], data[1], data[2], data[3]])
}
_ => {
// Invalid instruction size
ops.push(InstructionRef {
address: address as u64,
size: ins_size as u8,
opcode: OPCODE_INVALID,
branch_dest: None,
});
address += ins_size as u32;
continue;
}
let line = line_info.range(..=address as u64).last().map(|(_, &b)| b);
let reloc = relocations.iter().find(|r| (r.address as u32 & !1) == address).cloned();
let mut reloc_arg = None;
if let Some(reloc) = &reloc {
match reloc.flags {
// Calls
RelocationFlags::Elf { r_type: elf::R_ARM_THM_XPC22 }
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
| RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
reloc_arg = 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 (opcode, branch_dest) = match mode {
unarm::ParseMode::Arm => {
let ins = arm::Ins::new(code, &parse_flags);
let opcode = ins.op as u16 | (1 << 15);
let branch_dest = match ins.op {
arm::Opcode::B | arm::Opcode::Bl => {
address.checked_add_signed(ins.field_branch_offset())
}
arm::Opcode::BlxI => address.checked_add_signed(ins.field_blx_offset()),
_ => None,
};
(opcode, branch_dest)
}
unarm::ParseMode::Thumb => {
let ins = thumb::Ins::new(code, &parse_flags);
let opcode = ins.op as u16;
let branch_dest = match ins.op {
thumb::Opcode::B | thumb::Opcode::Bl => {
address.checked_add_signed(ins.field_branch_offset_8())
}
thumb::Opcode::BlH if data.len() >= 4 => {
// Combine BL instructions
let second_ins = thumb::Ins::new(
match self.endianness {
object::Endianness::Little => {
u16::from_le_bytes([data[2], data[3]]) as u32
}
object::Endianness::Big => {
u16::from_be_bytes([data[2], data[3]]) as u32
}
},
&parse_flags,
);
if let Some(low) = match second_ins.op {
thumb::Opcode::Bl => Some(second_ins.field_low_branch_offset_11()),
thumb::Opcode::BlxI => Some(second_ins.field_low_blx_offset_11()),
_ => None,
} {
ins_size = 4;
address.checked_add_signed(
(ins.field_high_branch_offset_11() + (low as i32)) << 9 >> 9,
)
} else {
None
}
}
thumb::Opcode::BLong => {
address.checked_add_signed(ins.field_branch_offset_11())
}
_ => None,
};
(opcode, branch_dest)
}
unarm::ParseMode::Data => (OPCODE_DATA, None),
let (args, branch_dest) = if reloc.is_some() && parser.mode == ParseMode::Data {
(vec![ObjInsArg::Reloc], None)
} else {
push_args(&parsed_ins, config, reloc_arg, address, display_options)?
};
ops.push(InstructionRef {
ops.push(ins.opcode_id());
insts.push(ObjIns {
address: address as u64,
size: ins_size as u8,
opcode,
branch_dest: branch_dest.map(|x| x 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,
});
address += ins_size as u32;
}
Ok(ops)
Ok(ProcessCodeResult { ops, insts })
}
fn display_instruction(
fn implcit_addend(
&self,
resolved: ResolvedInstructionRef,
diff_config: &DiffObjConfig,
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
) -> Result<()> {
let (ins, parsed_ins) = self.parse_ins_ref(resolved.ins_ref, resolved.code, diff_config)?;
cb(InstructionPart::opcode(parsed_ins.mnemonic, resolved.ins_ref.opcode))?;
if ins == unarm::Ins::Data && resolved.relocation.is_some() {
cb(InstructionPart::reloc())?;
} else {
push_args(
ins,
&parsed_ins,
resolved.relocation,
resolved.ins_ref.address as u32,
self.display_options(diff_config),
cb,
)?;
}
Ok(())
}
fn relocation_override(
&self,
_file: &object::File<'_>,
section: &object::Section,
_file: &File<'_>,
section: &ObjSection,
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() {
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)
}
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
}
_ => 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)
}
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 demangle(&self, name: &str) -> Option<String> {
cpp_demangle::Symbol::new(name)
.ok()
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
}
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))
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
Cow::Owned(format!("<{flags:?}>"))
}
}
#[derive(Clone, Copy, Debug)]
struct DisasmMode {
address: u32,
mapping: unarm::ParseMode,
mapping: ParseMode,
}
impl DisasmMode {
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 from_symbol<'a>(sym: &Symbol<'a, '_, &'a [u8]>) -> Option<Self> {
if let Ok(name) = sym.name() {
ParseMode::from_mapping_symbol(name)
.map(|mapping| DisasmMode { address: sym.address() as u32, mapping })
} else {
None
}
}
}
fn push_args(
ins: unarm::Ins,
parsed_ins: &unarm::ParsedIns,
relocation: Option<ResolvedRelocation>,
parsed_ins: &ParsedIns,
config: &DiffObjConfig,
reloc_arg: Option<usize>,
cur_addr: u32,
display_options: unarm::DisplayOptions,
mut arg_cb: impl FnMut(InstructionPart) -> Result<()>,
) -> Result<()> {
let reloc_arg = find_reloc_arg(parsed_ins, relocation);
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() {
for (i, arg) in parsed_ins.args_iter().enumerate() {
// Emit punctuation before separator
if deref {
match arg {
args::Argument::OffsetImm(args::OffsetImm { post_indexed: true, value: _ })
| args::Argument::OffsetReg(args::OffsetReg {
add: _,
post_indexed: true,
reg: _,
})
| args::Argument::CoOption(_) => {
Argument::OffsetImm(OffsetImm { post_indexed: true, value: _ })
| Argument::OffsetReg(OffsetReg { add: _, post_indexed: true, reg: _ })
| Argument::CoOption(_) => {
deref = false;
arg_cb(InstructionPart::basic("]"))?;
args.push(ObjInsArg::PlainText("]".into()));
if writeback {
writeback = false;
arg_cb(InstructionPart::opaque("!"))?;
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
}
}
_ => {}
@ -529,169 +327,117 @@ fn push_args(
}
if i > 0 {
arg_cb(InstructionPart::separator())?;
args.push(ObjInsArg::PlainText(config.separator().into()));
}
if reloc_arg == Some(i) {
arg_cb(InstructionPart::reloc())?;
args.push(ObjInsArg::Reloc);
} else {
match arg {
args::Argument::None => {}
args::Argument::Reg(reg) => {
Argument::None => {}
Argument::Reg(reg) => {
if reg.deref {
deref = true;
arg_cb(InstructionPart::basic("["))?;
args.push(ObjInsArg::PlainText("[".into()));
}
arg_cb(InstructionPart::opaque(
reg.reg.display(display_options.reg_names).to_string(),
))?;
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 {
arg_cb(InstructionPart::opaque("!"))?;
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
}
}
}
args::Argument::RegList(reg_list) => {
arg_cb(InstructionPart::basic("{"))?;
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 {
arg_cb(InstructionPart::separator())?;
args.push(ObjInsArg::PlainText(config.separator().into()));
}
arg_cb(InstructionPart::opaque(
args::Register::parse(i)
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
Register::parse(i)
.display(display_options.reg_names)
.to_string(),
))?;
.to_string()
.into(),
)));
first = false;
}
}
arg_cb(InstructionPart::basic("}"))?;
args.push(ObjInsArg::PlainText("}".into()));
if reg_list.user_mode {
arg_cb(InstructionPart::opaque("^"))?;
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("^".to_string().into())));
}
}
args::Argument::UImm(value)
| args::Argument::CoOpcode(value)
| args::Argument::SatImm(value) => {
arg_cb(InstructionPart::basic("#"))?;
arg_cb(InstructionPart::unsigned(value))?;
Argument::UImm(value) | Argument::CoOpcode(value) | Argument::SatImm(value) => {
args.push(ObjInsArg::PlainText("#".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
}
args::Argument::SImm(value)
| args::Argument::OffsetImm(args::OffsetImm { post_indexed: _, value }) => {
arg_cb(InstructionPart::basic("#"))?;
arg_cb(InstructionPart::signed(value))?;
Argument::SImm(value)
| Argument::OffsetImm(OffsetImm { post_indexed: _, value }) => {
args.push(ObjInsArg::PlainText("#".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(*value as i64)));
}
args::Argument::BranchDest(value) => {
arg_cb(InstructionPart::branch_dest(cur_addr.wrapping_add_signed(value)))?;
Argument::BranchDest(value) => {
let dest = cur_addr.wrapping_add_signed(*value) as u64;
args.push(ObjInsArg::BranchDest(dest));
branch_dest = Some(dest);
}
args::Argument::CoOption(value) => {
arg_cb(InstructionPart::basic("{"))?;
arg_cb(InstructionPart::unsigned(value))?;
arg_cb(InstructionPart::basic("}"))?;
Argument::CoOption(value) => {
args.push(ObjInsArg::PlainText("{".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
args.push(ObjInsArg::PlainText("}".into()));
}
args::Argument::CoprocNum(value) => {
arg_cb(InstructionPart::opaque(format!("p{value}")))?;
Argument::CoprocNum(value) => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(format!("p{}", value).into())));
}
args::Argument::ShiftImm(shift) => {
arg_cb(InstructionPart::opaque(shift.op.to_string()))?;
arg_cb(InstructionPart::basic(" #"))?;
arg_cb(InstructionPart::unsigned(shift.imm))?;
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)));
}
args::Argument::ShiftReg(shift) => {
arg_cb(InstructionPart::opaque(shift.op.to_string()))?;
arg_cb(InstructionPart::basic(" "))?;
arg_cb(InstructionPart::opaque(
shift.reg.display(display_options.reg_names).to_string(),
))?;
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(),
)));
}
args::Argument::OffsetReg(offset) => {
Argument::OffsetReg(offset) => {
if !offset.add {
arg_cb(InstructionPart::basic("-"))?;
args.push(ObjInsArg::PlainText("-".into()));
}
arg_cb(InstructionPart::opaque(
offset.reg.display(display_options.reg_names).to_string(),
))?;
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
offset.reg.display(display_options.reg_names).to_string().into(),
)));
}
args::Argument::CpsrMode(mode) => {
arg_cb(InstructionPart::basic("#"))?;
arg_cb(InstructionPart::unsigned(mode.mode))?;
Argument::CpsrMode(mode) => {
args.push(ObjInsArg::PlainText("#".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(mode.mode as u64)));
if mode.writeback {
arg_cb(InstructionPart::opaque("!"))?;
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
}
}
args::Argument::CoReg(_)
| args::Argument::StatusReg(_)
| args::Argument::StatusMask(_)
| args::Argument::Shift(_)
| args::Argument::CpsrFlags(_)
| args::Argument::Endian(_) => {
arg_cb(InstructionPart::opaque(
arg.display(display_options, None).to_string(),
))?;
}
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(),
))),
}
}
}
if deref {
arg_cb(InstructionPart::basic("]"))?;
args.push(ObjInsArg::PlainText("]".into()));
if writeback {
arg_cb(InstructionPart::opaque("!"))?;
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
}
}
let branch_dest = get_pc_relative_load_address(ins, cur_addr);
if let Some(branch_dest) = branch_dest {
arg_cb(InstructionPart::basic(" (->"))?;
arg_cb(InstructionPart::branch_dest(branch_dest))?;
arg_cb(InstructionPart::basic(")"))?;
}
Ok(())
}
fn find_reloc_arg(
parsed_ins: &unarm::ParsedIns,
relocation: Option<ResolvedRelocation>,
) -> Option<usize> {
if let Some(resolved) = relocation {
match resolved.relocation.flags {
// Calls
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) => {
parsed_ins.args.iter().rposition(|a| matches!(a, args::Argument::BranchDest(_)))
}
// Data
RelocationFlags::Elf(elf::R_ARM_ABS32) => {
parsed_ins.args.iter().rposition(|a| matches!(a, args::Argument::UImm(_)))
}
_ => None,
}
} else {
None
}
}
fn get_pc_relative_load_address(ins: unarm::Ins, address: u32) -> Option<u32> {
match ins {
unarm::Ins::Arm(ins)
if ins.op == arm::Opcode::Ldr
&& ins.modifier_addr_ldr_str() == arm::AddrLdrStr::Imm
&& ins.field_rn_deref().reg == args::Register::Pc =>
{
let offset = ins.field_offset_12().value;
Some(address.wrapping_add_signed(offset + 8))
}
unarm::Ins::Thumb(ins) if ins.op == thumb::Opcode::LdrPc => {
let offset = ins.field_rel_immed_8().value;
Some((address & !3).wrapping_add_signed(offset + 4))
}
_ => None,
}
Ok((args, branch_dest))
}

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,31 @@
use alloc::{
collections::{BTreeMap, BTreeSet},
string::ToString,
vec::Vec,
};
use std::{borrow::Cow, collections::BTreeMap, sync::Mutex};
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 anyhow::{anyhow, bail, Result};
use object::{
elf, Endian, Endianness, File, FileFlags, Object, ObjectSection, ObjectSymbol, Relocation,
RelocationFlags, RelocationTarget,
};
use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType};
use crate::{
arch::{Arch, RelocationOverride, RelocationOverrideTarget},
diff::{DiffObjConfig, DiffSide, MipsAbi, MipsInstrCategory, display::InstructionPart},
obj::{
InstructionArg, InstructionArgValue, InstructionRef, Relocation, RelocationFlags,
ResolvedInstructionRef, ResolvedRelocation, Section, Symbol, SymbolFlag, SymbolFlagSet,
},
arch::{ObjArch, ProcessCodeResult},
diff::{DiffObjConfig, MipsAbi, MipsInstrCategory},
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
};
#[derive(Debug)]
pub struct ArchMips {
pub endianness: object::Endianness,
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,
pub abi: Abi,
pub isa_extension: Option<IsaExtension>,
pub instr_category: InstrCategory,
pub ri_gp_value: i32,
pub paired_relocations: Vec<BTreeMap<u64, i64>>,
pub ignored_symbols: BTreeSet<usize>,
pub diff_side: DiffSide,
}
const EF_MIPS_ABI: u32 = 0x0000F000;
@ -38,31 +36,28 @@ const EF_MIPS_MACH_5900: u32 = 0x00920000;
const R_MIPS15_S3: u32 = 119;
impl ArchMips {
pub fn new(object: &object::File, diff_side: DiffSide) -> Result<Self> {
let mut abi = Abi::O32;
let mut isa_extension = None;
impl ObjArchMips {
pub fn new(object: &File) -> Result<Self> {
let mut abi = Abi::NUMERIC;
let mut instr_category = InstrCategory::CPU;
match object.flags() {
object::FileFlags::None => {}
object::FileFlags::Elf { e_flags, .. } => {
FileFlags::None => {}
FileFlags::Elf { e_flags, .. } => {
abi = match e_flags & EF_MIPS_ABI {
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,
elf::EF_MIPS_ABI_O32 | elf::EF_MIPS_ABI_O64 => Abi::O32,
elf::EF_MIPS_ABI_EABI32 | elf::EF_MIPS_ABI_EABI64 => Abi::N32,
_ => {
if e_flags & elf::EF_MIPS_ABI2 != 0 {
Abi::N32
} else {
Abi::O32
Abi::NUMERIC
}
}
};
isa_extension = match e_flags & EF_MIPS_MACH {
EF_MIPS_MACH_ALLEGREX => Some(IsaExtension::R4000ALLEGREX),
EF_MIPS_MACH_5900 => Some(IsaExtension::R5900EE),
_ => None,
instr_category = match e_flags & EF_MIPS_MACH {
EF_MIPS_MACH_ALLEGREX => InstrCategory::R4000ALLEGREX,
EF_MIPS_MACH_5900 => InstrCategory::R5900,
_ => InstrCategory::CPU,
};
}
_ => bail!("Unsupported MIPS file flags"),
@ -70,426 +65,248 @@ impl ArchMips {
// 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)
let endianness = object.endianness();
// 0x14 bytes into .reginfo (on 32 bit platforms)
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| endianness.read_i32_bytes(bytes))
.map(|bytes| object.endianness().read_i32_bytes(bytes))
.unwrap_or(0);
// 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,
})
Ok(Self { endianness: object.endianness(), abi, instr_category, ri_gp_value })
}
}
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 {
impl ObjArch for ObjArchMips {
fn process_code(
&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::O64 => Abi::O64,
MipsAbi::N32 => Abi::N32,
MipsAbi::N64 => Abi::N64,
MipsAbi::Eabi32 => Abi::EABI32,
MipsAbi::Eabi64 => Abi::EABI64,
});
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;
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 the relocation target is within the current function, we can
// convert it into a relative branch target. Note that we check
// target_address > start_address instead of >= so that recursive
// tail calls are not considered branch targets.
let target_address =
reloc.target.address.checked_add_signed(reloc.addend);
if reloc.target.orig_section_index == Some(section_index)
&& matches!(target_address, Some(addr) if addr > start_address && addr < end_address)
{
let target_address = target_address.unwrap();
args.push(ObjInsArg::BranchDest(target_address));
branch_dest = Some(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,
});
cur_addr += 4;
}
Ok(ProcessCodeResult { ops, insts })
}
fn implcit_addend(
&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)?;
// 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
} else {
(addend & 0x0000FFFF) as i16 as i64
}
}
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:?}"),
})
}
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 Arch for ArchMips {
fn scan_instructions_internal(
&self,
address: u64,
code: &[u8],
_section_index: usize,
_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 code = self.endianness.read_u32_bytes(chunk.try_into()?);
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(ops)
}
fn display_instruction(
&self,
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(())
}
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,
flags => bail!("Unsupported MIPS implicit relocation {flags:?}"),
};
Ok(Some(RelocationOverride { target: RelocationOverrideTarget::Keep, addend }))
} else {
Ok(None)
}
}
_ => Ok(None),
}
}
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
match 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"),
R_MIPS15_S3 => Some("R_MIPS15_S3"),
_ => None,
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:?}>")),
},
_ => None,
_ => Cow::Owned(format!("<{flags:?}>")),
}
}
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_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<()> {
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
match reloc.flags {
RelocationFlags::Elf(r_type) => match r_type {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_MIPS_HI16 => {
arg_cb(InstructionPart::basic("%hi("))?;
arg_cb(InstructionPart::reloc())?;
arg_cb(InstructionPart::basic(")"))?;
args.push(ObjInsArg::PlainText("%hi(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_LO16 => {
arg_cb(InstructionPart::basic("%lo("))?;
arg_cb(InstructionPart::reloc())?;
arg_cb(InstructionPart::basic(")"))?;
args.push(ObjInsArg::PlainText("%lo(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_GOT16 => {
arg_cb(InstructionPart::basic("%got("))?;
arg_cb(InstructionPart::reloc())?;
arg_cb(InstructionPart::basic(")"))?;
args.push(ObjInsArg::PlainText("%got(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_CALL16 => {
arg_cb(InstructionPart::basic("%call16("))?;
arg_cb(InstructionPart::reloc())?;
arg_cb(InstructionPart::basic(")"))?;
args.push(ObjInsArg::PlainText("%call16(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_GPREL16 => {
arg_cb(InstructionPart::basic("%gp_rel("))?;
arg_cb(InstructionPart::reloc())?;
arg_cb(InstructionPart::basic(")"))?;
args.push(ObjInsArg::PlainText("%gp_rel(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_32
| elf::R_MIPS_26
| elf::R_MIPS_LITERAL
| elf::R_MIPS_PC16
| R_MIPS15_S3 => {
arg_cb(InstructionPart::reloc())?;
args.push(ObjInsArg::Reloc);
}
_ => bail!("Unsupported ELF MIPS relocation type {r_type}"),
},

View File

@ -1,186 +1,102 @@
use alloc::{
borrow::Cow,
boxed::Box,
format,
string::{String, ToString},
vec::Vec,
};
use core::{
any::Any,
ffi::CStr,
fmt::{self, Debug},
};
use std::{borrow::Cow, collections::BTreeMap, ffi::CStr};
use anyhow::{Result, bail};
use encoding_rs::SHIFT_JIS;
use object::Endian as _;
use anyhow::{bail, Result};
use byteorder::ByteOrder;
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
use crate::{
diff::{
DiffObjConfig, DiffSide,
display::{ContextItem, HoverItem, InstructionPart},
},
obj::{
FlowAnalysisResult, InstructionArg, InstructionRef, Object, ParsedInstruction, Relocation,
RelocationFlags, ResolvedInstructionRef, ResolvedSymbol, Section, Symbol, SymbolFlagSet,
SymbolKind,
},
diff::DiffObjConfig,
obj::{ObjIns, ObjReloc, ObjSection},
util::ReallySigned,
};
#[cfg(feature = "arm")]
pub mod arm;
#[cfg(feature = "arm64")]
pub mod arm64;
mod arm;
#[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 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,
Int128,
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.
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
// TODO: Attempt to interpret large symbols as arrays of a smaller type,
// fallback to intrepreting it as bytes.
// https://github.com/encounter/objdiff/issues/124
if self.required_len().is_some_and(|l| bytes.len() != l) {
log::warn!("Failed to display a symbol value for a symbol whose size doesn't match the instruction referencing it.");
return None;
}
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));
format!("Int8: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int8: {:#x}", i)
}
}
DataType::Int16 => {
let i = endian.read_i16_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None));
let i = Endian::read_i16(bytes);
if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None));
format!("Int16: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int16: {:#x}", i)
}
}
DataType::Int32 => {
let i = endian.read_i32_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None));
let i = Endian::read_i32(bytes);
if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None));
format!("Int32: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int32: {:#x}", i)
}
}
DataType::Int64 => {
let i = endian.read_i64_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None));
let i = Endian::read_i64(bytes);
if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None));
format!("Int64: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int64: {:#x}", i)
}
}
DataType::Int128 => {
let i = Endian::read_i128(bytes);
if i < 0 {
format!("Int128: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int128: {:#x}", i)
}
}
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,
));
format!("Float: {}", Endian::read_f32(bytes))
}
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,
));
format!("Double: {}", Endian::read_f64(bytes))
}
DataType::Bytes => {
strs.push((format!("{bytes:#?}"), None));
format!("Bytes: {:#?}", bytes)
}
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())));
}
}
}
format!("String: {:?}", CStr::from_bytes_until_nul(bytes).ok()?)
}
}
strs
.into()
}
fn required_len(&self) -> Option<usize> {
@ -189,6 +105,7 @@ impl DataType {
DataType::Int16 => Some(2),
DataType::Int32 => Some(4),
DataType::Int64 => Some(8),
DataType::Int128 => Some(16),
DataType::Float => Some(4),
DataType::Double => Some(8),
DataType::Bytes => None,
@ -197,295 +114,57 @@ impl DataType {
}
}
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(
pub trait ObjArch: Send + Sync {
fn process_code(
&self,
address: u64,
code: &[u8],
section_index: usize,
relocations: &[Relocation],
diff_config: &DiffObjConfig,
) -> Result<Vec<InstructionRef>>;
relocations: &[ObjReloc],
line_info: &BTreeMap<u64, u32>,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult>;
/// 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(
fn implcit_addend(
&self,
resolved: ResolvedInstructionRef,
diff_config: &DiffObjConfig,
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
) -> Result<()>;
file: &File<'_>,
section: &ObjSection,
address: u64,
reloc: &Relocation,
) -> Result<i64>;
/// 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 demangle(&self, _name: &str) -> Option<String> { None }
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
fn display_data_type(&self, _ty: DataType, bytes: &[u8]) -> Option<String> {
Some(format!("Bytes: {:#x?}", bytes))
}
// Perform detailed data flow analysis
fn data_flow_analysis(
&self,
_obj: &Object,
_symbol: &Symbol,
_code: &[u8],
_relocations: &[Relocation],
) -> Option<Box<dyn FlowAnalysisResult>> {
None
}
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))
}
// Downcast methods
#[cfg(feature = "ppc")]
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }
}
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 struct ProcessCodeResult {
pub ops: Vec<u16>,
pub insts: Vec<ObjIns>,
}
pub fn new_arch(object: &File) -> Result<Box<dyn ObjArch>> {
Ok(match object.architecture() {
#[cfg(feature = "ppc")]
object::Architecture::PowerPc | object::Architecture::PowerPc64 => {
Box::new(ppc::ArchPpc::new(object)?)
}
Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?),
#[cfg(feature = "mips")]
object::Architecture::Mips => Box::new(mips::ArchMips::new(object, diff_side)?),
Architecture::Mips => Box::new(mips::ObjArchMips::new(object)?),
#[cfg(feature = "x86")]
object::Architecture::I386 | object::Architecture::X86_64 => {
Box::new(x86::ArchX86::new(object)?)
}
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
#[cfg(feature = "arm")]
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)?),
Architecture::Arm => Box::new(arm::ObjArchArm::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,
}

View File

@ -0,0 +1,383 @@
use std::{borrow::Cow, collections::BTreeMap};
use anyhow::{bail, ensure, Result};
use byteorder::BigEndian;
use cwextab::{decode_extab, ExceptionTableData};
use object::{
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
Symbol, SymbolKind,
};
use ppc750cl::{Argument, InsIter, Opcode, GPR};
use crate::{
arch::{DataType, ObjArch, ProcessCodeResult},
diff::DiffObjConfig,
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, ObjSymbol},
};
// 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 {
/// Exception info
pub extab: Option<BTreeMap<usize, ExceptionInfo>>,
}
impl ObjArchPpc {
pub fn new(file: &File) -> Result<Self> { Ok(Self { extab: decode_exception_info(file)? }) }
}
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 guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
// Always shows the first string of the table. Not ideal, but it's really hard to find
// the actual string being referenced.
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
return Some(DataType::String);
}
match Opcode::from(instruction.op as u8) {
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,
}
}
fn display_data_type(&self, ty: DataType, bytes: &[u8]) -> Option<String> {
ty.display_bytes::<BigEndian>(bytes)
}
fn ppc(&self) -> Option<&ObjArchPpc> { Some(self) }
}
impl ObjArchPpc {
pub fn extab_for_symbol(&self, symbol: &ObjSymbol) -> Option<&ExceptionInfo> {
symbol.original_index.and_then(|i| self.extab.as_ref()?.get(&i))
}
}
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(())
}
#[derive(Debug, Clone)]
pub struct ExtabSymbolRef {
pub original_index: usize,
pub name: String,
pub demangled_name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ExceptionInfo {
pub eti_symbol: ExtabSymbolRef,
pub etb_symbol: ExtabSymbolRef,
pub data: ExceptionTableData,
pub dtors: Vec<ExtabSymbolRef>,
}
fn decode_exception_info(file: &File<'_>) -> Result<Option<BTreeMap<usize, ExceptionInfo>>> {
let Some(extab_section) = file.section_by_name("extab") else {
return Ok(None);
};
let Some(extabindex_section) = file.section_by_name("extabindex") else {
return Ok(None);
};
let mut result = BTreeMap::new();
let extab_relocations = extab_section.relocations().collect::<BTreeMap<u64, Relocation>>();
let extabindex_relocations =
extabindex_section.relocations().collect::<BTreeMap<u64, Relocation>>();
for extabindex in file.symbols().filter(|symbol| {
symbol.section_index() == Some(extabindex_section.index())
&& symbol.kind() == SymbolKind::Data
}) {
if extabindex.size() != 12 {
log::warn!("Invalid extabindex entry size {}", extabindex.size());
continue;
}
// Each extabindex entry has two relocations:
// - 0x0: The function that the exception table is for
// - 0x8: The relevant entry in extab section
let Some(extab_func_reloc) = extabindex_relocations.get(&extabindex.address()) else {
log::warn!("Failed to find function relocation for extabindex entry");
continue;
};
let Some(extab_reloc) = extabindex_relocations.get(&(extabindex.address() + 8)) else {
log::warn!("Failed to find extab relocation for extabindex entry");
continue;
};
// Resolve the function and extab symbols
let Some(extab_func) = relocation_symbol(file, extab_func_reloc)? else {
log::warn!("Failed to find function symbol for extabindex entry");
continue;
};
let extab_func_name = extab_func.name()?;
let Some(extab) = relocation_symbol(file, extab_reloc)? else {
log::warn!("Failed to find extab symbol for extabindex entry");
continue;
};
let extab_start_addr = extab.address() - extab_section.address();
let extab_end_addr = extab_start_addr + extab.size();
// All relocations in the extab section are dtors
let mut dtors: Vec<ExtabSymbolRef> = vec![];
for (_, reloc) in extab_relocations.range(extab_start_addr..extab_end_addr) {
let Some(symbol) = relocation_symbol(file, reloc)? else {
log::warn!("Failed to find symbol for extab relocation");
continue;
};
dtors.push(make_symbol_ref(&symbol)?);
}
// Decode the extab data
let Some(extab_data) = extab_section.data_range(extab_start_addr, extab.size())? else {
log::warn!("Failed to get extab data for function {}", extab_func_name);
continue;
};
let data = match decode_extab(extab_data) {
Ok(decoded_data) => decoded_data,
Err(e) => {
log::warn!(
"Exception table decoding failed for function {}, reason: {}",
extab_func_name,
e.to_string()
);
return Ok(None);
}
};
//Add the new entry to the list
result.insert(extab_func.index().0, ExceptionInfo {
eti_symbol: make_symbol_ref(&extabindex)?,
etb_symbol: make_symbol_ref(&extab)?,
data,
dtors,
});
}
Ok(Some(result))
}
fn relocation_symbol<'data, 'file>(
file: &'file File<'data>,
relocation: &Relocation,
) -> Result<Option<Symbol<'data, 'file>>> {
let addend = relocation.addend();
match relocation.target() {
RelocationTarget::Symbol(idx) => {
ensure!(addend == 0, "Symbol relocations must have zero addend");
Ok(Some(file.symbol_by_index(idx)?))
}
RelocationTarget::Section(idx) => {
ensure!(addend >= 0, "Section relocations must have non-negative addend");
let addend = addend as u64;
Ok(file
.symbols()
.find(|symbol| symbol.section_index() == Some(idx) && symbol.address() == addend))
}
target => bail!("Unsupported relocation target: {target:?}"),
}
}
fn make_symbol_ref(symbol: &Symbol) -> Result<ExtabSymbolRef> {
let name = symbol.name()?.to_string();
let demangled_name = cwdemangle::demangle(&name, &cwdemangle::DemangleOptions::default());
Ok(ExtabSymbolRef { original_index: symbol.index().0, name, demangled_name })
}

View File

@ -1,649 +0,0 @@
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(&current_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)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,804 +0,0 @@
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

View File

@ -0,0 +1,245 @@
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(|ins_diff| InstructionDiff::new(object, ins_diff))
.collect();
Self {
symbol: Some(Symbol::new(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 Symbol {
pub fn new(value: &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 Instruction {
pub fn new(object: &ObjInfo, instruction: &ObjIns) -> Self {
Self {
address: instruction.address,
size: instruction.size as u32,
opcode: instruction.op as u32,
mnemonic: instruction.mnemonic.clone(),
formatted: instruction.formatted.clone(),
arguments: instruction.args.iter().map(Argument::new).collect(),
relocation: instruction.reloc.as_ref().map(|reloc| Relocation::new(object, reloc)),
branch_dest: instruction.branch_dest,
line_number: instruction.line,
original: instruction.orig.clone(),
}
}
}
impl Argument {
pub fn new(value: &ObjInsArg) -> Self {
Self {
value: Some(match value {
ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::new(v)),
ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
}),
}
}
}
impl ArgumentValue {
pub fn new(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 Relocation {
pub fn new(object: &ObjInfo, reloc: &ObjReloc) -> Self {
Self {
r#type: match reloc.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: object.arch.display_reloc(reloc.flags).into_owned(),
target: Some(RelocationTarget {
symbol: Some(Symbol::new(&reloc.target)),
addend: reloc.addend,
}),
}
}
}
impl InstructionDiff {
pub fn new(object: &ObjInfo, instruction_diff: &ObjInsDiff) -> Self {
Self {
instruction: instruction_diff.ins.as_ref().map(|ins| Instruction::new(object, ins)),
diff_kind: DiffKind::from(instruction_diff.kind) as i32,
branch_from: instruction_diff.branch_from.as_ref().map(InstructionBranchFrom::new),
branch_to: instruction_diff.branch_to.as_ref().map(InstructionBranchTo::new),
arg_diff: instruction_diff.arg_diff.iter().map(ArgumentDiff::new).collect(),
}
}
}
impl ArgumentDiff {
pub fn new(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 InstructionBranchFrom {
pub fn new(value: &ObjInsBranchFrom) -> Self {
Self {
instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
branch_index: value.branch_idx as u32,
}
}
}
impl InstructionBranchTo {
pub fn new(value: &ObjInsBranchTo) -> Self {
Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
}
}

View File

@ -1 +1,5 @@
#[cfg(feature = "any-arch")]
pub mod diff;
pub mod report;
#[cfg(feature = "wasm")]
pub mod wasm;

View File

@ -1,18 +1,11 @@
#![allow(clippy::needless_lifetimes)] // Generated serde code
use std::ops::AddAssign;
use alloc::{
string::{String, ToString},
vec,
vec::Vec,
};
use core::ops::AddAssign;
use anyhow::{Result, bail};
use anyhow::{bail, Result};
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 = 2;
@ -21,30 +14,23 @@ impl Report {
/// Attempts to parse the report as binary protobuf or JSON.
pub fn parse(data: &[u8]) -> Result<Self> {
if data.is_empty() {
bail!("Empty data");
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
}
let report = if data[0] == b'{' {
// Load as JSON
#[cfg(feature = "serde")]
{
Self::from_json(data)?
}
#[cfg(not(feature = "serde"))]
bail!("JSON report parsing requires the `serde` feature")
Self::from_json(data)?
} else {
// Load as binary protobuf
Self::decode(data).map_err(|e| anyhow::Error::msg(e.to_string()))?
Self::decode(data)?
};
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 => {
@ -187,7 +173,8 @@ impl Report {
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))
id.starts_with(parent)
&& id.get(parent.len()..).map_or(false, |s| s.starts_with(sep))
}
let mut sub_categories = self
.categories
@ -317,8 +304,7 @@ impl FromIterator<Measures> for Measures {
}
// Older JSON report types
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct LegacyReport {
fuzzy_match_percent: f32,
total_code: u64,
@ -355,8 +341,7 @@ impl From<LegacyReport> for Report {
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct LegacyReportUnit {
name: String,
fuzzy_match_percent: f32,
@ -366,11 +351,11 @@ struct LegacyReportUnit {
matched_data: u64,
total_functions: u32,
matched_functions: u32,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(skip_serializing_if = "Option::is_none")]
complete: Option<bool>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(skip_serializing_if = "Option::is_none")]
module_name: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(skip_serializing_if = "Option::is_none")]
module_id: Option<u32>,
sections: Vec<LegacyReportItem>,
functions: Vec<LegacyReportItem>,
@ -404,20 +389,16 @@ impl From<LegacyReportUnit> for ReportUnit {
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct LegacyReportItem {
name: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(skip_serializing_if = "Option::is_none")]
demangled_name: Option<String>,
#[cfg_attr(
feature = "serde",
serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_hex",
deserialize_with = "deserialize_hex"
)
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_hex",
deserialize_with = "deserialize_hex"
)]
address: Option<u64>,
size: u64,
@ -434,18 +415,19 @@ 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;

View File

@ -0,0 +1,78 @@
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) }
}

View File

@ -1,109 +0,0 @@
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 }
}

View File

@ -1,76 +0,0 @@
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)
}

View File

@ -1,59 +1,48 @@
pub mod path;
use alloc::{
borrow::Cow,
collections::BTreeMap,
string::{String, ToString},
vec::Vec,
use std::{
fs,
fs::File,
io::{BufReader, BufWriter, Read},
path::{Path, PathBuf},
};
use anyhow::{Context, Result, anyhow};
use anyhow::{anyhow, Context, Result};
use bimap::BiBTreeMap;
use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder};
use path::unix_path_serde_option;
use typed_path::Utf8UnixPathBuf;
#[derive(Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectConfig {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_make: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_args: Option<Vec<String>>,
#[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"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_dir: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_dir: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_base: Option<bool>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, 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")
)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub watch_patterns: Option<Vec<Glob>>,
#[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
pub units: Option<Vec<ProjectObject>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, 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>,
}
impl ProjectConfig {
#[inline]
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
#[inline]
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
self.units.get_or_insert_with(Vec::new)
}
#[inline]
pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
self.progress_categories.as_deref().unwrap_or_default()
@ -63,111 +52,88 @@ impl ProjectConfig {
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))]
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObject {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[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"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.reverse_fn_order")]
pub reverse_fn_order: Option<bool>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.complete")]
pub complete: Option<bool>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scratch: Option<ScratchConfig>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, 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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol_mappings: Option<SymbolMappings>,
}
#[derive(Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
pub type SymbolMappings = BiBTreeMap<String, String>;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObjectMetadata {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub complete: Option<bool>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reverse_fn_order: Option<bool>,
#[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"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Option<Vec<String>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_generated: Option<bool>,
}
#[derive(Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
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.as_str()
path.to_str().unwrap_or("[invalid path]")
} 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> {
#[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
@ -182,73 +148,42 @@ impl ProjectObject {
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
}
pub fn source_path(&self) -> Option<&Utf8UnixPathBuf> {
pub fn source_path(&self) -> Option<&String> {
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)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(default))]
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ScratchConfig {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compiler: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub c_flags: Option<String>,
#[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"))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ctx_path: Option<PathBuf>,
#[serde(default, 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", "*.cc", "*.cp", "*.cpp", "*.cxx", "*.c++", "*.h", "*.hh", "*.hp", "*.hpp", "*.hxx",
"*.h++", "*.pch", "*.pch++", "*.inc", "*.s", "*.S", "*.asm", "*.py", "*.yml", "*.txt",
"*.json",
"*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm",
"*.inc", "*.py", "*.yml", "*.txt", "*.json",
];
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 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>,
pub path: PathBuf,
pub timestamp: Option<FileTime>,
}
#[cfg(feature = "std")]
pub fn try_project_config(
dir: &std::path::Path,
) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
for filename in CONFIG_FILENAMES.iter() {
let config_path = dir.join(filename);
let Ok(file) = std::fs::File::open(&config_path) else {
let Ok(file) = File::open(&config_path) else {
continue;
};
let metadata = file.metadata();
@ -256,9 +191,12 @@ pub fn try_project_config(
if !metadata.is_file() {
continue;
}
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);
let ts = FileTime::from_last_modification_time(&metadata);
let mut reader = BufReader::new(file);
let mut result = match filename.contains("json") {
true => read_json_config(&mut reader),
false => read_yml_config(&mut reader),
};
if let Ok(config) = &result {
// Validate min_version if present
if let Err(e) = validate_min_version(config) {
@ -271,42 +209,40 @@ pub fn try_project_config(
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 let Ok(metadata) = fs::metadata(&info.path) {
let ts = 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 mut writer =
BufWriter::new(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"),
"yml" | "yaml" => {
serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML")
}
_ => 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);
let ts = 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)
.map_err(|e| anyhow::Error::msg(e.to_string()))
.context("Failed to parse min_version")?;
let min_version = semver::Version::parse(min_version).context("Failed to parse min_version")?;
if version >= min_version {
Ok(())
} else {
@ -314,59 +250,18 @@ fn validate_min_version(config: &ProjectConfig) -> Result<()> {
}
}
#[cfg(feature = "std")]
fn read_json_config<R: std::io::Read>(reader: &mut R) -> Result<ProjectConfig> {
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> {
Ok(serde_json::from_reader(reader)?)
}
pub fn build_globset(vec: &[Glob]) -> Result<GlobSet, globset::Error> {
pub fn build_globset(vec: &[Glob]) -> std::result::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
}

View File

@ -1,57 +0,0 @@
// 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))
}
}

View File

@ -1,515 +1,364 @@
use alloc::{
collections::{BTreeMap, btree_map},
string::{String, ToString},
vec,
vec::Vec,
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, ObjSymbolFlags, SymbolRef},
};
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 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 = &section.data
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
obj.arch.process_code(
symbol.address,
code,
section.orig_index,
&section.relocations,
&section.line_info,
config,
)
}
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 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, target_symbol: None, instructions: diff, match_percent: None })
}
pub fn diff_code(
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
)
})?;
left_obj: &ObjInfo,
right_obj: &ObjInfo,
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)?;
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);
resolve_branches(&mut left_diff);
resolve_branches(&mut right_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 mut diff_state = InsDiffState::default();
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
let result = compare_ins(config, left_obj, right_obj, 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 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
let total = left_out.insts.len().max(right_out.insts.len());
let percent = if diff_state.diff_count >= total {
0.0
} else {
((1.0 - (diff_score as f64 / max_score as f64)) * 100.0) as f32
((total - diff_state.diff_count) as f32 / total as f32) * 100.0
};
Ok((
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: left_symbol_ref,
target_symbol: Some(right_symbol_ref),
instructions: left_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()
ObjSymbolDiff {
symbol_ref: right_symbol_ref,
target_symbol: Some(left_symbol_ref),
instructions: right_diff,
match_percent: Some(percent),
},
))
}
fn diff_instructions(
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);
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);
if ops.is_empty() {
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 = left_range.len().max(right_range.len());
left_diff.extend(
left_range
.clone()
.map(|i| InstructionDiffRow { ins_ref: Some(left_insts[i]), ..Default::default() }),
left_code
.insts
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
right_diff.extend(
right_range.clone().map(|i| InstructionDiffRow {
ins_ref: Some(right_insts[i]),
..Default::default()
}),
right_code
.insts
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
return Ok(());
}
for op in ops {
let (_tag, left_range, right_range) = op.as_tag_tuple();
let len = max(left_range.len(), right_range.len());
left_diff.extend(
left_code.insts[left_range.clone()]
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
right_diff.extend(
right_code.insts[right_range.clone()]
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
if left_range.len() < len {
left_diff.extend((left_range.len()..len).map(|_| InstructionDiffRow::default()));
left_diff.extend((left_range.len()..len).map(|_| ObjInsDiff::default()));
}
if right_range.len() < len {
right_diff.extend((right_range.len()..len).map(|_| InstructionDiffRow::default()));
right_diff.extend((right_range.len()..len).map(|_| ObjInsDiff::default()));
}
}
Ok((left_diff, right_diff))
Ok(())
}
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;
fn resolve_branches(vec: &mut [ObjInsDiff]) {
let mut branch_idx = 0usize;
// Map addresses to indices
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);
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);
}
}
// Generate branches
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();
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(InstructionBranchTo { ins_idx, branch_idx: branch.branch_idx });
branch.ins_idx.push(i as u32);
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 });
branch_idx += 1;
}
}
}
}
// Store branch from
for (i, branch) in branches {
rows[i as usize].branch_from = Some(branch);
vec[i].branch_from = Some(branch);
}
}
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
}
fn address_eq(left: &ObjReloc, right: &ObjReloc) -> bool {
left.target.address as i64 + left.addend == right.target.address as i64 + right.addend
}
pub(crate) fn section_name_eq(
left_obj: &Object,
right_obj: &Object,
left_section_index: usize,
right_section_index: usize,
fn section_name_eq(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_orig_section_index: usize,
right_orig_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)
})
let Some(left_section) =
left_obj.sections.iter().find(|s| s.orig_index == left_orig_section_index)
else {
return false;
};
let Some(right_section) =
right_obj.sections.iter().find(|s| s.orig_index == right_orig_section_index)
else {
return false;
};
left_section.name == right_section.name
}
fn reloc_eq(
left_obj: &Object,
right_obj: &Object,
left_ins: ResolvedInstructionRef,
right_ins: ResolvedInstructionRef,
diff_config: &DiffObjConfig,
config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_reloc: Option<&ObjReloc>,
right_reloc: Option<&ObjReloc>,
) -> bool {
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,
let (Some(left), Some(right)) = (left_reloc, right_reloc) else {
return false;
};
if left_reloc.relocation.flags != right_reloc.relocation.flags {
if left.flags != right.flags {
return false;
}
if relax_reloc_diffs {
if config.relax_reloc_diffs {
return true;
}
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) {
let symbol_name_matches = left.target.name == right.target.name;
match (&left.target.orig_section_index, &right.target.orig_section_index) {
(Some(sl), Some(sr)) => {
// Match if section and name or address match
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))
&& (symbol_name_matches || address_eq(left, right))
}
(Some(_), None) | (None, Some(_)) | (None, None) => symbol_name_addend_matches,
(Some(_), None) => false,
(None, Some(_)) => {
// Match if possibly stripped weak symbol
symbol_name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
}
(None, None) => symbol_name_matches,
}
}
fn arg_eq(
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,
config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left: &ObjInsArg,
right: &ObjInsArg,
left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff,
) -> bool {
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
InstructionArg::Reloc => diff_config.function_reloc_diffs == FunctionRelocDiffs::None,
match left {
ObjInsArg::PlainText(l) => match right {
ObjInsArg::PlainText(r) => l == r,
_ => false,
},
InstructionArg::Reloc => {
matches!(right_arg, InstructionArg::Reloc)
&& reloc_eq(left_obj, right_obj, left_ins, right_ins, diff_config)
ObjInsArg::Arg(l) => match right {
ObjInsArg::Arg(r) => l == 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,
_ => false,
},
ObjInsArg::Reloc => {
matches!(right, ObjInsArg::Reloc)
&& reloc_eq(
config,
left_obj,
right_obj,
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::BranchDest(_) => match right_arg {
ObjInsArg::BranchDest(_) => {
// Compare dest instruction idx after diffing
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,
},
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
}
}
}
#[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>,
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>,
}
#[derive(Default)]
struct InstructionDiffResult {
kind: InstructionDiffKind,
left_args_diff: Vec<InstructionArgDiffIndex>,
right_args_diff: Vec<InstructionArgDiffIndex>,
struct InsDiffResult {
kind: ObjInsDiffKind,
left_args_diff: Vec<Option<ObjInsArgDiff>>,
right_args_diff: Vec<Option<ObjInsArgDiff>>,
}
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)),
};
// If opcodes don't match, replace
if l.opcode != r.opcode {
state.diff_score += PENALTY_REPLACE;
return Ok(InstructionDiffResult::new(InstructionDiffKind::Replace));
}
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")?;
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 {
state.diff_score += PENALTY_REG_DIFF;
result.kind = InstructionDiffKind::OpMismatch;
}
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 {
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 = 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 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(),
};
result.left_args_diff.push(InstructionArgDiffIndex::new(a_diff));
result.right_args_diff.push(InstructionArgDiffIndex::new(b_diff));
}
}
if result.kind == InstructionDiffKind::None
&& left_resolved.code.len() != right_resolved.code.len()
fn compare_ins(
config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
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,
})
{
// 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;
// Totally different op
result.kind = ObjInsDiffKind::Replace;
state.diff_count += 1;
return Ok(result);
}
return Ok(result);
if left_ins.mnemonic != right_ins.mnemonic {
// Same op but different mnemonic, still cmp args
result.kind = ObjInsDiffKind::OpMismatch;
state.diff_count += 1;
}
for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
if arg_eq(config, left_obj, right_obj, a, b, left, right) {
result.left_args_diff.push(None);
result.right_args_diff.push(None);
} else {
if result.kind == ObjInsDiffKind::None {
result.kind = ObjInsDiffKind::ArgMismatch;
state.diff_count += 1;
}
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_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 = 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));
}
}
} else if left.ins.is_some() {
result.kind = ObjInsDiffKind::Delete;
state.diff_count += 1;
} else {
result.kind = ObjInsDiffKind::Insert;
state.diff_count += 1;
}
Ok(InstructionDiffResult::new(InstructionDiffKind::None))
Ok(result)
}

View File

@ -1,145 +1,119 @@
use alloc::{vec, vec::Vec};
use core::{cmp::Ordering, ops::Range};
use std::cmp::{max, min, Ordering};
use anyhow::{Result, anyhow};
use similar::{Algorithm, capture_diff_slices, get_diff_ratio};
use anyhow::{anyhow, Result};
use similar::{capture_diff_slices_deadline, get_diff_ratio, Algorithm};
use super::{
DataDiff, DataDiffKind, DataDiffRow, DataRelocationDiff, ObjectDiff, SectionDiff, SymbolDiff,
code::{address_eq, section_name_eq},
use crate::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjSectionDiff, ObjSymbolDiff},
obj::{ObjInfo, ObjSection, SymbolRef},
};
use crate::obj::{Object, Relocation, ResolvedRelocation, Symbol, SymbolFlag, SymbolKind};
pub fn diff_bss_symbol(
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];
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);
let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 };
Ok((
SymbolDiff {
ObjSymbolDiff {
symbol_ref: left_symbol_ref,
target_symbol: Some(right_symbol_ref),
instructions: vec![],
match_percent: Some(percent),
diff_score: None,
..Default::default()
},
SymbolDiff {
ObjSymbolDiff {
symbol_ref: right_symbol_ref,
target_symbol: Some(left_symbol_ref),
instructions: vec![],
match_percent: Some(percent),
diff_score: None,
..Default::default()
},
))
}
pub fn symbol_name_matches(left_name: &str, right_name: &str) -> bool {
// Match Metrowerks symbol$1234 against symbol$2345
if let Some((prefix, suffix)) = left_name.split_once('$') {
if !suffix.chars().all(char::is_numeric) {
return false;
}
right_name
.split_once('$')
.is_some_and(|(p, s)| p == prefix && s.chars().all(char::is_numeric))
} else {
left_name == right_name
}
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None }
}
fn reloc_eq(
left_obj: &Object,
right_obj: &Object,
left: ResolvedRelocation,
right: ResolvedRelocation,
) -> bool {
if left.relocation.flags != right.relocation.flags {
return false;
}
/// 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;
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();
let mut left_diff = Vec::<ObjDataDiff>::new();
let mut right_diff = Vec::<ObjDataDiff>::new();
for op in ops {
let (tag, left_range, right_range) = op.as_tag_tuple();
let left_len = left_range.len();
let right_len = right_range.len();
let mut len = left_len.max(right_len);
let mut len = max(left_len, right_len);
let kind = match tag {
similar::DiffTag::Equal => DataDiffKind::None,
similar::DiffTag::Delete => DataDiffKind::Delete,
similar::DiffTag::Insert => DataDiffKind::Insert,
similar::DiffTag::Equal => ObjDataDiffKind::None,
similar::DiffTag::Delete => ObjDataDiffKind::Delete,
similar::DiffTag::Insert => ObjDataDiffKind::Insert,
similar::DiffTag::Replace => {
// Ensure replacements are equal length
len = left_len.min(right_len);
DataDiffKind::Replace
len = min(left_len, right_len);
ObjDataDiffKind::Replace
}
};
let left_data = &left_data[left_range];
let right_data = &right_data[right_range];
left_data_diff.push(DataDiff {
data: left_data[..len.min(left_data.len())].to_vec(),
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(),
kind,
size: len,
len,
..Default::default()
});
right_data_diff.push(DataDiff {
data: right_data[..len.min(right_data.len())].to_vec(),
right_diff.push(ObjDataDiff {
data: right_data[..min(len, right_data.len())].to_vec(),
kind,
size: len,
len,
..Default::default()
});
if kind == DataDiffKind::Replace {
if kind == ObjDataDiffKind::Replace {
match left_len.cmp(&right_len) {
Ordering::Less => {
let len = right_len - left_len;
left_data_diff.push(DataDiff {
left_diff.push(ObjDataDiff {
data: vec![],
kind: DataDiffKind::Insert,
size: len,
kind: ObjDataDiffKind::Insert,
len,
..Default::default()
});
right_data_diff.push(DataDiff {
right_diff.push(ObjDataDiff {
data: right_data[left_len..right_len].to_vec(),
kind: DataDiffKind::Insert,
size: len,
kind: ObjDataDiffKind::Insert,
len,
..Default::default()
});
}
Ordering::Greater => {
let len = left_len - right_len;
left_data_diff.push(DataDiff {
left_diff.push(ObjDataDiff {
data: left_data[right_len..left_len].to_vec(),
kind: DataDiffKind::Delete,
size: len,
kind: ObjDataDiffKind::Delete,
len,
..Default::default()
});
right_data_diff.push(DataDiff {
right_diff.push(ObjDataDiff {
data: vec![],
kind: DataDiffKind::Delete,
size: len,
kind: ObjDataDiffKind::Delete,
len,
..Default::default()
});
}
Ordering::Equal => {}
@ -147,360 +121,52 @@ fn diff_data_range(left_data: &[u8], right_data: &[u8]) -> (f32, Vec<DataDiff>,
}
}
(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);
}
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);
}
Ok((left_section_diff, right_section_diff))
}
pub fn no_diff_data_symbol(obj: &Object, symbol_index: usize) -> Result<SymbolDiff> {
let symbol = &obj.symbols[symbol_index];
let section_idx = symbol.section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
let section = &obj.sections[section_idx];
let start = symbol
.address
.checked_sub(section.address)
.ok_or_else(|| anyhow!("Symbol address out of section bounds"))?;
let end = start + symbol.size;
if end > section.size {
return Err(anyhow!(
"Symbol {} size out of section bounds ({} > {})",
symbol.name,
end,
section.size
));
}
let range = start as usize..end as usize;
let data = &section.data[range.clone()];
let 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: &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];
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);
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_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 = &left_obj.sections[left_section_idx];
let right_section = &right_obj.sections[right_section_idx];
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_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);
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;
Ok((
SymbolDiff {
target_symbol: Some(right_symbol_idx),
ObjSymbolDiff {
symbol_ref: left_symbol_ref,
target_symbol: Some(right_symbol_ref),
instructions: vec![],
match_percent: Some(match_percent),
diff_score: None,
data_rows: left_rows,
..Default::default()
},
SymbolDiff {
target_symbol: Some(left_symbol_idx),
ObjSymbolDiff {
symbol_ref: right_symbol_ref,
target_symbol: Some(left_symbol_ref),
instructions: vec![],
match_percent: Some(match_percent),
diff_score: None,
data_rows: right_rows,
..Default::default()
},
))
}
@ -508,150 +174,49 @@ 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_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))
{
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)) {
100.0 // Avoid fp precision issues
} else {
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 }
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
};
Ok((
SectionDiff { match_percent: Some(match_percent), data_diff: vec![], reloc_diff: vec![] },
SectionDiff { match_percent: None, data_diff: vec![], reloc_diff: vec![] },
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
))
}
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_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);
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);
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_obj,
right_obj,
left_diff,
right_diff,
left_section_idx,
right_section_idx,
)?;
let (generic_diff, _) = diff_generic_section(left, right, left_diff, right_diff)?;
if generic_diff.match_percent.unwrap_or(-1.0) > match_percent {
match_percent = generic_diff.match_percent.unwrap();
}
Ok((
SectionDiff { match_percent: Some(match_percent), data_diff: vec![], reloc_diff: vec![] },
SectionDiff { match_percent: None, data_diff: vec![], reloc_diff: vec![] },
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
))
}
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()
}

View File

@ -1,51 +0,0 @@
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(&cpp_demangle::DemangleOptions::default()).ok())
}
fn demangle_gnu_legacy(name: &str) -> Option<String> {
let name = name.trim_start_matches('.');
gnuv2_demangle::demangle(name, &gnuv2_demangle::DemangleConfig::new_no_cfilt_mimics()).ok()
}
}
#[cfg(not(feature = "demangler"))]
impl Demangler {
pub fn demangle(&self, _name: &str) -> Option<String> { None }
}

View File

@ -1,32 +1,16 @@
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 std::cmp::Ordering;
use crate::{
diff::{
DataDiffKind, DataDiffRow, DiffObjConfig, InstructionDiffKind, InstructionDiffRow,
ObjectDiff, SymbolDiff, data::resolve_relocation,
},
obj::{
FlowAnalysisValue, InstructionArg, InstructionArgValue, Object, ParsedInstruction,
ResolvedInstructionRef, ResolvedRelocation, SectionFlag, SectionKind, Symbol, SymbolFlag,
SymbolKind,
},
diff::{ObjInsArgDiff, ObjInsDiff},
obj::{ObjInsArg, ObjInsArgValue, ObjReloc, ObjSymbol},
};
#[derive(Debug, Clone)]
#[derive(Debug, Copy, Clone)]
pub enum DiffText<'a> {
/// Basic text
Basic(&'a str),
/// Colored text
BasicColor(&'a str, usize),
/// Line number
Line(u32),
/// Instruction address
@ -34,294 +18,86 @@ pub enum DiffText<'a> {
/// Instruction mnemonic
Opcode(&'a str, u16),
/// Instruction argument
Argument(InstructionArgValue<'a>),
Argument(&'a ObjInsArgValue, Option<&'a ObjInsArgDiff>),
/// Branch destination
BranchDest(u64),
BranchDest(u64, Option<&'a ObjInsArgDiff>),
/// Symbol name
Symbol(&'a Symbol),
/// Relocation addend
Addend(i64),
Symbol(&'a ObjSymbol),
/// Number of spaces
Spacing(u8),
Spacing(usize),
/// End of line
Eol,
}
#[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)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum HighlightKind {
#[default]
None,
Opcode(u16),
Argument(InstructionArgValue<'static>),
Arg(ObjInsArgValue),
Symbol(String),
Address(u64),
}
#[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)?;
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)?;
return Ok(());
};
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 })?;
if let Some(line) = ins.line {
cb(DiffText::Line(line))?;
}
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)))?;
cb(DiffText::Address(ins.address - base_addr))?;
if let Some(branch) = &ins_diff.branch_from {
cb(DiffText::BasicColor(" ~> ", branch.branch_idx))?;
} else {
cb(DiffTextSegment::spacing(4))?;
cb(DiffText::Spacing(4))?;
}
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))
}
cb(DiffText::Opcode(&ins.mnemonic, ins.op))?;
for (i, arg) in ins.args.iter().enumerate() {
if i == 0 {
cb(DiffText::Spacing(1))?;
}
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;
let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref());
match arg {
ObjInsArg::PlainText(s) => {
cb(DiffText::Basic(s))?;
}
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,
})
}
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>"))?;
}
}
}
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_row.branch_to {
cb(DiffTextSegment::basic(" ~>", DiffTextColor::Rotating(branch.branch_idx as u8)))?;
if let Some(branch) = &ins_diff.branch_to {
cb(DiffText::BasicColor(" ~>", branch.branch_idx))?;
}
cb(EOL_SEGMENT)?;
cb(DiffText::Eol)?;
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,
}
fn display_reloc_name<E>(
reloc: &ObjReloc,
mut cb: impl FnMut(DiffText) -> Result<(), E>,
) -> Result<(), E> {
cb(DiffText::Symbol(&reloc.target))?;
match reloc.addend.cmp(&0i64) {
Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.addend))),
Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.addend))),
_ => Ok(()),
}
}
@ -329,9 +105,11 @@ impl PartialEq<DiffText<'_>> for HighlightKind {
fn eq(&self, other: &DiffText) -> bool {
match (self, other) {
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b,
(HighlightKind::Argument(a), DiffText::Argument(b)) => a.loose_eq(b),
(HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b),
(HighlightKind::Symbol(a), DiffText::Symbol(b)) => a == &b.name,
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b)) => a == b,
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b, _)) => {
a == b
}
_ => false,
}
}
@ -341,537 +119,14 @@ 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::Argument(arg.to_static()),
DiffText::Opcode(_, op) => HighlightKind::Opcode(op),
DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()),
DiffText::Symbol(sym) => HighlightKind::Symbol(sym.name.to_string()),
DiffText::Address(addr) | DiffText::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

View File

@ -1,50 +0,0 @@
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)))
})
}

View File

@ -1,188 +0,0 @@
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)))
})
}

View File

@ -1,19 +1,11 @@
#![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")]

View File

@ -1,109 +0,0 @@
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:?}")
}

View File

@ -1,401 +0,0 @@
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)
}

View File

@ -1,43 +1,23 @@
#[cfg(feature = "dwarf")]
mod dwarf2;
mod mdebug;
pub mod read;
pub mod split_meta;
use alloc::{
borrow::Cow,
boxed::Box,
collections::BTreeMap,
string::{String, ToString},
vec,
vec::Vec,
};
use core::{
fmt,
num::{NonZeroU32, NonZeroU64},
};
use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
use flagset::{FlagSet, flags};
use filetime::FileTime;
use flagset::{flags, FlagSet};
use object::RelocationFlags;
use split_meta::SplitMeta;
use crate::{
arch::{Arch, ArchDummy},
obj::split_meta::SplitMeta,
util::ReallySigned,
};
use crate::{arch::ObjArch, util::ReallySigned};
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)]
pub enum SectionKind {
#[default]
Unknown = -1,
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum ObjSectionKind {
Code,
Data,
Bss,
Common,
}
flags! {
#[derive(Hash)]
pub enum SymbolFlag: u8 {
pub enum ObjSymbolFlags: u8 {
Global,
Local,
Weak,
@ -46,200 +26,94 @@ flags! {
/// 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>);
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,
#[derive(Debug, Clone)]
pub struct ObjSection {
pub name: String,
pub kind: ObjSectionKind,
pub address: u64,
pub size: u64,
pub kind: SectionKind,
pub data: SectionData,
pub flags: SectionFlagSet,
pub align: Option<NonZeroU64>,
pub relocations: Vec<Relocation>,
pub data: Vec<u8>,
pub orig_index: usize,
pub symbols: Vec<ObjSymbol>,
pub relocations: Vec<ObjReloc>,
pub virtual_address: Option<u64>,
/// Line number info (.line or .debug_line section)
pub line_info: BTreeMap<u64, u32>,
/// Original virtual address (from .note.split section)
pub virtual_address: Option<u64>,
}
#[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 }
}
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> {
pub enum ObjInsArgValue {
Signed(i64),
Unsigned(u64),
Opaque(Cow<'a, str>),
Opaque(Cow<'static, str>),
}
impl InstructionArgValue<'_> {
pub fn loose_eq(&self, other: &InstructionArgValue) -> bool {
impl ObjInsArgValue {
pub fn loose_eq(&self, other: &ObjInsArgValue) -> 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,
(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,
}
}
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<'_> {
impl fmt::Display for ObjInsArgValue {
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}"),
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 InstructionArg<'a> {
Value(InstructionArgValue<'a>),
pub enum ObjInsArg {
PlainText(Cow<'static, str>),
Arg(ObjInsArgValue),
Reloc,
BranchDest(u64),
}
impl InstructionArg<'_> {
pub fn loose_eq(&self, other: &InstructionArg) -> bool {
impl ObjInsArg {
pub fn loose_eq(&self, other: &ObjInsArg) -> 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,
(ObjInsArg::Arg(a), ObjInsArg::Arg(b)) => a.loose_eq(b),
(ObjInsArg::Reloc, ObjInsArg::Reloc) => true,
(ObjInsArg::BranchDest(a), ObjInsArg::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>>,
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, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum SymbolKind {
pub enum ObjSymbolKind {
#[default]
Unknown,
Function,
@ -247,195 +121,59 @@ pub enum SymbolKind {
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 {
#[derive(Debug, Clone)]
pub struct ObjSymbol {
pub name: String,
pub demangled_name: Option<String>,
pub address: u64,
pub section_address: u64,
pub size: u64,
pub kind: SymbolKind,
pub section: Option<usize>,
pub flags: SymbolFlagSet,
/// Alignment (from Metrowerks .comment section)
pub align: Option<NonZeroU32>,
pub size_known: bool,
pub kind: ObjSymbolKind,
pub flags: ObjSymbolFlagSet,
pub orig_section_index: Option<usize>,
/// Original virtual address (from .note.split section)
pub virtual_address: Option<u64>,
/// Original index in object symbol table
pub original_index: Option<usize>,
pub bytes: Vec<u8>,
}
#[derive(Debug)]
pub struct Object {
pub arch: Box<dyn Arch>,
pub endianness: object::Endianness,
pub symbols: Vec<Symbol>,
pub sections: Vec<Section>,
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>,
/// 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 {
#[derive(Debug, Clone)]
pub struct ObjReloc {
pub flags: RelocationFlags,
pub address: u64,
pub target_symbol: usize,
pub target: ObjSymbol,
pub addend: i64,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum RelocationFlags {
Elf(u32),
Coff(u16),
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SymbolRef {
pub section_idx: usize,
pub symbol_idx: usize,
}
#[derive(Debug, Copy, Clone)]
pub struct ResolvedRelocation<'a> {
pub relocation: &'a Relocation,
pub symbol: &'a Symbol,
}
pub const SECTION_COMMON: usize = usize::MAX - 1;
#[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,
impl ObjInfo {
pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (Option<&ObjSection>, &ObjSymbol) {
if symbol_ref.section_idx == SECTION_COMMON {
let symbol = &self.common[symbol_ref.symbol_idx];
return (None, symbol);
}
let section = &self.sections[symbol_ref.section_idx];
let symbol = &section.symbols[symbol_ref.symbol_idx];
(Some(section), symbol)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,165 +0,0 @@
---
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,
},
],
)

View File

@ -1,11 +1,6 @@
use alloc::{string::String, vec, vec::Vec};
use std::{io, io::Write};
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;
use object::{elf::SHT_NOTE, Endian, ObjectSection};
pub const SPLITMETA_SECTION: &str = ".note.split";
pub const SHT_SPLITMETA: u32 = SHT_NOTE;
@ -32,10 +27,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) -> Result<Self>
pub fn from_section<E>(section: object::Section, e: E, is_64: bool) -> io::Result<Self>
where E: Endian {
let mut result = SplitMeta::default();
let data = section.uncompressed_data().map_err(object_error)?;
let data = section.uncompressed_data().map_err(object_io_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 {
@ -43,19 +38,20 @@ impl SplitMeta {
}
match note.n_type {
NT_SPLIT_GENERATOR => {
let string =
String::from_utf8(note.desc.to_vec()).map_err(anyhow::Error::new)?;
let string = String::from_utf8(note.desc.to_vec())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
result.generator = Some(string);
}
NT_SPLIT_MODULE_NAME => {
let string =
String::from_utf8(note.desc.to_vec()).map_err(anyhow::Error::new)?;
let string = String::from_utf8(note.desc.to_vec())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
result.module_name = Some(string);
}
NT_SPLIT_MODULE_ID => {
result.module_id = Some(e.read_u32_bytes(
note.desc.try_into().map_err(|_| anyhow!("Invalid module ID size"))?,
));
result.module_id =
Some(e.read_u32_bytes(note.desc.try_into().map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, "Invalid module ID size")
})?));
}
NT_SPLIT_VIRTUAL_ADDRESSES => {
let vec = if is_64 {
@ -83,11 +79,10 @@ impl SplitMeta {
Ok(result)
}
#[cfg(feature = "std")]
pub fn to_writer<E, W>(&self, writer: &mut W, e: E, is_64: bool) -> std::io::Result<()>
pub fn to_writer<E, W>(&self, writer: &mut W, e: E, is_64: bool) -> io::Result<()>
where
E: Endian,
W: std::io::Write + ?Sized,
W: Write + ?Sized,
{
if let Some(generator) = &self.generator {
write_note_header(writer, e, NT_SPLIT_GENERATOR, generator.len())?;
@ -142,9 +137,10 @@ impl SplitMeta {
}
}
/// Convert an object::read::Error to a String.
#[inline]
fn object_error(err: object::read::Error) -> anyhow::Error { anyhow::Error::new(err) }
/// 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)
}
/// An ELF note entry.
struct Note<'data> {
@ -165,27 +161,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) -> Result<Self> {
fn new(data: &'data [u8], align: u64, e: E, is_64: bool) -> io::Result<Self> {
Ok(if is_64 {
NoteIterator::B64(
object::read::elf::NoteIterator::new(e, align, data).map_err(object_error)?,
object::read::elf::NoteIterator::new(e, align, data).map_err(object_io_error)?,
)
} else {
NoteIterator::B32(
object::read::elf::NoteIterator::new(e, align as u32, data)
.map_err(object_error)?,
.map_err(object_io_error)?,
)
})
}
fn next(&mut self, e: E) -> Result<Option<Note<'data>>> {
fn next(&mut self, e: E) -> io::Result<Option<Note<'data>>> {
match self {
NoteIterator::B32(iter) => Ok(iter.next().map_err(object_error)?.map(|note| Note {
NoteIterator::B32(iter) => Ok(iter.next().map_err(object_io_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_error)?.map(|note| Note {
NoteIterator::B64(iter) => Ok(iter.next().map_err(object_io_error)?.map(|note| Note {
n_type: note.n_type(e),
name: note.name(),
desc: note.desc(),
@ -194,6 +190,16 @@ 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)
@ -202,11 +208,10 @@ where E: Endian
// Desc | variable size, padded to a 4 byte boundary
const NOTE_HEADER_SIZE: usize = 12 + ((ELF_NOTE_SPLIT.len() + 4) & !3);
#[cfg(feature = "std")]
fn write_note_header<E, W>(writer: &mut W, e: E, kind: u32, desc_len: usize) -> std::io::Result<()>
fn write_note_header<E, W>(writer: &mut W, e: E, kind: u32, desc_len: usize) -> io::Result<()>
where
E: Endian,
W: std::io::Write + ?Sized,
W: 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

View File

@ -1,15 +1,18 @@
use alloc::{format, vec::Vec};
use core::fmt;
use std::{
fmt::{LowerHex, UpperHex},
io::Read,
};
use anyhow::{Result, ensure};
use anyhow::Result;
use byteorder::{NativeEndian, ReadBytesExt};
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 N);
pub struct ReallySigned<N: PrimInt>(pub(crate) N);
impl<N: PrimInt> fmt::LowerHex for ReallySigned<N> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
impl<N: PrimInt> LowerHex for ReallySigned<N> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let num = self.0.to_i64().unwrap();
let prefix = if f.alternate() { "0x" } else { "" };
let bare_hex = format!("{:x}", num.abs());
@ -17,8 +20,8 @@ impl<N: PrimInt> fmt::LowerHex for ReallySigned<N> {
}
}
impl<N: PrimInt> fmt::UpperHex for ReallySigned<N> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
impl<N: PrimInt> UpperHex for ReallySigned<N> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let num = self.0.to_i64().unwrap();
let prefix = if f.alternate() { "0x" } else { "" };
let bare_hex = format!("{:X}", num.abs());
@ -26,66 +29,10 @@ impl<N: PrimInt> fmt::UpperHex for ReallySigned<N> {
}
}
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_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_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)) }
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>()?))
}

View File

@ -1,75 +0,0 @@
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);
}

View File

@ -1,82 +0,0 @@
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);
}

View File

@ -1,124 +0,0 @@
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);
}

View File

@ -1,124 +0,0 @@
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);
}

View File

@ -1,52 +0,0 @@
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)
};
}

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +0,0 @@
---
source: objdiff-core/tests/arch_arm.rs
expression: output
---
[(Line(90), Dim, 5), (Address(0), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32799), 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", 32779), 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)]

View File

@ -1,48 +0,0 @@
---
source: objdiff-core/tests/arch_arm.rs
expression: diff.instruction_rows
---
[
InstructionDiffRow {
ins_ref: Some(
InstructionRef {
address: 76,
size: 4,
opcode: 32799,
branch_dest: None,
},
),
kind: None,
branch_from: None,
branch_to: None,
arg_diff: [],
},
InstructionDiffRow {
ins_ref: Some(
InstructionRef {
address: 80,
size: 4,
opcode: 32779,
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: [],
},
]

File diff suppressed because it is too large Load Diff

View File

@ -1,112 +0,0 @@
---
source: objdiff-core/tests/arch_arm.rs
expression: output
---
[(Address(0), Dim, 5), (Spacing(4), Normal, 0), (Opcode("stmdb", 32895), Normal, 10), (Argument(Opaque("sp")), Normal, 0), (Argument(Opaque("!")), 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", 32818), 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", 32818), 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", 32774), 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", 32799), 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", 32786), 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", 32770), 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), (Basic(" #"), Normal, 0), (Argument(Unsigned(2)), Normal, 0), (Eol, Normal, 0)]
[(Address(28), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Address(32), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Address(36), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Address(40), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Address(44), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(192), Normal, 0), (Basic(" ~>"), Rotating(1), 0), (Eol, Normal, 0)]
[(Address(48), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(124), Normal, 0), (Basic(" ~>"), Rotating(2), 0), (Eol, Normal, 0)]
[(Address(52), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Address(56), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(140), Normal, 0), (Basic(" ~>"), Rotating(3), 0), (Eol, Normal, 0)]
[(Address(60), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(76), Normal, 0), (Basic(" ~>"), Rotating(4), 0), (Eol, Normal, 0)]
[(Address(64), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(152), Normal, 0), (Basic(" ~>"), Rotating(5), 0), (Eol, Normal, 0)]
[(Address(68), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(164), Normal, 0), (Basic(" ~>"), Rotating(6), 0), (Eol, Normal, 0)]
[(Address(72), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(164), Normal, 0), (Basic(" ~>"), Rotating(6), 0), (Eol, Normal, 0)]
[(Address(76), Dim, 5), (Basic(" ~> "), Rotating(4), 0), (Opcode("ldr", 32799), 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", 32799), 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", 32774), 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", 32800), 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", 32786), 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", 32773), Normal, 10), (BranchDest(108), Normal, 0), (Basic(" ~>"), Rotating(7), 0), (Eol, Normal, 0)]
[(Address(100), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), Normal, 10), (BranchDest(424), Normal, 0), (Basic(" ~>"), Rotating(8), 0), (Eol, Normal, 0)]
[(Address(104), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), 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", 32799), 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", 32799), 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", 32777), 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", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Address(124), Dim, 5), (Basic(" ~> "), Rotating(2), 0), (Opcode("mov", 32818), 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", 32818), 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", 32774), 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", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Address(140), Dim, 5), (Basic(" ~> "), Rotating(3), 0), (Opcode("mov", 32818), 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", 32774), 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", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Address(152), Dim, 5), (Basic(" ~> "), Rotating(5), 0), (Opcode("mov", 32818), 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", 32774), 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", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Address(164), Dim, 5), (Basic(" ~> "), Rotating(6), 0), (Opcode("ldr", 32799), 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", 32818), 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", 32799), 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", 32818), 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", 32899), 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", 32774), 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", 32773), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Address(192), Dim, 5), (Basic(" ~> "), Rotating(1), 0), (Opcode("mov", 32818), 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", 32774), 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", 32799), 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", 32829), 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", 32786), 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", 32773), Normal, 10), (BranchDest(236), Normal, 0), (Basic(" ~>"), Rotating(9), 0), (Eol, Normal, 0)]
[(Address(216), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 32818), 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", 32774), 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", 32799), 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", 32799), 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", 32778), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Eol, Normal, 0)]
[(Address(236), Dim, 5), (Basic(" ~> "), Rotating(9), 0), (Opcode("ldr", 32799), 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", 32786), 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", 32773), Normal, 10), (BranchDest(288), Normal, 0), (Basic(" ~>"), Rotating(10), 0), (Eol, Normal, 0)]
[(Address(248), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bge", 32773), Normal, 10), (BranchDest(296), Normal, 0), (Basic(" ~>"), Rotating(11), 0), (Eol, Normal, 0)]
[(Address(252), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 32786), 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", 32773), Normal, 10), (BranchDest(308), Normal, 0), (Basic(" ~>"), Rotating(12), 0), (Eol, Normal, 0)]
[(Address(260), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mvn", 32829), 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", 32786), 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", 32773), Normal, 10), (BranchDest(308), Normal, 0), (Basic(" ~>"), Rotating(12), 0), (Eol, Normal, 0)]
[(Address(272), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmpne", 32786), 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", 32786), 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", 32773), Normal, 10), (BranchDest(340), Normal, 0), (Basic(" ~>"), Rotating(13), 0), (Eol, Normal, 0)]
[(Address(284), Dim, 5), (Spacing(4), Normal, 0), (Opcode("b", 32773), Normal, 10), (BranchDest(308), Normal, 0), (Basic(" ~>"), Rotating(12), 0), (Eol, Normal, 0)]
[(Address(288), Dim, 5), (Basic(" ~> "), Rotating(10), 0), (Opcode("cmp", 32786), 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", 32773), Normal, 10), (BranchDest(308), Normal, 0), (Basic(" ~>"), Rotating(12), 0), (Eol, Normal, 0)]
[(Address(296), Dim, 5), (Basic(" ~> "), Rotating(11), 0), (Opcode("mov", 32818), 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", 32774), 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", 32773), Normal, 10), (BranchDest(340), Normal, 0), (Basic(" ~>"), Rotating(13), 0), (Eol, Normal, 0)]
[(Address(308), Dim, 5), (Basic(" ~> "), Rotating(12), 0), (Opcode("mov", 32818), 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", 32774), 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", 32786), 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", 32786), 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", 32773), Normal, 10), (BranchDest(340), Normal, 0), (Basic(" ~>"), Rotating(13), 0), (Eol, Normal, 0)]
[(Address(328), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), 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", 32818), 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", 32899), 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", 32829), 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", 32770), 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", 32770), 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", 32898), 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", 32786), 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", 32773), Normal, 10), (BranchDest(384), Normal, 0), (Basic(" ~>"), Rotating(14), 0), (Eol, Normal, 0)]
[(Address(364), Dim, 5), (Basic(" ~> "), Rotating(15), 0), (Opcode("mov", 32818), 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", 32774), 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", 32770), 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", 32786), 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", 32773), Normal, 10), (BranchDest(364), Normal, 0), (Basic(" ~>"), Rotating(15), 0), (Eol, Normal, 0)]
[(Address(384), Dim, 5), (Basic(" ~> "), Rotating(14), 0), (Opcode("ldr", 32799), 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", 32799), 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", 32800), 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", 32786), 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", 32773), Normal, 10), (BranchDest(408), Normal, 0), (Basic(" ~>"), Rotating(16), 0), (Eol, Normal, 0)]
[(Address(404), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 32774), 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", 32818), 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", 32899), 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", 32793), Normal, 10), (Argument(Opaque("sp")), Normal, 0), (Argument(Opaque("!")), 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)]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,110 +0,0 @@
---
source: objdiff-core/tests/arch_arm.rs
expression: output
---
[(Line(37), Dim, 5), (Address(0), Dim, 5), (Spacing(4), Normal, 0), (Opcode("push", 56), Normal, 10), (Basic("{"), Normal, 0), (Argument(Opaque("r3")), 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("r7")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("lr")), Normal, 0), (Basic("}"), Normal, 0), (Eol, Normal, 0)]
[(Line(37), Dim, 5), (Address(2), Dim, 5), (Spacing(4), Normal, 0), (Opcode("sub", 74), Normal, 10), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(16)), Normal, 0), (Eol, Normal, 0)]
[(Line(37), Dim, 5), (Address(4), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
[(Line(39), Dim, 5), (Address(6), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Eol, Normal, 0)]
[(Line(39), Dim, 5), (Address(8), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r1")), Normal, 0), (Eol, Normal, 0)]
[(Line(39), Dim, 5), (Address(10), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r2")), Normal, 0), (Eol, Normal, 0)]
[(Line(39), Dim, 5), (Address(12), Dim, 5), (Spacing(4), Normal, 0), (Opcode("str", 66), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(4)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(39), Dim, 5), (Address(14), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 19), Normal, 10), (Symbol(Symbol { name: "PokeSet_IsRemovedAll", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-4), Bright, 0), (Eol, Normal, 0)]
[(Line(39), Dim, 5), (Address(18), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 25), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
[(Line(39), Dim, 5), (Address(20), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bne", 15), Normal, 10), (BranchDest(212), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Line(44), Dim, 5), (Address(22), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldrh", 38), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(44), Dim, 5), (Address(24), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 25), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(164)), Normal, 0), (Eol, Normal, 0)]
[(Line(44), Dim, 5), (Address(26), Dim, 5), (Spacing(4), Normal, 0), (Opcode("beq", 15), Normal, 10), (BranchDest(48), Normal, 0), (Basic(" ~>"), Rotating(1), 0), (Eol, Normal, 0)]
[(Line(44), Dim, 5), (Address(28), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 34), 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(184)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(216), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Line(44), Dim, 5), (Address(30), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 26), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
[(Line(44), Dim, 5), (Address(32), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bne", 15), Normal, 10), (BranchDest(94), Normal, 0), (Basic(" ~>"), Rotating(2), 0), (Eol, Normal, 0)]
[(Line(47), Dim, 5), (Address(34), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 35), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(4)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(47), Dim, 5), (Address(36), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
[(Line(47), Dim, 5), (Address(38), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Eol, Normal, 0)]
[(Line(47), Dim, 5), (Address(40), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 19), Normal, 10), (Symbol(Symbol { name: "ServerDisplay_SkillSwap", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-4), Bright, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(44), Dim, 5), (Spacing(4), Normal, 0), (Opcode("add", 7), Normal, 10), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(16)), Normal, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(46), Dim, 5), (Spacing(4), Normal, 0), (Opcode("pop", 55), Normal, 10), (Basic("{"), Normal, 0), (Argument(Opaque("r3")), 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("r7")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic("}"), Normal, 0), (Eol, Normal, 0)]
[(Line(50), Dim, 5), (Address(48), Dim, 5), (Basic(" ~> "), Rotating(1), 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
[(Line(50), Dim, 5), (Address(50), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Eol, Normal, 0)]
[(Line(50), Dim, 5), (Address(52), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 19), Normal, 10), (Symbol(Symbol { name: "ServerEvent_CreateSubstitute", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-4), Bright, 0), (Eol, Normal, 0)]
[(Line(50), Dim, 5), (Address(56), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 25), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
[(Line(50), Dim, 5), (Address(58), Dim, 5), (Spacing(4), Normal, 0), (Opcode("beq", 15), Normal, 10), (BranchDest(212), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(60), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 34), 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(156)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(220), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(62), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 33), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(64), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldrb", 36), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(5)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(66), Dim, 5), (Spacing(4), Normal, 0), (Opcode("lsl", 42), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(31)), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(68), Dim, 5), (Spacing(4), Normal, 0), (Opcode("lsr", 44), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(31)), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(70), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bne", 15), Normal, 10), (BranchDest(212), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(72), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 46), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(1)), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(74), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bic", 17), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r1")), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(76), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 46), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(1)), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(78), Dim, 5), (Spacing(4), Normal, 0), (Opcode("orr", 54), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r2")), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(80), Dim, 5), (Spacing(4), Normal, 0), (Opcode("strb", 67), 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(5)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(82), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldrb", 36), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(5)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(84), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 46), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(2)), Normal, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(86), Dim, 5), (Spacing(4), Normal, 0), (Opcode("add", 7), Normal, 10), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(16)), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(88), Dim, 5), (Spacing(4), Normal, 0), (Opcode("orr", 54), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r2")), Normal, 0), (Eol, Normal, 0)]
[(Line(52), Dim, 5), (Address(90), Dim, 5), (Spacing(4), Normal, 0), (Opcode("strb", 67), 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(5)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(92), Dim, 5), (Spacing(4), Normal, 0), (Opcode("pop", 55), Normal, 10), (Basic("{"), Normal, 0), (Argument(Opaque("r3")), 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("r7")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic("}"), Normal, 0), (Eol, Normal, 0)]
[(Line(57), Dim, 5), (Address(94), Dim, 5), (Basic(" ~> "), Rotating(2), 0), (Opcode("ldr", 34), 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(128)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(224), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Line(57), Dim, 5), (Address(96), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 34), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(128)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(228), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Line(57), Dim, 5), (Address(98), Dim, 5), (Spacing(4), Normal, 0), (Opcode("add", 4), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
[(Line(57), Dim, 5), (Address(100), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 19), Normal, 10), (Symbol(Symbol { name: "HEManager_PushState", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-4), Bright, 0), (Eol, Normal, 0)]
[(Line(57), Dim, 5), (Address(104), Dim, 5), (Spacing(4), Normal, 0), (Opcode("str", 66), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(8)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(61), Dim, 5), (Address(106), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
[(Line(61), Dim, 5), (Address(108), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 19), Normal, 10), (Symbol(Symbol { name: "BattleHandler_Result", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-4), Bright, 0), (Eol, Normal, 0)]
[(Line(61), Dim, 5), (Address(112), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r7")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
[(Line(63), Dim, 5), (Address(114), Dim, 5), (Spacing(4), Normal, 0), (Opcode("add", 6), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(12)), Normal, 0), (Eol, Normal, 0)]
[(Line(63), Dim, 5), (Address(116), Dim, 5), (Spacing(4), Normal, 0), (Opcode("str", 66), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(63), Dim, 5), (Address(118), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 35), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(4)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(63), Dim, 5), (Address(120), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
[(Line(63), Dim, 5), (Address(122), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Eol, Normal, 0)]
[(Line(63), Dim, 5), (Address(124), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Eol, Normal, 0)]
[(Line(63), Dim, 5), (Address(126), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 19), Normal, 10), (Symbol(Symbol { name: "ServerEvent_UncategorizedMove", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-4), Bright, 0), (Eol, Normal, 0)]
[(Line(63), Dim, 5), (Address(130), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 25), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
[(Line(63), Dim, 5), (Address(132), Dim, 5), (Spacing(4), Normal, 0), (Opcode("beq", 15), Normal, 10), (BranchDest(168), Normal, 0), (Basic(" ~>"), Rotating(3), 0), (Eol, Normal, 0)]
[(Line(65), Dim, 5), (Address(134), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Eol, Normal, 0)]
[(Line(65), Dim, 5), (Address(136), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 19), Normal, 10), (Symbol(Symbol { name: "BattleHandler_Result", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-4), Bright, 0), (Eol, Normal, 0)]
[(Line(65), Dim, 5), (Address(140), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 47), Normal, 10), (Argument(Opaque("r7")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
[(Line(66), Dim, 5), (Address(142), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 25), Normal, 10), (Argument(Opaque("r7")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(2)), Normal, 0), (Eol, Normal, 0)]
[(Line(66), Dim, 5), (Address(144), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bne", 15), Normal, 10), (BranchDest(168), Normal, 0), (Basic(" ~>"), Rotating(3), 0), (Eol, Normal, 0)]
[(Line(68), Dim, 5), (Address(146), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 34), 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(72)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(220), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Line(68), Dim, 5), (Address(148), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 33), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(68), Dim, 5), (Address(150), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldrb", 36), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(5)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(68), Dim, 5), (Address(152), Dim, 5), (Spacing(4), Normal, 0), (Opcode("lsl", 42), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(31)), Normal, 0), (Eol, Normal, 0)]
[(Line(68), Dim, 5), (Address(154), Dim, 5), (Spacing(4), Normal, 0), (Opcode("lsr", 44), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(31)), Normal, 0), (Eol, Normal, 0)]
[(Line(68), Dim, 5), (Address(156), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bne", 15), Normal, 10), (BranchDest(168), Normal, 0), (Basic(" ~>"), Rotating(3), 0), (Eol, Normal, 0)]
[(Line(68), Dim, 5), (Address(158), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 46), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(1)), Normal, 0), (Eol, Normal, 0)]
[(Line(68), Dim, 5), (Address(160), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bic", 17), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r1")), Normal, 0), (Eol, Normal, 0)]
[(Line(68), Dim, 5), (Address(162), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 46), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(1)), Normal, 0), (Eol, Normal, 0)]
[(Line(68), Dim, 5), (Address(164), Dim, 5), (Spacing(4), Normal, 0), (Opcode("orr", 54), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r2")), Normal, 0), (Eol, Normal, 0)]
[(Line(68), Dim, 5), (Address(166), Dim, 5), (Spacing(4), Normal, 0), (Opcode("strb", 67), 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(5)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(72), Dim, 5), (Address(168), Dim, 5), (Basic(" ~> "), Rotating(3), 0), (Opcode("cmp", 25), Normal, 10), (Argument(Opaque("r7")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(1)), Normal, 0), (Eol, Normal, 0)]
[(Line(72), Dim, 5), (Address(170), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bhi", 15), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(4), 0), (Eol, Normal, 0)]
[(Line(75), Dim, 5), (Address(172), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 35), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(12)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(75), Dim, 5), (Address(174), Dim, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 25), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
[(Line(75), Dim, 5), (Address(176), Dim, 5), (Spacing(4), Normal, 0), (Opcode("beq", 15), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(4), 0), (Eol, Normal, 0)]
[(Line(75), Dim, 5), (Address(178), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 34), 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(52)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(232), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Line(75), Dim, 5), (Address(180), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldrb", 37), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(75), Dim, 5), (Address(182), Dim, 5), (Spacing(4), Normal, 0), (Opcode("lsl", 42), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(27)), Normal, 0), (Eol, Normal, 0)]
[(Line(75), Dim, 5), (Address(184), Dim, 5), (Spacing(4), Normal, 0), (Opcode("lsr", 44), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(31)), Normal, 0), (Eol, Normal, 0)]
[(Line(75), Dim, 5), (Address(186), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bne", 15), Normal, 10), (BranchDest(200), Normal, 0), (Basic(" ~>"), Rotating(4), 0), (Eol, Normal, 0)]
[(Line(77), Dim, 5), (Address(188), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32), 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(12)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(77), Dim, 5), (Address(190), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 34), Normal, 10), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(44)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(236), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Line(77), Dim, 5), (Address(192), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 46), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(90)), Normal, 0), (Eol, Normal, 0)]
[(Line(77), Dim, 5), (Address(194), Dim, 5), (Spacing(4), Normal, 0), (Opcode("mov", 46), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(71)), Normal, 0), (Eol, Normal, 0)]
[(Line(77), Dim, 5), (Address(196), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 19), Normal, 10), (Symbol(Symbol { name: "SCQUE_PUT_MsgImpl", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-4), Bright, 0), (Eol, Normal, 0)]
[(Line(81), Dim, 5), (Address(200), Dim, 5), (Basic(" ~> "), Rotating(4), 0), (Opcode("ldr", 34), 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(20)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(224), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Line(81), Dim, 5), (Address(202), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 35), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(8)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Line(81), Dim, 5), (Address(204), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 34), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(32)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(240), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Line(81), Dim, 5), (Address(206), Dim, 5), (Spacing(4), Normal, 0), (Opcode("add", 4), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Eol, Normal, 0)]
[(Line(81), Dim, 5), (Address(208), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 19), Normal, 10), (Symbol(Symbol { name: "HEManager_PopState", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-4), Bright, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(212), Dim, 5), (Basic(" ~> "), Rotating(0), 0), (Opcode("add", 7), Normal, 10), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(16)), Normal, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(214), Dim, 5), (Spacing(4), Normal, 0), (Opcode("pop", 55), Normal, 10), (Basic("{"), Normal, 0), (Argument(Opaque("r3")), 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("r7")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic("}"), Normal, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(216), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(285)), Normal, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(220), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(1192)), Normal, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(224), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(7544)), Normal, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(228), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(9103)), Normal, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(232), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(1930)), Normal, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(236), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(4294901760)), Normal, 0), (Eol, Normal, 0)]
[(Line(86), Dim, 5), (Address(240), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(9129)), Normal, 0), (Eol, Normal, 0)]

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
---
source: objdiff-core/tests/arch_arm.rs
expression: output
---
[(Address(0), Dim, 5), (Spacing(4), Normal, 0), (Opcode("str", 64), 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(36)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Address(2), Dim, 5), (Spacing(4), Normal, 0), (Opcode("str", 64), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(40)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
[(Address(4), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bx", 23), Normal, 10), (Argument(Opaque("lr")), Normal, 0), (Eol, Normal, 0)]

View File

@ -1,48 +0,0 @@
---
source: objdiff-core/tests/arch_arm.rs
expression: diff.instruction_rows
---
[
InstructionDiffRow {
ins_ref: Some(
InstructionRef {
address: 0,
size: 2,
opcode: 64,
branch_dest: None,
},
),
kind: None,
branch_from: None,
branch_to: None,
arg_diff: [],
},
InstructionDiffRow {
ins_ref: Some(
InstructionRef {
address: 2,
size: 2,
opcode: 64,
branch_dest: None,
},
),
kind: None,
branch_from: None,
branch_to: None,
arg_diff: [],
},
InstructionDiffRow {
ins_ref: Some(
InstructionRef {
address: 4,
size: 2,
opcode: 23,
branch_dest: None,
},
),
kind: None,
branch_from: None,
branch_to: None,
arg_diff: [],
},
]

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