Compare commits

...

28 Commits

Author SHA1 Message Date
8250d26b77 Support R_MIPS_LITERAL, R_MIPS15_S3 relocations
Resolves #92
Resolves #95
2024-08-18 22:05:16 -06:00
fd555a6e0f Fix reading little-endian .line section 2024-08-18 21:57:53 -06:00
3710b6a91e Try even harder to recover from protoc missing 2024-08-18 14:01:49 -06:00
faebddbc5e More updates to report types 2024-08-18 13:42:41 -06:00
a733a950a3 Avoid requiring protoc unless protos change 2024-08-18 13:40:49 -06:00
cad9b70632 Support protobuf format for reports
This migrates to using protobuf to
define the "report" and "changes"
formats in objdiff-cli.

The JSON output now uses the Proto3
"JSON Mapping", which is slightly
incompatible with the existing JSON
format. Mainly, 64-bit numbers are
represented as strings, and addresses
are decimal strings instead of hex.

However, the older JSON format is
still accepted by "report changes"
to ease migration.
2024-08-16 00:52:24 -06:00
cf937b0be9 Guard against symbols larger than section 2024-08-11 16:54:24 -06:00
23b6d33a98 Fix botched find/replace 2024-08-11 16:53:35 -06:00
f17ee83622 Version v2.0.0-beta.3 2024-08-11 16:02:36 -06:00
615ec4c50a mips: Support R_MIPS_PC16 relocations 2024-08-11 16:00:10 -06:00
2cc10b0d06 cargo +nightly fmt 2024-08-11 14:29:58 -06:00
8091941448 Version v2.0.0-beta.2 2024-08-11 14:27:47 -06:00
de74dfdba7 Add dummy symbols to empty sections
Allows diffing sections without symbols

Resolves #87
2024-08-11 14:27:27 -06:00
177bd5e895 Fix branch immediates missing diff colors
Resolves #86
2024-08-11 14:01:53 -06:00
e1ccee1e73 Improve data section diffing more
This re-implements the older algorithm
used for data and BSS section match
percentages. We perform both and
choose the highest match percent
between the two options.

Resolves #84, #85
2024-08-11 13:53:52 -06:00
952b6a63c3 Better graphics backend fallback
This attempts the following in order:
- wgpu with user-selected backend
- wgpu with automatic backend
- glow (fallback OpenGL backend)

This should eliminate most issues
where objdiff fails to launch.
2024-08-11 13:33:10 -06:00
Steven Casper
09cc9952df Support R_MIPS_GPREL16 relocations correctly (#88)
* Support R_MIPS_GPREL16 relocations correctly

symbols defined in the same file require adding a
special ri_gp_value from the .reginfo section to
their relocation calculations.

* Run nightly rustfmt

* Prevent potential panic when slicing .reginfo
2024-08-08 20:20:41 -06:00
fc598af329 Version 2.0.0-beta.1 2024-07-21 23:03:15 -06:00
871407622d Use regex for symbol search
Also fixes case insensitivity and
properly searches the .comm section

Fixes #80
2024-07-21 23:01:58 -06:00
e3fff7b0dc Improve data section diff logic
More accurate in more cases

Fixes #81
2024-07-21 22:52:46 -06:00
Amber Brault
75b0e7d9e5 Exception table diff view (#82)
* Basic integration

* Implement basic right click option

Needs lotsa work

* nothing to worry about

* Convert extab diff to separate view

* Make clippy and fmt shut up

* Make clippy fmt shut up for real this time

* Print extab/extabindex symbol names in extab view

* I hate fmt

* Basic integration

* Implement basic right click option

Needs lotsa work

* nothing to worry about

* Convert extab diff to separate view

* Make clippy and fmt shut up

* Make clippy fmt shut up for real this time

* Print extab/extabindex symbol names in extab view

* I hate fmt

* Fix scroll position not being maintained from extab view

* Silly me

* Add rlwinm decoder window

* Remove extra files

* Create Cargo.lock

* Show extab symbol names in hover window

* Appease fmt

* Update symbol_diff.rs

* Update symbol_diff.rs

* Get extab symbol from extabindex relocations instead

* Update Cargo.lock

* Update Cargo.lock
2024-07-21 22:25:54 -06:00
Amber Brault
9f71ce9fea Add rlwinm decoder window (#83)
* Add rlwinm decoder window

* Remove extra files

* Create Cargo.lock

* Make fmt happy

* Update Cargo.lock

* Update Cargo.lock

* Update Cargo.lock
2024-07-21 17:56:46 -06:00
Aetias
d9fb48853e Options for ARM disassembly style (#78) 2024-07-14 16:00:57 -06:00
233839346a Version v2.0.0-alpha.5 2024-06-20 20:31:31 -06:00
95615c2ec5 Improve MIPS ABI auto-detection 2024-06-20 19:57:18 -06:00
Aetias
97981160f4 ARMv4T (GBA) and ARMv6K (3DS) support (#75)
* Initial ARM support

* Disassemble const pool reloc

* Disasm ARM/Thumb/data based on mapping symbols

* Fallback to mapping symbol `$a`

* Support multiple DWARF sequences

* Update line info

* Rework DWARF line info parsing

- Properly handles multiple sections
  in DWARF 1
- line_info moved into ObjSection
- DWARF 2 parser no longer errors with
  no .text section
- Both parsers properly skip empty
  sections

* Simplify line_info (no Option)

* Get line info from section; output formatted ins string

* Unwrap code section in `arm.rs`

* Handle reloc `R_ARM_SBREL32`

* Update ARM disassembler

* Update README.md

* Format

* Revert "Update README.md"

This reverts commit 8bbfcc6f45.

* Update README.md

* Detect ARM version; support ARMv4T and v6K

* Combobox to force ARM version

* Clear LSB in ARM symbol addresses

* Support big-endian ARM ELF files

* Bump `unarm`, `arm-attr`

* Handle ARM implicit addends

* Update README.md

* Explicitly handle all ARM argument types

* Format

* Display more ARM relocs

* Mask LSB on ARM code symbols only

* Read ARM implicit addends

* Format

---------

Co-authored-by: Luke Street <luke.street@encounterpc.com>
2024-06-20 18:36:25 -06:00
Aetias
1fd901a863 Option to combine data sections (#76)
Co-authored-by: Luke Street <luke.street@encounterpc.com>
2024-06-18 22:05:24 -06:00
759d55994a Fix clippy warning 2024-06-18 21:49:19 -06:00
35 changed files with 2122 additions and 509 deletions

1
.gitignore vendored
View File

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

211
Cargo.lock generated
View File

@@ -110,9 +110,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.82"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "arboard"
@@ -150,6 +150,15 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "arm-attr"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d0cabd3a7d2dfa96ab3faa7b532a83c5e090061bf6d83197ca2bc91f5afac6c"
dependencies = [
"thiserror",
]
[[package]]
name = "arrayvec"
version = "0.7.4"
@@ -515,9 +524,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.6.0"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
[[package]]
name = "calloop"
@@ -961,6 +970,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e06f9bce634a3c898eb1e5cb949ff63133cbb218af93cc9b38b31d6f3ea285"
[[package]]
name = "cwextab"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92e840be31d11ead5f357b8fc503133d3657845d0ccf539afcc7d1212fad226"
dependencies = [
"anyhow",
]
[[package]]
name = "d3d12"
version = "0.19.0"
@@ -968,7 +986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307"
dependencies = [
"bitflags 2.5.0",
"libloading 0.7.4",
"libloading 0.8.3",
"winapi",
]
@@ -1457,6 +1475,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "fixedbitset"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flagset"
version = "0.4.5"
@@ -1913,6 +1937,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
@@ -2174,6 +2204,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.11"
@@ -2265,9 +2304,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.154"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libloading"
@@ -2461,6 +2500,12 @@ dependencies = [
"bitflags 2.5.0",
]
[[package]]
name = "multimap"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
[[package]]
name = "naga"
version = "0.19.2"
@@ -2780,13 +2825,18 @@ dependencies = [
[[package]]
name = "objdiff-cli"
version = "2.0.0-alpha.4"
version = "2.0.0-beta.4"
dependencies = [
"anyhow",
"argp",
"crossterm",
"enable-ansi-support",
"memmap2",
"objdiff-core",
"pbjson",
"pbjson-build",
"prost",
"prost-build",
"ratatui",
"rayon",
"serde",
@@ -2799,12 +2849,14 @@ dependencies = [
[[package]]
name = "objdiff-core"
version = "2.0.0-alpha.4"
version = "2.0.0-beta.4"
dependencies = [
"anyhow",
"arm-attr",
"byteorder",
"cpp_demangle",
"cwdemangle",
"cwextab",
"filetime",
"flagset",
"gimli 0.29.0",
@@ -2828,7 +2880,7 @@ dependencies = [
[[package]]
name = "objdiff-gui"
version = "2.0.0-alpha.4"
version = "2.0.0-beta.4"
dependencies = [
"anyhow",
"bytes",
@@ -2836,6 +2888,7 @@ dependencies = [
"console_error_panic_hook",
"const_format",
"cwdemangle",
"cwextab",
"dirs",
"eframe",
"egui",
@@ -2851,8 +2904,10 @@ dependencies = [
"path-slash",
"png",
"pollster",
"regex",
"reqwest",
"rfd",
"rlwinmdec",
"ron",
"self_update",
"serde",
@@ -2895,9 +2950,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "openssl"
version = "0.10.64"
version = "0.10.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
dependencies = [
"bitflags 2.5.0",
"cfg-if",
@@ -2927,9 +2982,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.102"
version = "0.9.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
dependencies = [
"cc",
"libc",
@@ -3037,12 +3092,44 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "pbjson"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e6349fa080353f4a597daffd05cb81572a9c031a6d4fff7e504947496fcc68"
dependencies = [
"base64 0.21.7",
"serde",
]
[[package]]
name = "pbjson-build"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eea3058763d6e656105d1403cb04e0a41b7bbac6362d413e7c33be0c32279c9"
dependencies = [
"heck 0.5.0",
"itertools 0.13.0",
"prost",
"prost-types",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "petgraph"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]]
name = "pin-project"
version = "1.1.5"
@@ -3155,6 +3242,16 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
[[package]]
name = "prettyplease"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e"
dependencies = [
"proc-macro2",
"syn 2.0.60",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -3189,6 +3286,59 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
[[package]]
name = "prost"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13db3d3fde688c61e2446b4d843bc27a7e8af269a69440c0308021dc92333cc"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-build"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb182580f71dd070f88d01ce3de9f4da5021db7115d2e1c3605a754153b77c1"
dependencies = [
"bytes",
"heck 0.5.0",
"itertools 0.13.0",
"log",
"multimap",
"once_cell",
"petgraph",
"prettyplease",
"prost",
"prost-types",
"regex",
"syn 2.0.60",
"tempfile",
]
[[package]]
name = "prost-derive"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca"
dependencies = [
"anyhow",
"itertools 0.13.0",
"proc-macro2",
"quote",
"syn 2.0.60",
]
[[package]]
name = "prost-types"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cee5168b05f49d4b0ca581206eb14a7b22fafd963efe729ac48eb03266e25cc2"
dependencies = [
"prost",
]
[[package]]
name = "pulldown-cmark"
version = "0.9.6"
@@ -3286,7 +3436,7 @@ dependencies = [
"compact_str",
"crossterm",
"indoc",
"itertools",
"itertools 0.12.1",
"lru",
"paste",
"stability",
@@ -3367,9 +3517,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.4"
version = "1.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
dependencies = [
"aho-corasick",
"memchr",
@@ -3499,6 +3649,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rlwinmdec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2076dbc187938f3db71c03c85d143febf01026631189dc8ca85f8c886d90ea12"
dependencies = [
"anyhow",
]
[[package]]
name = "ron"
version = "0.8.1"
@@ -3956,7 +4115,7 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
dependencies = [
"heck",
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
@@ -4029,18 +4188,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.59"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.59"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
@@ -4320,9 +4479,9 @@ dependencies = [
[[package]]
name = "unarm"
version = "1.0.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c13fc9a9c95348bf7565e5c30688fc288239962958cac0ccdc7cd009141d850"
checksum = "379762d9433a2e6e498cde97801fb238318b024a513d0843eeac98b9056b9f3c"
[[package]]
name = "unicase"
@@ -4733,9 +4892,9 @@ dependencies = [
[[package]]
name = "wgpu-hal"
version = "0.19.4"
version = "0.19.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1a4924366df7ab41a5d8546d6534f1f33231aa5b3f72b9930e300f254e39c3"
checksum = "bfabcfc55fd86611a855816326b2d54c3b2fd7972c27ce414291562650552703"
dependencies = [
"android_system_properties",
"arrayvec",

View File

@@ -17,7 +17,7 @@ Supports:
- PowerPC 750CL (GameCube, Wii)
- MIPS (N64, PS1, PS2, PSP)
- x86 (COFF only at the moment)
- ARMv5 (DS)
- ARM (GBA, DS, 3DS)
See [Usage](#usage) for more information.

View File

@@ -1,6 +1,6 @@
[package]
name = "objdiff-cli"
version = "2.0.0-alpha.4"
version = "2.0.0-beta.4"
edition = "2021"
rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"]
@@ -18,7 +18,10 @@ anyhow = "1.0.82"
argp = "0.3.0"
crossterm = "0.27.0"
enable-ansi-support = "0.2.1"
memmap2 = "0.9.4"
objdiff-core = { path = "../objdiff-core", features = ["all"] }
pbjson = "0.7.0"
prost = "0.13.1"
ratatui = "0.26.2"
rayon = "1.10.0"
serde = { version = "1", features = ["derive"] }
@@ -27,3 +30,8 @@ supports-color = "3.0.0"
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
[build-dependencies]
prost-build = "0.13.1"
pbjson-build = "0.7.0"

View File

@@ -1,3 +1,5 @@
use std::path::{Path, PathBuf};
fn main() {
let output = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
@@ -6,4 +8,55 @@ fn main() {
let rev = String::from_utf8(output.stdout).expect("Failed to parse git output");
println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}");
println!("cargo:rustc-rerun-if-changed=.git/HEAD");
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
let descriptor_path = root.join("proto_descriptor.bin");
println!("cargo:rerun-if-changed={}", descriptor_path.display());
let descriptor_mtime = std::fs::metadata(&descriptor_path)
.map(|m| m.modified().unwrap())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
let mut run_protoc = false;
let proto_files = vec![root.join("report.proto")];
for proto_file in &proto_files {
println!("cargo:rerun-if-changed={}", proto_file.display());
let mtime = match std::fs::metadata(proto_file) {
Ok(m) => m.modified().unwrap(),
Err(e) => panic!("Failed to stat proto file {}: {:?}", proto_file.display(), e),
};
if mtime > descriptor_mtime {
run_protoc = true;
}
}
fn prost_config(descriptor_path: &Path, run_protoc: bool) -> prost_build::Config {
let mut config = prost_build::Config::new();
config.file_descriptor_set_path(descriptor_path);
// If our cached descriptor is up-to-date, we don't need to run protoc.
// This is helpful so that users don't need to have protoc installed
// unless they're updating the protos.
if !run_protoc {
config.skip_protoc_run();
}
config
}
if let Err(e) =
prost_config(&descriptor_path, run_protoc).compile_protos(&proto_files, &[root.as_path()])
{
if e.kind() == std::io::ErrorKind::NotFound && e.to_string().contains("protoc") {
eprintln!("protoc not found, skipping protobuf compilation");
prost_config(&descriptor_path, false)
.compile_protos(&proto_files, &[root.as_path()])
.expect("Failed to compile protos");
} else {
panic!("Failed to compile protos: {e:?}");
}
}
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
pbjson_build::Builder::new()
.register_descriptors(&descriptor_set)
.expect("Failed to register descriptors")
.preserve_proto_field_names()
.build(&[".objdiff"])
.expect("Failed to build pbjson");
}

Binary file not shown.

View File

@@ -0,0 +1,135 @@
syntax = "proto3";
package objdiff.report;
// Progress info for a report or unit
message Measures {
// Overall match percent, including partially matched functions and data
float fuzzy_match_percent = 1;
// Total size of code in bytes
uint64 total_code = 2;
// Fully matched code size in bytes
uint64 matched_code = 3;
// Fully matched code percent
float matched_code_percent = 4;
// Total size of data in bytes
uint64 total_data = 5;
// Fully matched data size in bytes
uint64 matched_data = 6;
// Fully matched data percent
float matched_data_percent = 7;
// Total number of functions
uint32 total_functions = 8;
// Fully matched functions
uint32 matched_functions = 9;
// Fully matched functions percent
float matched_functions_percent = 10;
}
// Project progress report
message Report {
// Overall progress info
Measures measures = 1;
// Units within this report
repeated ReportUnit units = 2;
}
// A unit of the report (usually a translation unit)
message ReportUnit {
// The name of the unit
string name = 1;
// Progress info for this unit
Measures measures = 2;
// Sections within this unit
repeated ReportItem sections = 3;
// Functions within this unit
repeated ReportItem functions = 4;
// Extra metadata for this unit
optional ReportUnitMetadata metadata = 5;
}
// Extra metadata for a unit
message ReportUnitMetadata {
// Whether this unit is marked as complete (or "linked")
optional bool complete = 1;
// The name of the module this unit belongs to
optional string module_name = 2;
// The ID of the module this unit belongs to
optional uint32 module_id = 3;
// The path to the source file of this unit
optional string source_path = 4;
}
// A section or function within a unit
message ReportItem {
// The name of the item
string name = 1;
// The size of the item in bytes
uint64 size = 2;
// The overall match percent for this item
float fuzzy_match_percent = 3;
// Extra metadata for this item
optional ReportItemMetadata metadata = 4;
}
// Extra metadata for an item
message ReportItemMetadata {
// The demangled name of the function
optional string demangled_name = 1;
// The virtual address of the function or section
optional uint64 virtual_address = 2;
}
// A pair of reports to compare and generate changes
message ChangesInput {
// The previous report
Report from = 1;
// The current report
Report to = 2;
}
// Changes between two reports
message Changes {
// The progress info for the previous report
Measures from = 1;
// The progress info for the current report
Measures to = 2;
// Units that changed
repeated ChangeUnit units = 3;
}
// A changed unit
message ChangeUnit {
// The name of the unit
string name = 1;
// The previous progress info (omitted if new)
optional Measures from = 2;
// The current progress info (omitted if removed)
optional Measures to = 3;
// Sections that changed
repeated ChangeItem sections = 4;
// Functions that changed
repeated ChangeItem functions = 5;
// Extra metadata for this unit
optional ReportUnitMetadata metadata = 6;
}
// A changed section or function
message ChangeItem {
// The name of the item
string name = 1;
// The previous progress info (omitted if new)
optional ChangeItemInfo from = 2;
// The current progress info (omitted if removed)
optional ChangeItemInfo to = 3;
// Extra metadata for this item
optional ReportItemMetadata metadata = 4;
}
// Progress info for a section or function
message ChangeItemInfo {
// The overall match percent for this item
float fuzzy_match_percent = 1;
// The size of the item in bytes
uint64 size = 2;
}

View File

@@ -750,8 +750,11 @@ impl FunctionDiffUi {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::BranchDest(addr) => {
DiffText::BranchDest(addr, diff) => {
label_text = format!("{addr:x}");
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::Symbol(sym) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
@@ -809,23 +812,35 @@ impl FunctionDiffUi {
fn reload(&mut self) -> Result<()> {
let prev = self.right_obj.take();
let config = diff::DiffObjConfig {
relax_reloc_diffs: self.relax_reloc_diffs,
space_between_args: true, // TODO
combine_data_sections: false, // TODO
x86_formatter: Default::default(), // TODO
mips_abi: Default::default(), // TODO
mips_instr_category: Default::default(), // TODO
arm_arch_version: Default::default(), // TODO
arm_unified_syntax: true, // TODO
arm_av_registers: false, // TODO
arm_r9_usage: Default::default(), // TODO
arm_sl_usage: false, // TODO
arm_fp_usage: false, // TODO
arm_ip_usage: false, // TODO
};
let target = self
.target_path
.as_deref()
.map(|p| obj::read::read(p).with_context(|| format!("Loading {}", p.display())))
.map(|p| {
obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))
})
.transpose()?;
let base = self
.base_path
.as_deref()
.map(|p| obj::read::read(p).with_context(|| format!("Loading {}", p.display())))
.map(|p| {
obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))
})
.transpose()?;
let config = diff::DiffObjConfig {
relax_reloc_diffs: self.relax_reloc_diffs,
space_between_args: true, // TODO
x86_formatter: Default::default(), // TODO
mips_abi: Default::default(), // TODO
mips_instr_category: Default::default(), // TODO
};
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), prev.as_ref())?;
let left_sym = target.as_ref().and_then(|o| find_function(o, &self.symbol_name));

View File

@@ -1,7 +1,8 @@
use std::{
collections::HashSet,
fs::File,
io::{BufReader, BufWriter, Write},
io::{BufWriter, Read, Write},
ops::DerefMut,
path::{Path, PathBuf},
time::Instant,
};
@@ -13,9 +14,15 @@ use objdiff_core::{
diff, obj,
obj::{ObjSectionKind, ObjSymbolFlags},
};
use prost::Message;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
use tracing::{info, warn};
use crate::util::report::{
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report, ReportItem,
ReportItemMetadata, ReportUnit, ReportUnitMetadata,
};
#[derive(FromArgs, PartialEq, Debug)]
/// Commands for processing NVIDIA Shield TV alf files.
#[argp(subcommand, name = "report")]
@@ -39,11 +46,14 @@ pub struct GenerateArgs {
/// Project directory
project: Option<PathBuf>,
#[argp(option, short = 'o')]
/// Output JSON file
/// Output file
output: Option<PathBuf>,
#[argp(switch, short = 'd')]
/// Deduplicate global and weak symbols (runs single-threaded)
deduplicate: bool,
#[argp(option, short = 'f')]
/// Output format (json or proto, default json)
format: Option<String>,
}
#[derive(FromArgs, PartialEq, Debug)]
@@ -51,65 +61,17 @@ pub struct GenerateArgs {
#[argp(subcommand, name = "changes")]
pub struct ChangesArgs {
#[argp(positional)]
/// Previous report JSON file
/// Previous report file
previous: PathBuf,
#[argp(positional)]
/// Current report JSON file
/// Current report file
current: PathBuf,
#[argp(option, short = 'o')]
/// Output JSON file
/// Output file
output: Option<PathBuf>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct Report {
fuzzy_match_percent: f32,
total_code: u64,
matched_code: u64,
matched_code_percent: f32,
total_data: u64,
matched_data: u64,
matched_data_percent: f32,
total_functions: u32,
matched_functions: u32,
matched_functions_percent: f32,
units: Vec<ReportUnit>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct ReportUnit {
name: String,
fuzzy_match_percent: f32,
total_code: u64,
matched_code: u64,
total_data: u64,
matched_data: u64,
total_functions: u32,
matched_functions: u32,
#[serde(skip_serializing_if = "Option::is_none")]
complete: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
module_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
module_id: Option<u32>,
sections: Vec<ReportItem>,
functions: Vec<ReportItem>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct ReportItem {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
demangled_name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_hex",
deserialize_with = "deserialize_hex"
)]
address: Option<u64>,
size: u64,
fuzzy_match_percent: f32,
#[argp(option, short = 'f')]
/// Output format (json or proto, default json)
format: Option<String>,
}
pub fn run(args: Args) -> Result<()> {
@@ -119,7 +81,28 @@ pub fn run(args: Args) -> Result<()> {
}
}
enum OutputFormat {
Json,
Proto,
}
impl OutputFormat {
fn from_str(s: &str) -> Result<Self> {
match s {
"json" => Ok(Self::Json),
"binpb" | "pb" | "proto" | "protobuf" => Ok(Self::Proto),
_ => bail!("Invalid output format: {}", s),
}
}
}
fn generate(args: GenerateArgs) -> Result<()> {
let output_format = if let Some(format) = &args.format {
OutputFormat::from_str(format)?
} else {
OutputFormat::Json
};
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
info!("Loading project {}", project_dir.display());
@@ -134,7 +117,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
);
let start = Instant::now();
let mut report = Report::default();
let mut units = vec![];
let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate {
// If deduplicating, we need to run single-threaded
@@ -146,11 +129,11 @@ fn generate(args: GenerateArgs) -> Result<()> {
project.base_dir.as_deref(),
Some(&mut existing_functions),
)? {
report.units.push(unit);
units.push(unit);
}
}
} else {
let units = project
let vec = project
.objects
.par_iter_mut()
.map(|object| {
@@ -163,51 +146,52 @@ fn generate(args: GenerateArgs) -> Result<()> {
)
})
.collect::<Result<Vec<Option<ReportUnit>>>>()?;
report.units = units.into_iter().flatten().collect();
units = vec.into_iter().flatten().collect();
}
for unit in &report.units {
report.fuzzy_match_percent += unit.fuzzy_match_percent * unit.total_code as f32;
report.total_code += unit.total_code;
report.matched_code += unit.matched_code;
report.total_data += unit.total_data;
report.matched_data += unit.matched_data;
report.total_functions += unit.total_functions;
report.matched_functions += unit.matched_functions;
}
if report.total_code == 0 {
report.fuzzy_match_percent = 100.0;
} else {
report.fuzzy_match_percent /= report.total_code as f32;
}
report.matched_code_percent = if report.total_code == 0 {
100.0
} else {
report.matched_code as f32 / report.total_code as f32 * 100.0
};
report.matched_data_percent = if report.total_data == 0 {
100.0
} else {
report.matched_data as f32 / report.total_data as f32 * 100.0
};
report.matched_functions_percent = if report.total_functions == 0 {
100.0
} else {
report.matched_functions as f32 / report.total_functions as f32 * 100.0
};
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
let report = Report { measures: Some(measures), units };
let duration = start.elapsed();
info!("Report generated in {}.{:03}s", duration.as_secs(), duration.subsec_millis());
if let Some(output) = &args.output {
write_output(&report, args.output.as_deref(), output_format)?;
Ok(())
}
fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
where T: serde::Serialize + prost::Message {
if let Some(output) = output {
info!("Writing to {}", output.display());
let mut output = BufWriter::new(
File::create(output)
.with_context(|| format!("Failed to create file {}", output.display()))?,
);
serde_json::to_writer_pretty(&mut output, &report)?;
output.flush()?;
let file = File::options()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(output)
.with_context(|| format!("Failed to create file {}", output.display()))?;
match format {
OutputFormat::Json => {
let mut output = BufWriter::new(file);
serde_json::to_writer_pretty(&mut output, input)
.context("Failed to write output file")?;
output.flush().context("Failed to flush output file")?;
}
OutputFormat::Proto => {
file.set_len(input.encoded_len() as u64)?;
let map =
unsafe { memmap2::Mmap::map(&file) }.context("Failed to map output file")?;
let mut output = map.make_mut().context("Failed to remap output file")?;
input.encode(&mut output.deref_mut()).context("Failed to encode output")?;
}
}
} else {
serde_json::to_writer_pretty(std::io::stdout(), &report)?;
}
match format {
OutputFormat::Json => {
serde_json::to_writer_pretty(std::io::stdout(), input)?;
}
OutputFormat::Proto => {
std::io::stdout().write_all(&input.encode_to_vec())?;
}
}
};
Ok(())
}
@@ -230,30 +214,37 @@ fn report_object(
}
_ => {}
}
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
let target = object
.target_path
.as_ref()
.map(|p| obj::read::read(p).with_context(|| format!("Failed to open {}", p.display())))
.map(|p| {
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
})
.transpose()?;
let base = object
.base_path
.as_ref()
.map(|p| obj::read::read(p).with_context(|| format!("Failed to open {}", p.display())))
.map(|p| {
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
})
.transpose()?;
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
let mut unit = ReportUnit {
name: object.name().to_string(),
let metadata = ReportUnitMetadata {
complete: object.complete,
module_name: target
.as_ref()
.and_then(|o| o.split_meta.as_ref())
.and_then(|m| m.module_name.clone()),
module_id: target.as_ref().and_then(|o| o.split_meta.as_ref()).and_then(|m| m.module_id),
..Default::default()
source_path: None, // TODO
};
let obj = target.as_ref().or(base.as_ref()).unwrap();
let mut measures = Measures::default();
let mut sections = vec![];
let mut functions = vec![];
let obj = target.as_ref().or(base.as_ref()).unwrap();
let obj_diff = result.left.as_ref().or(result.right.as_ref()).unwrap();
for (section, section_diff) in obj.sections.iter().zip(&obj_diff.sections) {
let section_match_percent = section_diff.match_percent.unwrap_or_else(|| {
@@ -265,19 +256,21 @@ fn report_object(
0.0
}
});
unit.sections.push(ReportItem {
sections.push(ReportItem {
name: section.name.clone(),
demangled_name: None,
fuzzy_match_percent: section_match_percent,
size: section.size,
address: section.virtual_address,
metadata: Some(ReportItemMetadata {
demangled_name: None,
virtual_address: section.virtual_address,
}),
});
match section.kind {
ObjSectionKind::Data | ObjSectionKind::Bss => {
unit.total_data += section.size;
measures.total_data += section.size;
if section_match_percent == 100.0 {
unit.matched_data += section.size;
measures.matched_data += section.size;
}
continue;
}
@@ -305,119 +298,35 @@ fn report_object(
0.0
}
});
unit.fuzzy_match_percent += match_percent * symbol.size as f32;
unit.total_code += symbol.size;
measures.fuzzy_match_percent += match_percent * symbol.size as f32;
measures.total_code += symbol.size;
if match_percent == 100.0 {
unit.matched_code += symbol.size;
measures.matched_code += symbol.size;
}
unit.functions.push(ReportItem {
functions.push(ReportItem {
name: symbol.name.clone(),
demangled_name: symbol.demangled_name.clone(),
size: symbol.size,
fuzzy_match_percent: match_percent,
address: symbol.virtual_address,
metadata: Some(ReportItemMetadata {
demangled_name: symbol.demangled_name.clone(),
virtual_address: symbol.virtual_address,
}),
});
if match_percent == 100.0 {
unit.matched_functions += 1;
measures.matched_functions += 1;
}
unit.total_functions += 1;
measures.total_functions += 1;
}
}
if unit.total_code == 0 {
unit.fuzzy_match_percent = 100.0;
} else {
unit.fuzzy_match_percent /= unit.total_code as f32;
}
Ok(Some(unit))
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct Changes {
from: ChangeInfo,
to: ChangeInfo,
units: Vec<ChangeUnit>,
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
struct ChangeInfo {
fuzzy_match_percent: f32,
total_code: u64,
matched_code: u64,
matched_code_percent: f32,
total_data: u64,
matched_data: u64,
matched_data_percent: f32,
total_functions: u32,
matched_functions: u32,
matched_functions_percent: f32,
}
impl From<&Report> for ChangeInfo {
fn from(report: &Report) -> Self {
Self {
fuzzy_match_percent: report.fuzzy_match_percent,
total_code: report.total_code,
matched_code: report.matched_code,
matched_code_percent: report.matched_code_percent,
total_data: report.total_data,
matched_data: report.matched_data,
matched_data_percent: report.matched_data_percent,
total_functions: report.total_functions,
matched_functions: report.matched_functions,
matched_functions_percent: report.matched_functions_percent,
}
}
}
impl From<&ReportUnit> for ChangeInfo {
fn from(value: &ReportUnit) -> Self {
Self {
fuzzy_match_percent: value.fuzzy_match_percent,
total_code: value.total_code,
matched_code: value.matched_code,
matched_code_percent: if value.total_code == 0 {
100.0
} else {
value.matched_code as f32 / value.total_code as f32 * 100.0
},
total_data: value.total_data,
matched_data: value.matched_data,
matched_data_percent: if value.total_data == 0 {
100.0
} else {
value.matched_data as f32 / value.total_data as f32 * 100.0
},
total_functions: value.total_functions,
matched_functions: value.matched_functions,
matched_functions_percent: if value.total_functions == 0 {
100.0
} else {
value.matched_functions as f32 / value.total_functions as f32 * 100.0
},
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct ChangeUnit {
name: String,
from: Option<ChangeInfo>,
to: Option<ChangeInfo>,
sections: Vec<ChangeItem>,
functions: Vec<ChangeItem>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct ChangeItem {
name: String,
from: Option<ChangeItemInfo>,
to: Option<ChangeItemInfo>,
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
struct ChangeItemInfo {
fuzzy_match_percent: f32,
size: u64,
measures.calc_fuzzy_match_percent();
measures.calc_matched_percent();
Ok(Some(ReportUnit {
name: object.name().to_string(),
measures: Some(measures),
sections,
functions,
metadata: Some(metadata),
}))
}
impl From<&ReportItem> for ChangeItemInfo {
@@ -427,27 +336,42 @@ impl From<&ReportItem> for ChangeItemInfo {
}
fn changes(args: ChangesArgs) -> Result<()> {
let previous = read_report(&args.previous)?;
let current = read_report(&args.current)?;
let mut changes = Changes {
from: ChangeInfo::from(&previous),
to: ChangeInfo::from(&current),
units: vec![],
let output_format = if let Some(format) = &args.format {
OutputFormat::from_str(format)?
} else {
OutputFormat::Json
};
let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") {
// Special case for comparing two reports from stdin
let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?;
let input = ChangesInput::decode(data.as_slice())?;
(input.from.unwrap(), input.to.unwrap())
} else {
let previous = read_report(&args.previous)?;
let current = read_report(&args.current)?;
(previous, current)
};
let mut changes = Changes { from: previous.measures, to: current.measures, units: vec![] };
for prev_unit in &previous.units {
let curr_unit = current.units.iter().find(|u| u.name == prev_unit.name);
let sections = process_items(prev_unit, curr_unit, |u| &u.sections);
let functions = process_items(prev_unit, curr_unit, |u| &u.functions);
let prev_unit_info = ChangeInfo::from(prev_unit);
let curr_unit_info = curr_unit.map(ChangeInfo::from);
if !functions.is_empty() || !matches!(&curr_unit_info, Some(v) if v == &prev_unit_info) {
let prev_measures = prev_unit.measures;
let curr_measures = curr_unit.and_then(|u| u.measures);
if !functions.is_empty() || prev_measures != curr_measures {
changes.units.push(ChangeUnit {
name: prev_unit.name.clone(),
from: Some(prev_unit_info),
to: curr_unit_info,
from: prev_measures,
to: curr_measures,
sections,
functions,
metadata: curr_unit
.as_ref()
.and_then(|u| u.metadata.clone())
.or_else(|| prev_unit.metadata.clone()),
});
}
}
@@ -456,23 +380,14 @@ fn changes(args: ChangesArgs) -> Result<()> {
changes.units.push(ChangeUnit {
name: curr_unit.name.clone(),
from: None,
to: Some(ChangeInfo::from(curr_unit)),
to: curr_unit.measures,
sections: process_new_items(&curr_unit.sections),
functions: process_new_items(&curr_unit.functions),
metadata: curr_unit.metadata.clone(),
});
}
}
if let Some(output) = &args.output {
info!("Writing to {}", output.display());
let mut output = BufWriter::new(
File::create(output)
.with_context(|| format!("Failed to create file {}", output.display()))?,
);
serde_json::to_writer_pretty(&mut output, &changes)?;
output.flush()?;
} else {
serde_json::to_writer_pretty(std::io::stdout(), &changes)?;
}
write_output(&changes, args.output.as_deref(), output_format)?;
Ok(())
}
@@ -495,6 +410,7 @@ fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
name: prev_func.name.clone(),
from: Some(prev_func_info),
to: Some(curr_func_info),
metadata: curr_func.as_ref().unwrap().metadata.clone(),
});
}
} else {
@@ -502,6 +418,7 @@ fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
name: prev_func.name.clone(),
from: Some(prev_func_info),
to: None,
metadata: prev_func.metadata.clone(),
});
}
}
@@ -511,6 +428,7 @@ fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
name: curr_func.name.clone(),
from: None,
to: Some(ChangeItemInfo::from(curr_func)),
metadata: curr_func.metadata.clone(),
});
}
}
@@ -520,6 +438,7 @@ fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
name: prev_func.name.clone(),
from: Some(ChangeItemInfo::from(prev_func)),
to: None,
metadata: prev_func.metadata.clone(),
});
}
}
@@ -529,35 +448,24 @@ fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
items
.iter()
.map(|f| ChangeItem { name: f.name.clone(), from: None, to: Some(ChangeItemInfo::from(f)) })
.map(|item| ChangeItem {
name: item.name.clone(),
from: None,
to: Some(ChangeItemInfo::from(item)),
metadata: item.metadata.clone(),
})
.collect()
}
fn read_report(path: &Path) -> Result<Report> {
serde_json::from_reader(BufReader::new(
File::open(path).with_context(|| format!("Failed to open {}", path.display()))?,
))
.with_context(|| format!("Failed to read report {}", path.display()))
}
fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
if let Some(x) = x {
s.serialize_str(&format!("{:#x}", x))
} else {
s.serialize_none()
}
}
fn deserialize_hex<'de, D>(d: D) -> Result<Option<u64>, D::Error>
where D: serde::Deserializer<'de> {
use serde::Deserialize;
let s = String::deserialize(d)?;
if s.is_empty() {
Ok(None)
} else if !s.starts_with("0x") {
Err(serde::de::Error::custom("expected hex string"))
} else {
u64::from_str_radix(&s[2..], 16).map(Some).map_err(serde::de::Error::custom)
if path == Path::new("-") {
let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?;
return Report::parse(&data).with_context(|| "Failed to load report from stdin");
}
let file = File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
let mmap = unsafe { memmap2::Mmap::map(&file) }
.with_context(|| format!("Failed to map {}", path.display()))?;
Report::parse(mmap.as_ref())
.with_context(|| format!("Failed to load report {}", path.display()))
}

View File

@@ -96,7 +96,7 @@ fn main() {
// Try to enable ANSI support on Windows.
let _ = enable_ansi_support();
// Disable isatty check for supports-color. (e.g. when used with ninja)
env::set_var("IGNORE_IS_TERMINAL", "1");
unsafe { env::set_var("IGNORE_IS_TERMINAL", "1") };
supports_color::on(Stream::Stdout).is_some_and(|c| c.has_basic)
};

View File

@@ -1 +1,2 @@
pub mod report;
pub mod term;

View File

@@ -0,0 +1,226 @@
use anyhow::{bail, Result};
use prost::Message;
use serde_json::error::Category;
// Protobuf report types
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
impl Report {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.is_empty() {
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
}
if data[0] == b'{' {
// Load as JSON
Self::from_json(data).map_err(anyhow::Error::new)
} else {
// Load as binary protobuf
Self::decode(data).map_err(anyhow::Error::new)
}
}
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
match serde_json::from_slice::<Self>(bytes) {
Ok(report) => Ok(report),
Err(e) => {
match e.classify() {
Category::Io | Category::Eof | Category::Syntax => Err(e),
Category::Data => {
// Try to load as legacy report
match serde_json::from_slice::<LegacyReport>(bytes) {
Ok(legacy_report) => Ok(Report::from(legacy_report)),
Err(_) => Err(e),
}
}
}
}
}
}
}
impl Measures {
/// Average the fuzzy match percentage over total code bytes.
pub fn calc_fuzzy_match_percent(&mut self) {
if self.total_code == 0 {
self.fuzzy_match_percent = 100.0;
} else {
self.fuzzy_match_percent /= self.total_code as f32;
}
}
/// Calculate the percentage of matched code, data, and functions.
pub fn calc_matched_percent(&mut self) {
self.matched_code_percent = if self.total_code == 0 {
100.0
} else {
self.matched_code as f32 / self.total_code as f32 * 100.0
};
self.matched_data_percent = if self.total_data == 0 {
100.0
} else {
self.matched_data as f32 / self.total_data as f32 * 100.0
};
self.matched_functions_percent = if self.total_functions == 0 {
100.0
} else {
self.matched_functions as f32 / self.total_functions as f32 * 100.0
};
}
}
/// Allows [collect](Iterator::collect) to be used on an iterator of [Measures].
impl FromIterator<Measures> for Measures {
fn from_iter<T>(iter: T) -> Self
where T: IntoIterator<Item = Measures> {
let mut measures = Measures::default();
for other in iter {
measures.fuzzy_match_percent += other.fuzzy_match_percent * other.total_code as f32;
measures.total_code += other.total_code;
measures.matched_code += other.matched_code;
measures.total_data += other.total_data;
measures.matched_data += other.matched_data;
measures.total_functions += other.total_functions;
measures.matched_functions += other.matched_functions;
}
measures.calc_fuzzy_match_percent();
measures.calc_matched_percent();
measures
}
}
// Older JSON report types
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct LegacyReport {
fuzzy_match_percent: f32,
total_code: u64,
matched_code: u64,
matched_code_percent: f32,
total_data: u64,
matched_data: u64,
matched_data_percent: f32,
total_functions: u32,
matched_functions: u32,
matched_functions_percent: f32,
units: Vec<LegacyReportUnit>,
}
impl From<LegacyReport> for Report {
fn from(value: LegacyReport) -> Self {
Self {
measures: Some(Measures {
fuzzy_match_percent: value.fuzzy_match_percent,
total_code: value.total_code,
matched_code: value.matched_code,
matched_code_percent: value.matched_code_percent,
total_data: value.total_data,
matched_data: value.matched_data,
matched_data_percent: value.matched_data_percent,
total_functions: value.total_functions,
matched_functions: value.matched_functions,
matched_functions_percent: value.matched_functions_percent,
}),
units: value.units.into_iter().map(ReportUnit::from).collect(),
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct LegacyReportUnit {
name: String,
fuzzy_match_percent: f32,
total_code: u64,
matched_code: u64,
total_data: u64,
matched_data: u64,
total_functions: u32,
matched_functions: u32,
#[serde(skip_serializing_if = "Option::is_none")]
complete: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
module_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
module_id: Option<u32>,
sections: Vec<LegacyReportItem>,
functions: Vec<LegacyReportItem>,
}
impl From<LegacyReportUnit> for ReportUnit {
fn from(value: LegacyReportUnit) -> Self {
let mut measures = Measures {
fuzzy_match_percent: value.fuzzy_match_percent,
total_code: value.total_code,
matched_code: value.matched_code,
total_data: value.total_data,
matched_data: value.matched_data,
total_functions: value.total_functions,
matched_functions: value.matched_functions,
..Default::default()
};
measures.calc_matched_percent();
Self {
name: value.name.clone(),
measures: Some(measures),
sections: value.sections.into_iter().map(ReportItem::from).collect(),
functions: value.functions.into_iter().map(ReportItem::from).collect(),
metadata: Some(ReportUnitMetadata {
complete: value.complete,
module_name: value.module_name.clone(),
module_id: value.module_id,
..Default::default()
}),
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct LegacyReportItem {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
demangled_name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_hex",
deserialize_with = "deserialize_hex"
)]
address: Option<u64>,
size: u64,
fuzzy_match_percent: f32,
}
impl From<LegacyReportItem> for ReportItem {
fn from(value: LegacyReportItem) -> Self {
Self {
name: value.name,
size: value.size,
fuzzy_match_percent: value.fuzzy_match_percent,
metadata: Some(ReportItemMetadata {
demangled_name: value.demangled_name,
virtual_address: value.address,
}),
}
}
}
fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
if let Some(x) = x {
s.serialize_str(&format!("{:#x}", x))
} else {
s.serialize_none()
}
}
fn deserialize_hex<'de, D>(d: D) -> Result<Option<u64>, D::Error>
where D: serde::Deserializer<'de> {
use serde::Deserialize;
let s = String::deserialize(d)?;
if s.is_empty() {
Ok(None)
} else if !s.starts_with("0x") {
Err(serde::de::Error::custom("expected hex string"))
} else {
u64::from_str_radix(&s[2..], 16).map(Some).map_err(serde::de::Error::custom)
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "objdiff-core"
version = "2.0.0-alpha.4"
version = "2.0.0-beta.4"
edition = "2021"
rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"]
@@ -17,9 +17,9 @@ any-arch = [] # Implicit, used to check if any arch is enabled
config = ["globset", "semver", "serde_json", "serde_yaml"]
dwarf = ["gimli"]
mips = ["any-arch", "rabbitizer"]
ppc = ["any-arch", "cwdemangle", "ppc750cl"]
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"]
arm = ["any-arch", "cpp_demangle", "unarm"]
arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
[dependencies]
anyhow = "1.0.82"
@@ -45,6 +45,7 @@ gimli = { version = "0.29.0", default-features = false, features = ["read-all"],
# ppc
cwdemangle = { version = "1.0.0", optional = true }
cwextab = { version = "0.2.3", optional = true }
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "6cbd7d888c7082c2c860f66cbb9848d633f753ed", optional = true }
# mips
@@ -56,4 +57,5 @@ iced-x86 = { version = "1.21.0", default-features = false, features = ["std", "d
msvc-demangler = { version = "0.10.0", optional = true }
# arm
unarm = { version = "1.0.0", optional = true }
unarm = { version = "1.4.0", optional = true }
arm-attr = { version = "0.1.1", optional = true }

View File

@@ -4,53 +4,107 @@ use std::{
};
use anyhow::{bail, Result};
use arm_attr::{enums::CpuArch, tag::Tag, BuildAttrs};
use object::{
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, SectionIndex,
SectionKind, Symbol,
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},
ParsedIns,
DisplayOptions, ParseFlags, ParsedIns, RegNames,
};
use crate::{
arch::{ObjArch, ProcessCodeResult},
diff::DiffObjConfig,
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig},
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
};
pub struct ObjArchArm {
/// Maps section index, to list of disasm modes (arm, thumb or data) sorted by address
disasm_modes: HashMap<SectionIndex, Vec<DisasmMode>>,
detected_version: Option<ArmVersion>,
endianness: object::Endianness,
}
impl ObjArchArm {
pub fn new(file: &File) -> Result<Self> {
let endianness = file.endianness();
match file {
File::Elf32(_) => {
let disasm_modes: HashMap<_, _> = file
.sections()
.filter(|s| s.kind() == SectionKind::Text)
.map(|s| {
let index = s.index();
let mut mapping_symbols: Vec<_> = file
.symbols()
.filter(|s| s.section_index().map(|i| i == index).unwrap_or(false))
.filter_map(|s| DisasmMode::from_symbol(&s))
.collect();
mapping_symbols.sort_unstable_by_key(|x| x.address);
(s.index(), mapping_symbols)
})
.collect();
Ok(Self { disasm_modes })
let disasm_modes = Self::elf_get_mapping_symbols(file);
let detected_version = Self::elf_detect_arm_version(file)?;
Ok(Self { disasm_modes, detected_version, endianness })
}
_ => bail!("Unsupported file format {:?}", file.format()),
}
}
fn elf_detect_arm_version(file: &File) -> Result<Option<ArmVersion>> {
// Check ARM attributes
if let Some(arm_attrs) = file.sections().find(|s| {
s.kind() == SectionKind::Elf(SHT_ARM_ATTRIBUTES) && s.name() == Ok(".ARM.attributes")
}) {
let attr_data = arm_attrs.uncompressed_data()?;
let build_attrs = BuildAttrs::new(&attr_data, match file.endianness() {
object::Endianness::Little => arm_attr::Endian::Little,
object::Endianness::Big => arm_attr::Endian::Big,
})?;
for subsection in build_attrs.subsections() {
let subsection = subsection?;
if !subsection.is_aeabi() {
continue;
}
// Only checking first CpuArch tag. Others may exist, but that's very unlikely.
let cpu_arch = subsection.into_public_tag_iter()?.find_map(|(_, tag)| {
if let Tag::CpuArch(cpu_arch) = tag {
Some(cpu_arch)
} else {
None
}
});
match cpu_arch {
Some(CpuArch::V4T) => return Ok(Some(ArmVersion::V4T)),
Some(CpuArch::V5TE) => return Ok(Some(ArmVersion::V5Te)),
Some(CpuArch::V6K) => return Ok(Some(ArmVersion::V6K)),
Some(arch) => bail!("ARM arch {} not supported", arch),
None => {}
};
}
}
Ok(None)
}
fn elf_get_mapping_symbols(file: &File) -> HashMap<SectionIndex, Vec<DisasmMode>> {
file.sections()
.filter(|s| s.kind() == SectionKind::Text)
.map(|s| {
let index = s.index();
let mut mapping_symbols: Vec<_> = file
.symbols()
.filter(|s| s.section_index().map(|i| i == index).unwrap_or(false))
.filter_map(|s| DisasmMode::from_symbol(&s))
.collect();
mapping_symbols.sort_unstable_by_key(|x| x.address);
(s.index(), mapping_symbols)
})
.collect()
}
}
impl ObjArch for ObjArchArm {
fn symbol_address(&self, symbol: &Symbol) -> u64 {
let address = symbol.address();
if symbol.kind() == SymbolKind::Text {
address & !1
} else {
address
}
}
fn process_code(
&self,
address: u64,
@@ -81,10 +135,38 @@ impl ObjArch for ObjArchArm {
mapping_symbols.iter().skip(first_mapping_idx + 1).take_while(|x| x.address < end_addr);
let mut next_mapping = mappings_iter.next();
let ins_count = code.len() / first_mapping.instruction_size();
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 mut parser = Parser::new(ArmVersion::V5Te, first_mapping, start_addr, code);
let version = match config.arm_arch_version {
ArmArchVersion::Auto => self.detected_version.unwrap_or(ArmVersion::V5Te),
ArmArchVersion::V4T => ArmVersion::V4T,
ArmArchVersion::V5TE => ArmVersion::V5Te,
ArmArchVersion::V6K => ArmVersion::V6K,
};
let endian = match self.endianness {
object::Endianness::Little => unarm::Endian::Little,
object::Endianness::Big => unarm::Endian::Big,
};
let parse_flags = ParseFlags { ual: config.arm_unified_syntax };
let mut parser = Parser::new(version, first_mapping, start_addr, endian, parse_flags, code);
let display_options = DisplayOptions {
reg_names: RegNames {
av_registers: config.arm_av_registers,
r9_use: match config.arm_r9_usage {
ArmR9Usage::GeneralPurpose => unarm::R9Use::GeneralPurpose,
ArmR9Usage::Sb => unarm::R9Use::Pid,
ArmR9Usage::Tr => unarm::R9Use::Tls,
},
explicit_stack_limit: config.arm_sl_usage,
frame_pointer: config.arm_fp_usage,
ip: config.arm_ip_usage,
},
};
while let Some((address, op, ins)) = parser.next() {
if let Some(next) = next_mapping {
@@ -95,7 +177,6 @@ impl ObjArch for ObjArchArm {
next_mapping = mappings_iter.next();
}
}
let line = line_info.range(..=address as u64).last().map(|(_, &b)| b);
let reloc = relocations.iter().find(|r| (r.address as u32 & !1) == address).cloned();
@@ -103,11 +184,19 @@ impl ObjArch for ObjArchArm {
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_PC24 } => {
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
| RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
reloc_arg =
ins.args.iter().rposition(|a| matches!(a, Argument::BranchDest(_)));
}
// Data
RelocationFlags::Elf { r_type: elf::R_ARM_ABS32 } => {
reloc_arg = ins.args.iter().rposition(|a| matches!(a, Argument::UImm(_)));
}
_ => (),
}
};
@@ -115,7 +204,7 @@ impl ObjArch for ObjArchArm {
let (args, branch_dest) = if reloc.is_some() && parser.mode == ParseMode::Data {
(vec![ObjInsArg::Reloc], None)
} else {
push_args(&ins, config, reloc_arg, address)?
push_args(&ins, config, reloc_arg, address, display_options)?
};
ops.push(op.id());
@@ -128,7 +217,7 @@ impl ObjArch for ObjArchArm {
reloc,
branch_dest,
line,
formatted: ins.to_string(),
formatted: ins.display(display_options).to_string(),
orig: None,
});
}
@@ -138,11 +227,43 @@ impl ObjArch for ObjArchArm {
fn implcit_addend(
&self,
_section: &ObjSection,
_file: &File<'_>,
section: &ObjSection,
address: u64,
reloc: &Relocation,
) -> anyhow::Result<i64> {
bail!("Unsupported ARM implicit relocation {:#x}{:?}", address, reloc.flags())
let address = address as usize;
Ok(match reloc.flags() {
// ARM calls
RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
let data = section.data[address..address + 4].try_into()?;
let addend = self.endianness.read_i32_bytes(data);
let imm24 = addend & 0xffffff;
(imm24 << 2) << 8 >> 8
}
// Thumb calls
RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_XPC22 } => {
let data = section.data[address..address + 2].try_into()?;
let high = self.endianness.read_i16_bytes(data) as i32;
let data = section.data[address + 2..address + 4].try_into()?;
let low = self.endianness.read_i16_bytes(data) as i32;
let imm22 = ((high & 0x7ff) << 11) | (low & 0x7ff);
(imm22 << 1) << 9 >> 9
}
// Data
RelocationFlags::Elf { r_type: elf::R_ARM_ABS32 } => {
let data = section.data[address..address + 4].try_into()?;
self.endianness.read_i32_bytes(data)
}
flags => bail!("Unsupported ARM implicit relocation {flags:?}"),
} as i64)
}
fn demangle(&self, name: &str) -> Option<String> {
@@ -178,6 +299,7 @@ fn push_args(
config: &DiffObjConfig,
reloc_arg: Option<usize>,
cur_addr: u32,
display_options: DisplayOptions,
) -> Result<(Vec<ObjInsArg>, Option<u64>)> {
let mut args = vec![];
let mut branch_dest = None;
@@ -209,12 +331,15 @@ fn push_args(
args.push(ObjInsArg::Reloc);
} else {
match arg {
Argument::None => {}
Argument::Reg(reg) => {
if reg.deref {
deref = true;
args.push(ObjInsArg::PlainText("[".into()));
}
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(reg.reg.to_string().into())));
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
reg.reg.display(display_options.reg_names).to_string().into(),
)));
if reg.writeback {
if reg.deref {
writeback = true;
@@ -232,7 +357,10 @@ fn push_args(
args.push(ObjInsArg::PlainText(config.separator().into()));
}
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
Register::parse(i).to_string().into(),
Register::parse(i)
.display(display_options.reg_names)
.to_string()
.into(),
)));
first = false;
}
@@ -242,7 +370,7 @@ fn push_args(
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("^".to_string().into())));
}
}
Argument::UImm(value) | Argument::CoOpcode(value) => {
Argument::UImm(value) | Argument::CoOpcode(value) | Argument::SatImm(value) => {
args.push(ObjInsArg::PlainText("#".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
}
@@ -272,17 +400,33 @@ fn push_args(
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.to_string().into())));
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
shift.reg.display(display_options.reg_names).to_string().into(),
)));
}
Argument::OffsetReg(offset) => {
if !offset.add {
args.push(ObjInsArg::PlainText("-".into()));
}
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
offset.reg.to_string().into(),
offset.reg.display(display_options.reg_names).to_string().into(),
)));
}
_ => args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(arg.to_string().into()))),
Argument::CpsrMode(mode) => {
args.push(ObjInsArg::PlainText("#".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(mode.mode as u64)));
if mode.writeback {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
}
}
Argument::CoReg(_)
| Argument::StatusReg(_)
| Argument::StatusMask(_)
| Argument::Shift(_)
| Argument::CpsrFlags(_)
| Argument::Endian(_) => args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
arg.display(display_options).to_string().into(),
))),
}
}
}

View File

@@ -1,7 +1,10 @@
use std::{borrow::Cow, collections::BTreeMap, sync::Mutex};
use anyhow::{anyhow, bail, Result};
use object::{elf, Endian, Endianness, File, FileFlags, Object, Relocation, RelocationFlags};
use object::{
elf, Endian, Endianness, File, FileFlags, Object, ObjectSection, ObjectSymbol, Relocation,
RelocationFlags, RelocationTarget,
};
use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType};
use crate::{
@@ -22,13 +25,16 @@ pub struct ObjArchMips {
pub endianness: Endianness,
pub abi: Abi,
pub instr_category: InstrCategory,
pub ri_gp_value: i32,
}
const EF_MIPS_ABI: u32 = 0x0000F000;
const EF_MIPS_MACH: u32 = 0x00FF0000;
const E_MIPS_MACH_ALLEGREX: u32 = 0x00840000;
const E_MIPS_MACH_5900: u32 = 0x00920000;
const EF_MIPS_MACH_ALLEGREX: u32 = 0x00840000;
const EF_MIPS_MACH_5900: u32 = 0x00920000;
const R_MIPS15_S3: u32 = 119;
impl ObjArchMips {
pub fn new(object: &File) -> Result<Self> {
@@ -38,19 +44,37 @@ impl ObjArchMips {
FileFlags::None => {}
FileFlags::Elf { e_flags, .. } => {
abi = match e_flags & EF_MIPS_ABI {
elf::EF_MIPS_ABI_O32 => Abi::O32,
elf::EF_MIPS_ABI_O32 | elf::EF_MIPS_ABI_O64 => Abi::O32,
elf::EF_MIPS_ABI_EABI32 | elf::EF_MIPS_ABI_EABI64 => Abi::N32,
_ => Abi::NUMERIC,
_ => {
if e_flags & elf::EF_MIPS_ABI2 != 0 {
Abi::N32
} else {
Abi::NUMERIC
}
}
};
instr_category = match e_flags & EF_MIPS_MACH {
E_MIPS_MACH_ALLEGREX => InstrCategory::R4000ALLEGREX,
E_MIPS_MACH_5900 => InstrCategory::R5900,
EF_MIPS_MACH_ALLEGREX => InstrCategory::R4000ALLEGREX,
EF_MIPS_MACH_5900 => InstrCategory::R5900,
_ => InstrCategory::CPU,
};
}
_ => bail!("Unsupported MIPS file flags"),
}
Ok(Self { endianness: object.endianness(), abi, instr_category })
// 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 ri_gp_value = object
.section_by_name(".reginfo")
.and_then(|section| section.data().ok())
.and_then(|data| data.get(0x14..0x18))
.and_then(|s| s.try_into().ok())
.map(|bytes| object.endianness().read_i32_bytes(bytes))
.unwrap_or(0);
Ok(Self { endianness: object.endianness(), abi, instr_category, ri_gp_value })
}
}
@@ -98,7 +122,7 @@ impl ObjArch for ObjArchMips {
let mnemonic = instruction.opcode_name().to_string();
let is_branch = instruction.is_branch();
let branch_offset = instruction.branch_offset();
let branch_dest = if is_branch {
let mut branch_dest = if is_branch {
cur_addr.checked_add_signed(branch_offset).map(|a| a as u64)
} else {
None
@@ -115,9 +139,7 @@ impl ObjArch for ObjArchMips {
OperandType::cpu_immediate
| OperandType::cpu_label
| OperandType::cpu_branch_target_label => {
if let Some(branch_dest) = branch_dest {
args.push(ObjInsArg::BranchDest(branch_dest));
} else if let Some(reloc) = reloc {
if let Some(reloc) = reloc {
if matches!(&reloc.target_section, Some(s) if s == ".text")
&& reloc.target.address > start_address
&& reloc.target.address < end_address
@@ -125,7 +147,10 @@ impl ObjArch for ObjArchMips {
args.push(ObjInsArg::BranchDest(reloc.target.address));
} else {
push_reloc(&mut args, reloc)?;
branch_dest = None;
}
} else if let Some(branch_dest) = branch_dest {
args.push(ObjInsArg::BranchDest(branch_dest));
} else {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
op.disassemble(&instruction, None).into(),
@@ -146,6 +171,18 @@ impl ObjArch for ObjArchMips {
)));
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(),
@@ -173,6 +210,7 @@ impl ObjArch for ObjArchMips {
fn implcit_addend(
&self,
file: &File<'_>,
section: &ObjSection,
address: u64,
reloc: &Relocation,
@@ -181,14 +219,29 @@ impl ObjArch for ObjArchMips {
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 | elf::R_MIPS_GPREL16,
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_26 } => ((addend & 0x03FFFFFF) << 2) 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:?}"),
})
}
@@ -196,13 +249,16 @@ impl ObjArch for ObjArchMips {
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_MIPS_HI16 => Cow::Borrowed("R_MIPS_HI16"),
elf::R_MIPS_LO16 => Cow::Borrowed("R_MIPS_LO16"),
elf::R_MIPS_GOT16 => Cow::Borrowed("R_MIPS_GOT16"),
elf::R_MIPS_CALL16 => Cow::Borrowed("R_MIPS_CALL16"),
elf::R_MIPS_GPREL16 => Cow::Borrowed("R_MIPS_GPREL16"),
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:?}>")),
},
_ => Cow::Owned(format!("<{flags:?}>")),
@@ -238,7 +294,11 @@ fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_32 | elf::R_MIPS_26 => {
elf::R_MIPS_32
| elf::R_MIPS_26
| elf::R_MIPS_LITERAL
| elf::R_MIPS_PC16
| R_MIPS15_S3 => {
args.push(ObjInsArg::Reloc);
}
_ => bail!("Unsupported ELF MIPS relocation type {r_type}"),

View File

@@ -1,7 +1,7 @@
use std::{borrow::Cow, collections::BTreeMap};
use anyhow::{bail, Result};
use object::{Architecture, Object, Relocation, RelocationFlags};
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
use crate::{
diff::DiffObjConfig,
@@ -28,12 +28,19 @@ pub trait ObjArch: Send + Sync {
config: &DiffObjConfig,
) -> Result<ProcessCodeResult>;
fn implcit_addend(&self, section: &ObjSection, address: u64, reloc: &Relocation)
-> Result<i64>;
fn implcit_addend(
&self,
file: &File<'_>,
section: &ObjSection,
address: u64,
reloc: &Relocation,
) -> Result<i64>;
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() }
}
pub struct ProcessCodeResult {

View File

@@ -150,6 +150,7 @@ impl ObjArch for ObjArchPpc {
fn implcit_addend(
&self,
_file: &File<'_>,
_section: &ObjSection,
address: u64,
reloc: &Relocation,

View File

@@ -128,6 +128,7 @@ impl ObjArch for ObjArchX86 {
fn implcit_addend(
&self,
_file: &File<'_>,
section: &ObjSection,
address: u64,
reloc: &Relocation,

View File

@@ -11,27 +11,6 @@ use crate::{
obj::{ObjInfo, ObjSection, SymbolRef},
};
/// Compare the addresses and sizes of each symbol in the BSS sections.
pub fn diff_bss_section(
left: &ObjSection,
right: &ObjSection,
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
let deadline = Instant::now() + Duration::from_secs(5);
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,
Some(deadline),
);
let match_percent = get_diff_ratio(&ops, left_sizes.len(), right_sizes.len()) * 100.0;
Ok((
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
))
}
pub fn diff_bss_symbol(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
@@ -65,10 +44,14 @@ pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
pub fn diff_data_section(
left: &ObjSection,
right: &ObjSection,
left_section_diff: &ObjSectionDiff,
right_section_diff: &ObjSectionDiff,
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
let deadline = Instant::now() + Duration::from_secs(5);
let left_max = left.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0);
let right_max = right.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0);
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 =
@@ -143,18 +126,18 @@ pub fn diff_data_section(
}
}
Ok((
ObjSectionDiff {
symbols: vec![],
data_diff: left_diff,
match_percent: Some(match_percent),
},
ObjSectionDiff {
symbols: vec![],
data_diff: right_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 diff_data_symbol(
@@ -195,21 +178,56 @@ pub fn diff_data_symbol(
))
}
/// Compare the text sections of two object files.
/// This essentially adds up the match percentage of each symbol in the text section.
pub fn diff_text_section(
/// Compares a section of two object files.
/// This essentially adds up the match percentage of each symbol in the section.
pub fn diff_generic_section(
left: &ObjSection,
_right: &ObjSection,
left_diff: &ObjSectionDiff,
_right_diff: &ObjSectionDiff,
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
let match_percent = left
.symbols
.iter()
.zip(left_diff.symbols.iter())
.map(|(s, d)| d.match_percent.unwrap_or(0.0) * s.size as f32)
.sum::<f32>()
/ left.size as f32;
let match_percent = if left_diff.symbols.iter().all(|d| d.match_percent == Some(100.0)) {
100.0 // Avoid fp precision issues
} else {
left.symbols
.iter()
.zip(left_diff.symbols.iter())
.map(|(s, d)| d.match_percent.unwrap_or(0.0) * s.size as f32)
.sum::<f32>()
/ left.size as f32
};
Ok((
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
))
}
/// Compare the addresses and sizes of each symbol in the BSS sections.
pub fn diff_bss_section(
left: &ObjSection,
right: &ObjSection,
left_diff: &ObjSectionDiff,
right_diff: &ObjSectionDiff,
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
let deadline = Instant::now() + Duration::from_secs(5);
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,
Some(deadline),
);
let mut match_percent = get_diff_ratio(&ops, left_sizes.len(), right_sizes.len()) * 100.0;
// Use the highest match percent between two options:
// - Left symbols matching right symbols by name
// - Diff of the addresses and sizes of each symbol
let (generic_diff, _) = diff_generic_section(left, right, left_diff, right_diff)?;
if generic_diff.match_percent.unwrap_or(-1.0) > match_percent {
match_percent = generic_diff.match_percent.unwrap();
}
Ok((
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },

View File

@@ -20,7 +20,7 @@ pub enum DiffText<'a> {
/// Instruction argument
Argument(&'a ObjInsArgValue, Option<&'a ObjInsArgDiff>),
/// Branch destination
BranchDest(u64),
BranchDest(u64, Option<&'a ObjInsArgDiff>),
/// Symbol name
Symbol(&'a ObjSymbol),
/// Number of spaces
@@ -62,12 +62,12 @@ pub fn display_diff<E>(
if i == 0 {
cb(DiffText::Spacing(1))?;
}
let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref());
match arg {
ObjInsArg::PlainText(s) => {
cb(DiffText::Basic(s))?;
}
ObjInsArg::Arg(v) => {
let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref());
cb(DiffText::Argument(v, diff))?;
}
ObjInsArg::Reloc => {
@@ -75,7 +75,7 @@ pub fn display_diff<E>(
}
ObjInsArg::BranchDest(dest) => {
if let Some(dest) = dest.checked_sub(base_addr) {
cb(DiffText::BranchDest(dest))?;
cb(DiffText::BranchDest(dest, diff))?;
} else {
cb(DiffText::Basic("<unknown>"))?;
}
@@ -107,7 +107,9 @@ impl PartialEq<DiffText<'_>> for HighlightKind {
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == 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,
}
}
@@ -123,7 +125,7 @@ impl From<DiffText<'_>> for HighlightKind {
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,
}
}

View File

@@ -7,7 +7,7 @@ use crate::{
code::{diff_code, no_diff_code, process_code_symbol},
data::{
diff_bss_section, diff_bss_symbol, diff_data_section, diff_data_symbol,
diff_text_section, no_diff_symbol,
diff_generic_section, no_diff_symbol,
},
},
obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef},
@@ -93,6 +93,58 @@ pub enum MipsInstrCategory {
R5900,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
pub enum ArmArchVersion {
#[default]
#[strum(message = "Auto (default)")]
Auto,
#[strum(message = "ARMv4T (GBA)")]
V4T,
#[strum(message = "ARMv5TE (DS)")]
V5TE,
#[strum(message = "ARMv6K (3DS)")]
V6K,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
pub enum ArmR9Usage {
#[default]
#[strum(
message = "R9 or V6 (default)",
detailed_message = "Use R9 as a general-purpose register."
)]
GeneralPurpose,
#[strum(
message = "SB (static base)",
detailed_message = "Used for position-independent data (PID)."
)]
Sb,
#[strum(message = "TR (TLS register)", detailed_message = "Used for thread-local storage.")]
Tr,
}
#[inline]
const fn default_true() -> bool { true }
@@ -102,11 +154,20 @@ pub struct DiffObjConfig {
pub relax_reloc_diffs: bool,
#[serde(default = "default_true")]
pub space_between_args: bool,
pub combine_data_sections: bool,
// x86
pub x86_formatter: X86Formatter,
// MIPS
pub mips_abi: MipsAbi,
pub mips_instr_category: MipsInstrCategory,
// ARM
pub arm_arch_version: ArmArchVersion,
pub arm_unified_syntax: bool,
pub arm_av_registers: bool,
pub arm_r9_usage: ArmR9Usage,
pub arm_sl_usage: bool,
pub arm_fp_usage: bool,
pub arm_ip_usage: bool,
}
impl Default for DiffObjConfig {
@@ -114,9 +175,17 @@ impl Default for DiffObjConfig {
Self {
relax_reloc_diffs: false,
space_between_args: true,
combine_data_sections: false,
x86_formatter: Default::default(),
mips_abi: Default::default(),
mips_instr_category: Default::default(),
arm_arch_version: Default::default(),
arm_unified_syntax: true,
arm_av_registers: false,
arm_r9_usage: Default::default(),
arm_sl_usage: false,
arm_fp_usage: false,
arm_ip_usage: false,
}
}
}
@@ -417,7 +486,7 @@ pub fn diff_objs(
ObjSectionKind::Code => {
let left_section_diff = left_out.section_diff(left_section_idx);
let right_section_diff = right_out.section_diff(right_section_idx);
let (left_diff, right_diff) = diff_text_section(
let (left_diff, right_diff) = diff_generic_section(
left_section,
right_section,
left_section_diff,
@@ -427,12 +496,26 @@ pub fn diff_objs(
right_out.section_diff_mut(right_section_idx).merge(right_diff);
}
ObjSectionKind::Data => {
let (left_diff, right_diff) = diff_data_section(left_section, right_section)?;
let left_section_diff = left_out.section_diff(left_section_idx);
let right_section_diff = right_out.section_diff(right_section_idx);
let (left_diff, right_diff) = diff_data_section(
left_section,
right_section,
left_section_diff,
right_section_diff,
)?;
left_out.section_diff_mut(left_section_idx).merge(left_diff);
right_out.section_diff_mut(right_section_idx).merge(right_diff);
}
ObjSectionKind::Bss => {
let (left_diff, right_diff) = diff_bss_section(left_section, right_section)?;
let left_section_diff = left_out.section_diff(left_section_idx);
let right_section_diff = right_out.section_diff(right_section_idx);
let (left_diff, right_diff) = diff_bss_section(
left_section,
right_section,
left_section_diff,
right_section_diff,
)?;
left_out.section_diff_mut(left_section_idx).merge(left_diff);
right_out.section_diff_mut(right_section_idx).merge(right_diff);
}
@@ -475,8 +558,8 @@ fn matching_symbols(
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
let symbol_match = SymbolMatch {
left: Some(SymbolRef { section_idx, symbol_idx }),
right: find_symbol(right, symbol, section),
prev: find_symbol(prev, symbol, section),
right: find_symbol(right, symbol, section, Some(&right_used)),
prev: find_symbol(prev, symbol, section, None),
section_kind: section.kind,
};
matches.push(symbol_match);
@@ -508,7 +591,7 @@ fn matching_symbols(
matches.push(SymbolMatch {
left: None,
right: Some(symbol_ref),
prev: find_symbol(prev, symbol, section),
prev: find_symbol(prev, symbol, section, None),
section_kind: section.kind,
});
}
@@ -529,10 +612,25 @@ fn matching_symbols(
Ok(matches)
}
fn unmatched_symbols<'section, 'used>(
section: &'section ObjSection,
section_idx: usize,
used: Option<&'used HashSet<SymbolRef>>,
) -> impl Iterator<Item = (usize, &'section ObjSymbol)> + 'used
where
'section: 'used,
{
section.symbols.iter().enumerate().filter(move |&(symbol_idx, _)| {
// Skip symbols that have already been matched
!used.map(|u| u.contains(&SymbolRef { section_idx, symbol_idx })).unwrap_or(false)
})
}
fn find_symbol(
obj: Option<&ObjInfo>,
in_symbol: &ObjSymbol,
in_section: &ObjSection,
used: Option<&HashSet<SymbolRef>>,
) -> Option<SymbolRef> {
let obj = obj?;
// Try to find an exact name match
@@ -540,8 +638,8 @@ fn find_symbol(
if section.kind != in_section.kind {
continue;
}
if let Some(symbol_idx) =
section.symbols.iter().position(|symbol| symbol.name == in_symbol.name)
if let Some((symbol_idx, _)) = unmatched_symbols(section, section_idx, used)
.find(|(_, symbol)| symbol.name == in_symbol.name)
{
return Some(SymbolRef { section_idx, symbol_idx });
}
@@ -554,9 +652,33 @@ fn find_symbol(
if let Some((section_idx, section)) =
obj.sections.iter().enumerate().find(|(_, s)| s.name == in_section.name)
{
if let Some(symbol_idx) = section.symbols.iter().position(|symbol| {
symbol.address == in_symbol.address && symbol.name.starts_with('@')
}) {
if let Some((symbol_idx, _)) =
unmatched_symbols(section, section_idx, used).find(|(_, symbol)| {
symbol.address == in_symbol.address && symbol.name.starts_with('@')
})
{
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
// Match Metrowerks symbol$1234 against symbol$2345
if let Some((prefix, suffix)) = in_symbol.name.split_once('$') {
if !suffix.chars().all(char::is_numeric) {
return None;
}
for (section_idx, section) in obj.sections.iter().enumerate() {
if section.kind != in_section.kind {
continue;
}
if let Some((symbol_idx, _)) =
unmatched_symbols(section, section_idx, used).find(|&(_, symbol)| {
if let Some((p, s)) = symbol.name.split_once('$') {
prefix == p && s.chars().all(char::is_numeric)
} else {
false
}
})
{
return Some(SymbolRef { section_idx, symbol_idx });
}
}

View File

@@ -3,6 +3,7 @@ pub mod split_meta;
use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
use cwextab::*;
use filetime::FileTime;
use flagset::{flags, FlagSet};
use object::RelocationFlags;
@@ -113,6 +114,9 @@ pub struct ObjIns {
pub struct ObjSymbol {
pub name: String,
pub demangled_name: Option<String>,
pub has_extab: bool,
pub extab_name: Option<String>,
pub extabindex_name: Option<String>,
pub address: u64,
pub section_address: u64,
pub size: u64,
@@ -123,6 +127,13 @@ pub struct ObjSymbol {
pub virtual_address: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct ObjExtab {
pub func: ObjSymbol,
pub data: ExceptionTableData,
pub dtors: Vec<ObjSymbol>,
}
pub struct ObjInfo {
pub arch: Box<dyn ObjArch>,
pub path: PathBuf,
@@ -130,6 +141,8 @@ pub struct ObjInfo {
pub sections: Vec<ObjSection>,
/// Common BSS symbols
pub common: Vec<ObjSymbol>,
/// Exception tables
pub extab: Option<Vec<ObjExtab>>,
/// Split object metadata (.note.split section)
pub split_meta: Option<SplitMeta>,
}

View File

@@ -1,20 +1,23 @@
use std::{fs, io::Cursor, path::Path};
use std::{collections::HashSet, fs, io::Cursor, path::Path};
use anyhow::{anyhow, bail, ensure, Context, Result};
use byteorder::{BigEndian, ReadBytesExt};
use cwextab::decode_extab;
use filetime::FileTime;
use flagset::Flags;
use object::{
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, SectionIndex,
SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
Architecture, BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget,
SectionIndex, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
};
use crate::{
arch::{new_arch, ObjArch},
diff::DiffObjConfig,
obj::{
split_meta::{SplitMeta, SPLITMETA_SECTION},
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
ObjExtab, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet,
ObjSymbolFlags,
},
util::{read_u16, read_u32},
};
fn to_obj_section_kind(kind: SectionKind) -> Option<ObjSectionKind> {
@@ -54,12 +57,13 @@ fn to_obj_symbol(
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
}
let address = arch.symbol_address(symbol);
let section_address = if let Some(section) =
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
{
symbol.address() - section.address()
address - section.address()
} else {
symbol.address()
address
};
let demangled_name = arch.demangle(name);
// Find the virtual address for the symbol if available
@@ -69,7 +73,10 @@ fn to_obj_symbol(
Ok(ObjSymbol {
name: name.to_string(),
demangled_name,
address: symbol.address(),
has_extab: false,
extab_name: None,
extabindex_name: None,
address,
section_address,
size: symbol.size(),
size_known: symbol.size() != 0,
@@ -142,7 +149,7 @@ fn symbols_by_section(
}
}
}
result.sort_by_key(|v| v.address);
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
let mut iter = result.iter_mut().peekable();
while let Some(symbol) = iter.next() {
if symbol.size == 0 {
@@ -153,6 +160,23 @@ fn symbols_by_section(
}
}
}
if result.is_empty() {
// Dummy symbol for empty sections
result.push(ObjSymbol {
name: format!("[{}]", section.name),
demangled_name: None,
has_extab: false,
extab_name: None,
extabindex_name: None,
address: 0,
section_address: 0,
size: section.size,
size_known: true,
flags: Default::default(),
addend: 0,
virtual_address: None,
});
}
Ok(result)
}
@@ -168,6 +192,111 @@ fn common_symbols(
.collect::<Result<Vec<ObjSymbol>>>()
}
fn section_by_name<'a>(sections: &'a mut [ObjSection], name: &str) -> Option<&'a mut ObjSection> {
sections.iter_mut().find(|section| section.name == name)
}
fn exception_tables(
sections: &mut [ObjSection],
obj_file: &File<'_>,
) -> Result<Option<Vec<ObjExtab>>> {
//PowerPC only
if obj_file.architecture() != Architecture::PowerPc {
return Ok(None);
}
//Find the extab/extabindex sections
let extab_section = match section_by_name(sections, "extab") {
Some(section) => section.clone(),
None => {
return Ok(None);
}
};
let extabindex_section = match section_by_name(sections, "extabindex") {
Some(section) => section.clone(),
None => {
return Ok(None);
}
};
let text_section = match section_by_name(sections, ".text") {
Some(section) => section,
None => bail!(".text section is somehow missing, this should not happen"),
};
let mut result: Vec<ObjExtab> = vec![];
let extab_symbol_count = extab_section.symbols.len();
let extabindex_symbol_count = extabindex_section.symbols.len();
let extab_reloc_count = extab_section.relocations.len();
let table_count = extab_symbol_count;
let mut extab_reloc_index: usize = 0;
//Make sure that the number of symbols in the extab/extabindex section matches. If not, exit early
if extab_symbol_count != extabindex_symbol_count {
bail!("Extab/Extabindex symbol counts do not match");
}
//Convert the extab/extabindex section data
//Go through each extabindex entry
for i in 0..table_count {
let extabindex = &extabindex_section.symbols[i];
/* Get the function symbol and extab symbol from the extabindex relocations array. Each extabindex
entry has two relocations (the first for the function, the second for the extab entry) */
let extab_func = extabindex_section.relocations[i * 2].target.clone();
let extab = &extabindex_section.relocations[(i * 2) + 1].target;
let extab_start_addr = extab.address;
let extab_end_addr = extab_start_addr + extab.size;
//Find the function in the text section, and set the has extab flag
for i in 0..text_section.symbols.len() {
let func = &mut text_section.symbols[i];
if func.name == extab_func.name {
func.has_extab = true;
func.extab_name = Some(extab.name.clone());
func.extabindex_name = Some(extabindex.name.clone());
}
}
/* Iterate through the list of extab relocations, continuing until we hit a relocation
that isn't within the current extab symbol. Get the target dtor function symbol from
each relocation used, and add them to the list. */
let mut dtors: Vec<ObjSymbol> = vec![];
while extab_reloc_index < extab_reloc_count {
let extab_reloc = &extab_section.relocations[extab_reloc_index];
//If the current entry is past the current extab table, stop here
if extab_reloc.address >= extab_end_addr {
break;
}
//Otherwise, the current relocation is used by the current table
dtors.push(extab_reloc.target.clone());
//Go to the next entry
extab_reloc_index += 1;
}
//Decode the extab data
let start_index = extab_start_addr as usize;
let end_index = extab_end_addr as usize;
let extab_data = extab_section.data[start_index..end_index].try_into().unwrap();
let data = match decode_extab(extab_data) {
Some(decoded_data) => decoded_data,
None => {
log::warn!("Exception table decoding failed for function {}", extab_func.name);
return Ok(None);
}
};
//Add the new entry to the list
let entry = ObjExtab { func: extab_func, data, dtors };
result.push(entry);
}
Ok(Some(result))
}
fn find_section_symbol(
arch: &dyn ObjArch,
obj_file: &File<'_>,
@@ -203,6 +332,9 @@ fn find_section_symbol(
Ok(ObjSymbol {
name: name.to_string(),
demangled_name: None,
has_extab: false,
extab_name: None,
extabindex_name: None,
address: offset,
section_address: address - section.address(),
size: 0,
@@ -249,7 +381,7 @@ fn relocations_by_section(
_ => None,
};
let addend = if reloc.has_implicit_addend() {
arch.implcit_addend(section, address, &reloc)?
arch.implcit_addend(obj_file, section, address, &reloc)?
} else {
reloc.addend()
};
@@ -283,8 +415,8 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> {
.index()
.0;
let start = reader.position();
let size = reader.read_u32::<BigEndian>()?;
let base_address = reader.read_u32::<BigEndian>()? as u64;
let size = read_u32(obj_file, &mut reader)?;
let base_address = read_u32(obj_file, &mut reader)? as u64;
let Some(out_section) =
sections.iter_mut().find(|s| s.orig_index == text_section_index)
else {
@@ -294,12 +426,12 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> {
};
let end = start + size as u64;
while reader.position() < end {
let line_number = reader.read_u32::<BigEndian>()? as u64;
let statement_pos = reader.read_u16::<BigEndian>()?;
let line_number = read_u32(obj_file, &mut reader)? as u64;
let statement_pos = read_u16(obj_file, &mut reader)?;
if statement_pos != 0xFFFF {
log::warn!("Unhandled statement pos {}", statement_pos);
}
let address_delta = reader.read_u32::<BigEndian>()? as u64;
let address_delta = read_u32(obj_file, &mut reader)? as u64;
out_section.line_info.insert(base_address + address_delta, line_number);
log::debug!("Line: {:#x} -> {}", base_address + address_delta, line_number);
}
@@ -361,7 +493,108 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> {
Ok(())
}
pub fn read(obj_path: &Path) -> Result<ObjInfo> {
fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjSymbol> {
Ok(ObjSymbol {
name: symbol.name,
demangled_name: symbol.demangled_name,
has_extab: symbol.has_extab,
extab_name: symbol.extab_name,
extabindex_name: symbol.extabindex_name,
address: (symbol.address as i64 + address_change).try_into()?,
section_address: (symbol.section_address as i64 + address_change).try_into()?,
size: symbol.size,
size_known: symbol.size_known,
flags: symbol.flags,
addend: symbol.addend,
virtual_address: if let Some(virtual_address) = symbol.virtual_address {
Some((virtual_address as i64 + address_change).try_into()?)
} else {
None
},
})
}
fn combine_sections(section: ObjSection, combine: ObjSection) -> Result<ObjSection> {
let mut data = section.data;
data.extend(combine.data);
let address_change: i64 = (section.address + section.size) as i64 - combine.address as i64;
let mut symbols = section.symbols;
for symbol in combine.symbols {
symbols.push(update_combined_symbol(symbol, address_change)?);
}
let mut relocations = section.relocations;
for reloc in combine.relocations {
relocations.push(ObjReloc {
flags: reloc.flags,
address: (reloc.address as i64 + address_change).try_into()?,
target: reloc.target, // TODO: Should be updated?
target_section: reloc.target_section, // TODO: Same as above
});
}
let mut line_info = section.line_info;
for (addr, line) in combine.line_info {
let key = (addr as i64 + address_change).try_into()?;
line_info.insert(key, line);
}
Ok(ObjSection {
name: section.name,
kind: section.kind,
address: section.address,
size: section.size + combine.size,
data,
orig_index: section.orig_index,
symbols,
relocations,
virtual_address: section.virtual_address,
line_info,
})
}
fn combine_data_sections(sections: &mut Vec<ObjSection>) -> Result<()> {
let names_to_combine: HashSet<_> = sections
.iter()
.filter(|s| s.kind == ObjSectionKind::Data)
.map(|s| s.name.clone())
.collect();
for name in names_to_combine {
// Take section with lowest index
let (mut section_index, _) = sections
.iter()
.enumerate()
.filter(|(_, s)| s.name == name)
.min_by_key(|(_, s)| s.orig_index)
// Should not happen
.context("No combine section found with name")?;
let mut section = sections.remove(section_index);
// Remove equally named sections
let mut combines = vec![];
for i in (0..sections.len()).rev() {
if sections[i].name != name || sections[i].orig_index == section.orig_index {
continue;
}
combines.push(sections.remove(i));
if i < section_index {
section_index -= 1;
}
}
// Combine sections ordered by index
combines.sort_unstable_by_key(|c| c.orig_index);
for combine in combines {
section = combine_sections(section, combine)?;
}
sections.insert(section_index, section);
}
Ok(())
}
pub fn read(obj_path: &Path, config: &DiffObjConfig) -> Result<ObjInfo> {
let (data, timestamp) = {
let file = fs::File::open(obj_path)?;
let timestamp = FileTime::from_last_modification_time(&file.metadata()?);
@@ -377,9 +610,13 @@ pub fn read(obj_path: &Path) -> Result<ObjInfo> {
section.relocations =
relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
}
if config.combine_data_sections {
combine_data_sections(&mut sections)?;
}
line_info(&obj_file, &mut sections)?;
let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?;
Ok(ObjInfo { arch, path: obj_path.to_owned(), timestamp, sections, common, split_meta })
let extab = exception_tables(&mut sections, &obj_file)?;
Ok(ObjInfo { arch, path: obj_path.to_owned(), timestamp, sections, common, extab, split_meta })
}
pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> {

View File

@@ -1,6 +1,12 @@
use std::fmt::{LowerHex, UpperHex};
use std::{
fmt::{LowerHex, UpperHex},
io::Read,
};
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(crate) struct ReallySigned<N: PrimInt>(pub(crate) N);
@@ -22,3 +28,11 @@ impl<N: PrimInt> UpperHex for ReallySigned<N> {
f.pad_integral(num >= 0, prefix, &bare_hex)
}
}
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<R: Read>(obj_file: &object::File, reader: &mut R) -> Result<u16> {
Ok(obj_file.endianness().read_u16(reader.read_u16::<NativeEndian>()?))
}

View File

@@ -1,6 +1,6 @@
[package]
name = "objdiff-gui"
version = "2.0.0-alpha.4"
version = "2.0.0-beta.4"
edition = "2021"
rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"]
@@ -18,7 +18,7 @@ name = "objdiff"
path = "src/main.rs"
[features]
default = ["wgpu", "wsl"]
default = ["glow", "wgpu", "wsl"]
glow = ["eframe/glow"]
wgpu = ["eframe/wgpu", "dep:wgpu"]
wsl = []
@@ -29,6 +29,7 @@ bytes = "1.6.0"
cfg-if = "1.0.0"
const_format = "0.2.32"
cwdemangle = "1.0.0"
cwextab = "0.2.3"
dirs = "5.0.1"
egui = "0.27.2"
egui_extras = "0.27.2"
@@ -41,7 +42,9 @@ notify = { git = "https://github.com/encounter/notify", rev = "4c1783e8e041b5f69
objdiff-core = { path = "../objdiff-core", features = ["all"] }
png = "0.17.13"
pollster = "0.3.0"
regex = "1.10.5"
rfd = { version = "0.14.1" } #, default-features = false, features = ['xdg-portal']
rlwinmdec = "1.0.1"
ron = "0.8.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.116"

View File

@@ -35,10 +35,12 @@ use crate::{
data_diff::data_diff_ui,
debug::debug_window,
demangle::{demangle_window, DemangleViewState},
extab_diff::extab_diff_ui,
frame_history::FrameHistory,
function_diff::function_diff_ui,
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
jobs::jobs_ui,
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
symbol_diff::{symbol_diff_ui, DiffViewState, View},
},
};
@@ -48,11 +50,13 @@ pub struct ViewState {
pub jobs: JobQueue,
pub config_state: ConfigViewState,
pub demangle_state: DemangleViewState,
pub rlwinm_decode_state: RlwinmDecodeViewState,
pub diff_state: DiffViewState,
pub graphics_state: GraphicsViewState,
pub frame_history: FrameHistory,
pub show_appearance_config: bool,
pub show_demangle: bool,
pub show_rlwinm_decode: bool,
pub show_project_config: bool,
pub show_arch_config: bool,
pub show_debug: bool,
@@ -271,7 +275,7 @@ impl App {
#[cfg(feature = "glow")]
if let Some(gl) = &cc.gl {
use eframe::glow::HasContext;
app.view_state.graphics_state.active_backend = "OpenGL".to_string();
app.view_state.graphics_state.active_backend = "OpenGL (Fallback)".to_string();
app.view_state.graphics_state.active_device =
unsafe { gl.get_parameter_string(0x1F01) }; // GL_RENDERER
}
@@ -450,11 +454,13 @@ impl eframe::App for App {
jobs,
config_state,
demangle_state,
rlwinm_decode_state,
diff_state,
graphics_state,
frame_history,
show_appearance_config,
show_demangle,
show_rlwinm_decode,
show_project_config,
show_arch_config,
show_debug,
@@ -513,6 +519,10 @@ impl eframe::App for App {
*show_demangle = !*show_demangle;
ui.close_menu();
}
if ui.button("Rlwinm Decoder…").clicked() {
*show_rlwinm_decode = !*show_rlwinm_decode;
ui.close_menu();
}
});
ui.menu_button("Diff Options", |ui| {
if ui.button("Arch Settings…").clicked() {
@@ -559,6 +569,16 @@ impl eframe::App for App {
{
config.queue_reload = true;
}
if ui
.checkbox(
&mut config.diff_obj_config.combine_data_sections,
"Combine data sections",
)
.on_hover_text("Combines data sections with equal names.")
.changed()
{
config.queue_reload = true;
}
});
});
});
@@ -572,6 +592,10 @@ impl eframe::App for App {
egui::CentralPanel::default().show(ctx, |ui| {
data_diff_ui(ui, diff_state, appearance);
});
} else if diff_state.current_view == View::ExtabDiff && build_success {
egui::CentralPanel::default().show(ctx, |ui| {
extab_diff_ui(ui, diff_state, appearance);
});
} else {
egui::SidePanel::left("side_panel").show(ctx, |ui| {
egui::ScrollArea::both().show(ui, |ui| {
@@ -588,6 +612,7 @@ impl eframe::App for App {
project_window(ctx, config, show_project_config, config_state, appearance);
appearance_window(ctx, show_appearance_config, appearance);
demangle_window(ctx, show_demangle, demangle_state, appearance);
rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance);
arch_config_window(ctx, config, show_arch_config, appearance);
debug_window(ctx, show_debug, frame_history, appearance);
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);

View File

@@ -40,6 +40,7 @@ pub struct BuildConfig {
pub project_dir: Option<PathBuf>,
pub custom_make: Option<String>,
pub custom_args: Option<Vec<String>>,
#[allow(unused)]
pub selected_wsl_distro: Option<String>,
}
@@ -235,7 +236,7 @@ fn run_build(
total,
&cancel,
)?;
Some(read::read(target_path).with_context(|| {
Some(read::read(target_path, &config.diff_obj_config).with_context(|| {
format!("Failed to read object '{}'", target_path.display())
})?)
}
@@ -252,7 +253,7 @@ fn run_build(
&cancel,
)?;
Some(
read::read(base_path)
read::read(base_path, &config.diff_obj_config)
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
)
}

View File

@@ -11,6 +11,7 @@ mod views;
use std::{
path::PathBuf,
process::ExitCode,
rc::Rc,
sync::{Arc, Mutex},
};
@@ -37,7 +38,7 @@ const APP_NAME: &str = "objdiff";
// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() {
fn main() -> ExitCode {
// Log to stdout (if you run with `RUST_LOG=debug`).
tracing_subscriber::fmt::init();
@@ -48,7 +49,6 @@ fn main() {
let app_path = std::env::current_exe().ok();
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
let exec_path_clone = exec_path.clone();
let mut native_options =
eframe::NativeOptions { follow_system_theme: false, ..Default::default() };
match load_icon() {
@@ -56,7 +56,7 @@ fn main() {
native_options.viewport.icon = Some(Arc::new(data));
}
Err(e) => {
log::warn!("Failed to load application icon: {}", e);
log::warn!("Failed to load application icon: {e:?}");
}
}
let mut graphics_config = GraphicsConfig::default();
@@ -69,7 +69,7 @@ fn main() {
}
Ok(None) => {}
Err(e) => {
log::error!("Failed to load native config: {:?}", e);
log::error!("Failed to load native config: {e:?}");
}
}
graphics_config_path = Some(config_path);
@@ -87,6 +87,104 @@ fn main() {
};
}
}
let mut eframe_error = None;
if let Err(e) = run_eframe(
native_options.clone(),
utc_offset,
exec_path.clone(),
app_path.clone(),
graphics_config.clone(),
graphics_config_path.clone(),
) {
eframe_error = Some(e);
}
#[cfg(feature = "wgpu")]
if let Some(e) = eframe_error {
// Attempt to relaunch using wgpu auto backend if the desired backend failed
#[allow(unused_mut)]
let mut should_relaunch = graphics_config.desired_backend != GraphicsBackend::Auto;
#[cfg(feature = "glow")]
{
// If the desired backend is OpenGL, we should try to relaunch using the glow renderer
should_relaunch &= graphics_config.desired_backend != GraphicsBackend::OpenGL;
}
if should_relaunch {
log::warn!("Failed to launch application: {e:?}");
log::warn!("Attempting to relaunch using auto-detected backend");
native_options.wgpu_options.supported_backends = Default::default();
if let Err(e) = run_eframe(
native_options.clone(),
utc_offset,
exec_path.clone(),
app_path.clone(),
graphics_config.clone(),
graphics_config_path.clone(),
) {
eframe_error = Some(e);
} else {
eframe_error = None;
}
} else {
eframe_error = Some(e);
}
}
#[cfg(all(feature = "wgpu", feature = "glow"))]
if let Some(e) = eframe_error {
// Attempt to relaunch using the glow renderer if the wgpu backend failed
log::warn!("Failed to launch application: {e:?}");
log::warn!("Attempting to relaunch using fallback OpenGL backend");
native_options.renderer = eframe::Renderer::Glow;
if let Err(e) = run_eframe(
native_options,
utc_offset,
exec_path.clone(),
app_path,
graphics_config,
graphics_config_path,
) {
eframe_error = Some(e);
} else {
eframe_error = None;
}
}
if let Some(e) = eframe_error {
log::error!("Failed to launch application: {e:?}");
return ExitCode::FAILURE;
}
// Attempt to relaunch application from the updated path
if let Ok(mut guard) = exec_path.lock() {
if let Some(path) = guard.take() {
cfg_if! {
if #[cfg(unix)] {
let e = exec::Command::new(path)
.args(&std::env::args().collect::<Vec<String>>())
.exec();
log::error!("Failed to relaunch: {e:?}");
return ExitCode::FAILURE;
} else {
let result = std::process::Command::new(path)
.args(std::env::args())
.spawn();
if let Err(e) = result {
log::error!("Failed to relaunch: {e:?}");
return ExitCode::FAILURE;
}
}
}
}
};
ExitCode::SUCCESS
}
fn run_eframe(
native_options: eframe::NativeOptions,
utc_offset: UtcOffset,
exec_path_clone: Rc<Mutex<Option<PathBuf>>>,
app_path: Option<PathBuf>,
graphics_config: GraphicsConfig,
graphics_config_path: Option<PathBuf>,
) -> Result<(), eframe::Error> {
eframe::run_native(
APP_NAME,
native_options,
@@ -101,28 +199,6 @@ fn main() {
))
}),
)
.expect("Failed to run eframe application");
// Attempt to relaunch application from the updated path
if let Ok(mut guard) = exec_path.lock() {
if let Some(path) = guard.take() {
cfg_if! {
if #[cfg(unix)] {
let result = exec::Command::new(path)
.args(&std::env::args().collect::<Vec<String>>())
.exec();
log::error!("Failed to relaunch: {result:?}");
} else {
let result = std::process::Command::new(path)
.args(std::env::args())
.spawn();
if let Err(e) = result {
log::error!("Failed to relaunch: {:?}", e);
}
}
}
}
};
}
// when compiling to web using trunk.

View File

@@ -16,7 +16,7 @@ use egui::{
use globset::Glob;
use objdiff_core::{
config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
diff::{MipsAbi, MipsInstrCategory, X86Formatter},
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter},
};
use self_update::cargo_crate_version;
use strum::{EnumMessage, VariantArray};
@@ -907,4 +907,69 @@ fn arch_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appea
}
}
});
ui.separator();
ui.heading("ARM");
egui::ComboBox::new("arm_arch_version", "Architecture Version")
.selected_text(config.diff_obj_config.arm_arch_version.get_message().unwrap())
.show_ui(ui, |ui| {
for &version in ArmArchVersion::VARIANTS {
if ui
.selectable_label(
config.diff_obj_config.arm_arch_version == version,
version.get_message().unwrap(),
)
.clicked()
{
config.diff_obj_config.arm_arch_version = version;
config.queue_reload = true;
}
}
});
let response = ui
.checkbox(&mut config.diff_obj_config.arm_unified_syntax, "Unified syntax")
.on_hover_text("Disassemble as unified assembly language (UAL).");
if response.changed() {
config.queue_reload = true;
}
let response = ui
.checkbox(&mut config.diff_obj_config.arm_av_registers, "Use A/V registers")
.on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
if response.changed() {
config.queue_reload = true;
}
egui::ComboBox::new("arm_r9_usage", "Display R9 as")
.selected_text(config.diff_obj_config.arm_r9_usage.get_message().unwrap())
.show_ui(ui, |ui| {
for &usage in ArmR9Usage::VARIANTS {
if ui
.selectable_label(
config.diff_obj_config.arm_r9_usage == usage,
usage.get_message().unwrap(),
)
.on_hover_text(usage.get_detailed_message().unwrap())
.clicked()
{
config.diff_obj_config.arm_r9_usage = usage;
config.queue_reload = true;
}
}
});
let response = ui
.checkbox(&mut config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
.on_hover_text("Used for explicit stack limits.");
if response.changed() {
config.queue_reload = true;
}
let response = ui
.checkbox(&mut config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
.on_hover_text("Used for frame pointers.");
if response.changed() {
config.queue_reload = true;
}
let response = ui
.checkbox(&mut config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
.on_hover_text("Used for interworking and long branches.");
if response.changed() {
config.queue_reload = true;
}
}

View File

@@ -0,0 +1,218 @@
use egui::{text::LayoutJob, Align, Layout, ScrollArea, Ui, Vec2};
use egui_extras::{Size, StripBuilder};
use objdiff_core::{
diff::ObjDiff,
obj::{ObjExtab, ObjInfo, ObjSymbol, SymbolRef},
};
use time::format_description;
use crate::views::{
appearance::Appearance,
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
};
fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == selected_symbol.symbol_name {
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}
fn decode_extab(extab: &ObjExtab) -> String {
let mut text = String::from("");
let mut dtor_names: Vec<&str> = vec![];
for dtor in &extab.dtors {
//For each function name, use the demangled name by default,
//and if not available fallback to the original name
let name = match &dtor.demangled_name {
Some(demangled_name) => demangled_name,
None => &dtor.name,
};
dtor_names.push(name.as_str());
}
if let Some(decoded) = extab.data.to_string(&dtor_names) {
text += decoded.as_str();
}
text
}
fn find_extab_entry(obj: &ObjInfo, symbol: &ObjSymbol) -> Option<ObjExtab> {
if let Some(extab_array) = &obj.extab {
for extab_entry in extab_array {
if extab_entry.func.name == symbol.name {
return Some(extab_entry.clone());
}
}
} else {
return None;
}
None
}
fn extab_text_ui(
ui: &mut Ui,
obj: &(ObjInfo, ObjDiff),
symbol_ref: SymbolRef,
appearance: &Appearance,
) -> Option<()> {
let (_section, symbol) = obj.0.section_symbol(symbol_ref);
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) {
let text = decode_extab(&extab_entry);
ui.colored_label(appearance.replace_color, &text);
return Some(());
}
None
}
fn extab_ui(
ui: &mut Ui,
obj: Option<&(ObjInfo, ObjDiff)>,
selected_symbol: &SymbolRefByName,
appearance: &Appearance,
_left: bool,
) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
if let (Some(object), Some(symbol_ref)) = (obj, symbol) {
extab_text_ui(ui, object, symbol_ref, appearance);
}
});
});
}
pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
else {
return;
};
// Header
let available_width = ui.available_width();
let column_width = available_width / 2.0;
ui.allocate_ui_with_layout(
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
// Left column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() {
state.current_view = View::SymbolDiff;
}
});
let name = selected_symbol
.demangled_symbol_name
.as_deref()
.unwrap_or(&selected_symbol.symbol_name);
let mut job = LayoutJob::simple(
name.to_string(),
appearance.code_font.clone(),
appearance.highlight_color,
column_width,
);
job.wrap.break_anywhere = true;
job.wrap.max_rows = 1;
ui.label(job);
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.label("Diff target:");
});
},
);
// Right column
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
ui.horizontal(|ui| {
if ui
.add_enabled(!state.build_running, egui::Button::new("Build"))
.clicked()
{
state.queue_build = true;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format =
format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
}
});
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if let Some(match_percent) = result
.second_obj
.as_ref()
.and_then(|(obj, diff)| {
find_symbol(obj, selected_symbol).map(|sref| {
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
})
})
.and_then(|symbol| symbol.match_percent)
{
ui.colored_label(
match_color_for_symbol(match_percent, appearance),
&format!("{match_percent:.0}%"),
);
} else {
ui.colored_label(appearance.replace_color, "Missing");
}
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// Table
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
strip.cell(|ui| {
extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true);
});
strip.cell(|ui| {
extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false);
});
});
});
});
}

View File

@@ -216,8 +216,11 @@ fn diff_text_ui(
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
}
}
DiffText::BranchDest(addr) => {
DiffText::BranchDest(addr, diff) => {
label_text = format!("{addr:x}");
if let Some(diff) = diff {
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
}
}
DiffText::Symbol(sym) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);

View File

@@ -36,7 +36,7 @@ pub enum GraphicsBackend {
OpenGL,
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct GraphicsConfig {
#[serde(default)]
pub desired_backend: GraphicsBackend,

View File

@@ -5,11 +5,13 @@ pub(crate) mod config;
pub(crate) mod data_diff;
pub(crate) mod debug;
pub(crate) mod demangle;
pub(crate) mod extab_diff;
pub(crate) mod file;
pub(crate) mod frame_history;
pub(crate) mod function_diff;
pub(crate) mod graphics;
pub(crate) mod jobs;
pub(crate) mod rlwinm;
pub(crate) mod symbol_diff;
#[inline]

View File

@@ -0,0 +1,34 @@
use egui::TextStyle;
use crate::views::appearance::Appearance;
#[derive(Default)]
pub struct RlwinmDecodeViewState {
pub text: String,
}
pub fn rlwinm_decode_window(
ctx: &egui::Context,
show: &mut bool,
state: &mut RlwinmDecodeViewState,
appearance: &Appearance,
) {
egui::Window::new("Rlwinm Decoder").open(show).show(ctx, |ui| {
ui.text_edit_singleline(&mut state.text);
ui.add_space(10.0);
if let Some(demangled) = rlwinmdec::decode(&state.text) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(appearance.replace_color, &demangled);
});
if ui.button("Copy").clicked() {
ui.output_mut(|output| output.copied_text = demangled);
}
} else {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(appearance.replace_color, "[invalid]");
});
}
});
}

View File

@@ -9,6 +9,7 @@ use objdiff_core::{
diff::{ObjDiff, ObjSymbolDiff},
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef},
};
use regex::{Regex, RegexBuilder};
use crate::{
app::AppConfigRef,
@@ -33,6 +34,7 @@ pub enum View {
SymbolDiff,
FunctionDiff,
DataDiff,
ExtabDiff,
}
#[derive(Default)]
@@ -43,6 +45,7 @@ pub struct DiffViewState {
pub symbol_state: SymbolViewState,
pub function_state: FunctionViewState,
pub search: String,
pub search_regex: Option<Regex>,
pub queue_build: bool,
pub build_running: bool,
pub scratch_available: bool,
@@ -57,6 +60,7 @@ pub struct SymbolViewState {
pub reverse_fn_order: bool,
pub disable_reverse_fn_order: bool,
pub show_hidden_symbols: bool,
pub queue_extab_decode: bool,
}
impl DiffViewState {
@@ -131,7 +135,12 @@ pub fn match_color_for_symbol(match_percent: f32, appearance: &Appearance) -> Co
}
}
fn symbol_context_menu_ui(ui: &mut Ui, symbol: &ObjSymbol) {
fn symbol_context_menu_ui(
ui: &mut Ui,
state: &mut SymbolViewState,
symbol: &ObjSymbol,
section: Option<&ObjSection>,
) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
@@ -152,6 +161,17 @@ fn symbol_context_menu_ui(ui: &mut Ui, symbol: &ObjSymbol) {
ui.close_menu();
}
}
if let Some(section) = section {
if symbol.has_extab && ui.button("Decode exception table").clicked() {
state.queue_extab_decode = true;
state.selected_symbol = Some(SymbolRefByName {
symbol_name: symbol.name.clone(),
demangled_symbol_name: symbol.demangled_name.clone(),
section_name: section.name.clone(),
});
ui.close_menu();
}
}
});
}
@@ -173,6 +193,20 @@ fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol, appearance: &Appearance) {
if let Some(address) = symbol.virtual_address {
ui.colored_label(appearance.replace_color, format!("Virtual address: {:#x}", address));
}
if symbol.has_extab {
if let (Some(extab_name), Some(extabindex_name)) =
(&symbol.extab_name, &symbol.extabindex_name)
{
ui.colored_label(
appearance.highlight_color,
format!("Extab Symbol: {}", extab_name),
);
ui.colored_label(
appearance.highlight_color,
format!("Extabindex Symbol: {}", extabindex_name),
);
}
}
});
}
@@ -199,21 +233,28 @@ fn symbol_ui(
{
selected = symbol_diff.symbol_ref == sym_ref;
}
write_text("[", appearance.text_color, &mut job, appearance.code_font.clone());
if symbol.flags.0.contains(ObjSymbolFlags::Common) {
write_text("c", appearance.replace_color, &mut job, appearance.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
write_text("g", appearance.insert_color, &mut job, appearance.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
write_text("l", appearance.text_color, &mut job, appearance.code_font.clone());
if !symbol.flags.0.is_empty() {
write_text("[", appearance.text_color, &mut job, appearance.code_font.clone());
if symbol.flags.0.contains(ObjSymbolFlags::Common) {
write_text("c", appearance.replace_color, &mut job, appearance.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
write_text("g", appearance.insert_color, &mut job, appearance.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
write_text("l", appearance.text_color, &mut job, appearance.code_font.clone());
}
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
write_text("w", appearance.text_color, &mut job, appearance.code_font.clone());
}
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
write_text(
"h",
appearance.deemphasized_text_color,
&mut job,
appearance.code_font.clone(),
);
}
write_text("] ", appearance.text_color, &mut job, appearance.code_font.clone());
}
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
write_text("w", appearance.text_color, &mut job, appearance.code_font.clone());
}
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
write_text("h", appearance.deemphasized_text_color, &mut job, appearance.code_font.clone());
}
write_text("] ", appearance.text_color, &mut job, appearance.code_font.clone());
if let Some(match_percent) = symbol_diff.match_percent {
write_text("(", appearance.text_color, &mut job, appearance.code_font.clone());
write_text(
@@ -228,7 +269,7 @@ fn symbol_ui(
let response = SelectableLabel::new(selected, job)
.ui(ui)
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, symbol, appearance));
response.context_menu(|ui| symbol_context_menu_ui(ui, symbol));
response.context_menu(|ui| symbol_context_menu_ui(ui, state, symbol, section));
if response.clicked() {
if let Some(section) = section {
if section.kind == ObjSectionKind::Code {
@@ -258,17 +299,23 @@ fn symbol_ui(
(None, None)
};
}
//If the decode extab context menu option was clicked, switch to the extab view
if state.queue_extab_decode {
ret = Some(View::ExtabDiff);
state.queue_extab_decode = false;
}
ret
}
fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool {
search_str.is_empty()
|| symbol.name.contains(search_str)
|| symbol
.demangled_name
.as_ref()
.map(|s| s.to_ascii_lowercase().contains(search_str))
.unwrap_or(false)
fn symbol_matches_search(symbol: &ObjSymbol, search_regex: Option<&Regex>) -> bool {
if let Some(search_regex) = search_regex {
search_regex.is_match(&symbol.name)
|| symbol.demangled_name.as_ref().map(|s| search_regex.is_match(s)).unwrap_or(false)
} else {
true
}
}
#[must_use]
@@ -276,7 +323,7 @@ fn symbol_list_ui(
ui: &mut Ui,
obj: &(ObjInfo, ObjDiff),
state: &mut SymbolViewState,
lower_search: &str,
search_regex: Option<&Regex>,
appearance: &Appearance,
left: bool,
) -> Option<View> {
@@ -289,6 +336,9 @@ fn symbol_list_ui(
if !obj.0.common.is_empty() {
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
for (symbol, symbol_diff) in obj.0.common.iter().zip(&obj.1.common) {
if !symbol_matches_search(symbol, search_regex) {
continue;
}
ret = ret.or(symbol_ui(
ui,
symbol,
@@ -336,7 +386,7 @@ fn symbol_list_ui(
for (symbol, symbol_diff) in
section.symbols.iter().zip(&section_diff.symbols).rev()
{
if !symbol_matches_search(symbol, lower_search) {
if !symbol_matches_search(symbol, search_regex) {
continue;
}
ret = ret.or(symbol_ui(
@@ -353,7 +403,7 @@ fn symbol_list_ui(
for (symbol, symbol_diff) in
section.symbols.iter().zip(&section_diff.symbols)
{
if !symbol_matches_search(symbol, lower_search) {
if !symbol_matches_search(symbol, search_regex) {
continue;
}
ret = ret.or(symbol_ui(
@@ -407,7 +457,7 @@ fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) {
}
pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) {
let DiffViewState { build, current_view, symbol_state, search, .. } = state;
let DiffViewState { build, current_view, symbol_state, search, search_regex, .. } = state;
let Some(result) = build else {
return;
};
@@ -442,7 +492,17 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
}
});
TextEdit::singleline(search).hint_text("Filter symbols").ui(ui);
if TextEdit::singleline(search).hint_text("Filter symbols").ui(ui).changed() {
if search.is_empty() {
*search_regex = None;
} else if let Ok(regex) =
RegexBuilder::new(search).case_insensitive(true).build()
{
*search_regex = Some(regex);
} else {
*search_regex = None;
}
}
},
);
@@ -480,7 +540,6 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
// Table
let mut ret = None;
let lower_search = search.to_ascii_lowercase();
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
@@ -492,7 +551,7 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
ui,
obj,
symbol_state,
&lower_search,
search_regex.as_ref(),
appearance,
true,
));
@@ -512,7 +571,7 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
ui,
obj,
symbol_state,
&lower_search,
search_regex.as_ref(),
appearance,
false,
));