Compare commits

..

35 Commits

Author SHA1 Message Date
Luke Street 7834185223 Version 3.5.1 2025-12-23 12:33:12 -07:00
LagoLunatic 216dd70d5b Fix mouse hovers overriding typing in the symbol search field (#308) 2025-12-23 12:32:07 -07:00
LagoLunatic 976e629f08 Fix non-strings also being escaped when copied (#307)
* Fix non-strings also being escaped when copied

* Move copy escape logic into objdiff-core
2025-12-23 12:31:36 -07:00
Luke Street 67b28b7da1 Version 3.5.0 2025-12-21 10:02:59 -07:00
LagoLunatic c0b9bfae21 Fix special ASCII characters not being escaped when copied to clipboard (#306) 2025-12-21 09:58:45 -07:00
LagoLunatic f3f4b0482a Combine .init sections separately from .text sections (#304) 2025-12-21 09:58:13 -07:00
LagoLunatic 278683f103 Improve automatic symbol pairing for functions with compiler-generated names (#303)
* Fix `__arraydtor$1234`s sometimes being swapped

The list of symbols is not necessarily sorted by address, so if there are multiple `__arraydtor$1234` functions it's possible that they're in reverse order on the right, which would cause them to be paired up incorrectly.

To fix this we sort by section index and symbol address after filtering down the list of symbols to ones that have matching names.

* Support pairing up symbols for anonymous classes

* Disallow exact-name pairups for compiler-generated symbols
2025-12-21 09:57:17 -07:00
LagoLunatic 6ad60de32a Allow clicking on branch arrows to scroll target into view (#302) 2025-12-21 09:55:55 -07:00
Luke Street 90c93164ab Update deny.toml 2025-12-21 09:36:27 -07:00
Luke Street c02eb31dbb Version 3.4.5 2025-12-03 18:03:11 -07:00
Darxoon 86d92866aa Fix "Unsupported ARM implicit relocation 29" on armcc binaries (#296) 2025-12-03 17:59:24 -07:00
Luke Street 51c3af2bbe Version 3.4.4 2025-11-26 19:33:00 -07:00
LagoLunatic d0b8b449d9 Ignore hidden symbols when diffing data sections (#291) 2025-11-26 18:49:36 -07:00
LagoLunatic 32f5f202f7 Update outdated extab test snapshot (#292) 2025-11-26 17:56:32 -05:00
LagoLunatic 481dbc185a Support more string encodings and allow copying unescaped strings (#288)
* Copy strings without escape sequences in them

* Add support for more encodings from encoding_rs

Also use encoding_rs instead of CStr for UTF-8.
2025-11-22 19:36:41 -07:00
Luke Street 26a4cc79cf Version 3.4.3 2025-11-22 12:47:03 -07:00
Luke Street 5c96c2ee51 Update cwextab 2025-11-22 12:45:58 -07:00
Luke Street d162fe847e Remove --mapping, --selecting-{left,right} from CLI 2025-11-21 23:55:26 -07:00
Luke Street 2a24eb5aec Version 3.4.2 2025-11-21 22:15:20 -07:00
Aetias f7c291bd55 arm: Fix .word reading 4 bytes when less than 4 remain (#285) 2025-11-20 15:38:55 -07:00
Luke Street 03a578c1bb Update test snapshots for new cwextab 2025-11-18 22:29:42 -07:00
Luke Street d09ef8e207 Version v3.4.1 2025-11-18 22:22:43 -07:00
Luke Street 63552a58ae Move copy button to first header row 2025-11-18 22:22:02 -07:00
Luke Street 827f4a42bd Upgrade all dependencies (incl. egui) 2025-11-18 22:21:46 -07:00
Franco M b2dcecc5d8 Copy button (#271)
* Update diff.rs

* Fixes

* More fixes

* More fixes...

* Cargo fmt

* fmt

* Trim extra

* cargo +nightly fmt

* Update 'Copy ASM' button text to include emoji
2025-11-18 22:00:20 -07:00
Dávid Balatoni 67b237eab6 Adjust symbol name matching logic for GCC (#278)
* Adjust symbol name matching logic for GCC

* Turn $ and . into a list

* Fix borrow issue
2025-11-18 21:55:55 -07:00
Aetias 66da80ff69 arm: Fix .word not on 4-byte boundary (#282) 2025-11-18 21:55:40 -07:00
Luke Street 0a85f540f2 Version v3.4.0 2025-11-12 12:22:01 -07:00
Anghelo Carvajal d79b5b1233 Update gnuv2_demangle to 0.4 (#280) 2025-11-09 11:53:02 -07:00
Anghelo Carvajal 8f5519cb6a Update gnuv2_demangle to version 0.3 (#275) 2025-10-12 21:19:07 -06:00
Aetias 19ec08be9a Update unarm to 2.0 (#274)
* Migrate to unarm 2.0

* Update unarm to proper 2.0 release

* Deduplicate formatters for opaque instruction parts in ARM

* Add option to enable VFPv2 for ARM

* Update unarm to 2.0.1

* Fix read order for big-endian Thumb code

* Skip leading space in ARM instruction params

* Update ARM tests

* arm.rs: Use `alloc::borrow::Cow`
2025-10-12 21:13:29 -06:00
Luke Street 2ad0898efa Version v3.3.1 2025-10-08 21:47:36 -06:00
Ethan Lafrenais 03f2bcb8b1 R_MIPS_GPREL32 Support (#270)
* R_MIPS_GPREL32 Support

* cargo fmt

* Add name for R_MIPS_GPREL32 relocation
2025-10-08 20:34:35 -06:00
BR- 781071761a show symbol size in symbol list (#268)
* show symbol size in symbol list

* configs for show symbol size

* move symbol size config to config-schema
2025-10-08 20:32:07 -06:00
Luke Street 572afa8551 Support IDO .mdebug for line numbers (#264)
* Support IDO .mdebug for line numbers

Resolves #179

* Fix wasm build && cargo clippy --fix

* Resolve review comment
2025-09-25 10:00:40 -06:00
47 changed files with 2382 additions and 1787 deletions
Generated
+754 -729
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -14,7 +14,7 @@ default-members = [
resolver = "3"
[workspace.package]
version = "3.3.0"
version = "3.5.1"
authors = ["Luke Street <luke@street.dev>"]
edition = "2024"
license = "MIT OR Apache-2.0"
+2
View File
@@ -75,6 +75,8 @@ ignore = [
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate is an indirect dependency" },
{ id = "RUSTSEC-2025-0052", reason = "Unmaintained async-std crate is an indirect dependency" },
{ id = "RUSTSEC-2025-0119", reason = "Unmaintained number_prefix crate is an indirect dependency" },
{ id = "RUSTSEC-2025-0134", reason = "Unmaintained rustls-pemfile crate is an indirect dependency" },
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
+3 -3
View File
@@ -16,19 +16,19 @@ publish = false
anyhow = "1.0"
argp = "0.4"
crossterm = "0.29"
enable-ansi-support = "0.2"
enable-ansi-support = "0.3"
memmap2 = "0.9"
objdiff-core = { path = "../objdiff-core", features = ["all"] }
prost = "0.14"
ratatui = "0.29"
rayon = "1.10"
rayon = "1.11"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
supports-color = "3.0"
time = { version = "0.3", features = ["formatting", "local-offset"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
typed-path = "0.11"
typed-path = "0.12"
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1"
+1 -20
View File
@@ -66,15 +66,6 @@ pub struct Args {
#[argp(option, short = 'c')]
/// Configuration property (key=value)
config: Vec<String>,
#[argp(option, short = 'm')]
/// Symbol mapping (target=base)
mapping: Vec<String>,
#[argp(option)]
/// Left symbol name for selection
selecting_left: Option<String>,
#[argp(option)]
/// Right symbol name for selection
selecting_right: Option<String>,
}
pub fn run(args: Args) -> Result<()> {
@@ -183,17 +174,7 @@ fn build_config_from_args(
apply_project_options(&mut diff_config, options)?;
}
apply_config_args(&mut diff_config, &args.config)?;
let mut mapping_config = MappingConfig {
mappings: Default::default(),
selecting_left: args.selecting_left.clone(),
selecting_right: args.selecting_right.clone(),
};
for mapping in &args.mapping {
let (target, base) =
mapping.split_once('=').context("--mapping expects \"target=base\"")?;
mapping_config.mappings.insert(target.to_string(), base.to_string());
}
Ok((diff_config, mapping_config))
Ok((diff_config, MappingConfig::default()))
}
pub struct AppState {
+1 -2
View File
@@ -378,7 +378,6 @@ impl UiView for FunctionDiffUi {
}
// Reload
KeyCode::Char('r') => {
result.redraw = true;
return EventControlFlow::Reload;
}
// Scroll right
@@ -400,7 +399,6 @@ impl UiView for FunctionDiffUi {
FunctionRelocDiffs::DataValue => FunctionRelocDiffs::All,
FunctionRelocDiffs::All => FunctionRelocDiffs::None,
};
result.redraw = true;
return EventControlFlow::Reload;
}
// Toggle three-way diff
@@ -541,6 +539,7 @@ impl FunctionDiffUi {
DiffText::Opcode(mnemonic, _op) => format!("{mnemonic} "),
DiffText::Argument(arg) => arg.to_string(),
DiffText::BranchDest(addr) => format!("{addr:x}"),
DiffText::BranchArrow(_) => " ~> ".to_string(),
DiffText::Symbol(sym) => {
sym.demangled_name.as_ref().unwrap_or(&sym.name).clone()
}
+10 -10
View File
@@ -132,13 +132,13 @@ itertools = { version = "0.14", default-features = false, features = ["use_alloc
log = { version = "0.4", default-features = false, optional = true }
memmap2 = { version = "0.9", optional = true }
num-traits = { version = "0.2", default-features = false, optional = true }
object = { git = "https://github.com/encounter/object", branch = "omf", default-features = false, features = ["read_core", "elf", "coff", "omf"] }
object = { version = "0.37", default-features = false, features = ["read_core", "elf", "coff"] }
pbjson = { version = "0.8", default-features = false, optional = true }
prost = { version = "0.14", default-features = false, features = ["derive"], optional = true }
regex = { version = "1.11", default-features = false, features = [], optional = true }
regex = { version = "1.12", default-features = false, features = [], optional = true }
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
similar = { git = "https://github.com/encounter/similar.git", branch = "no_std", default-features = false, features = ["hashbrown"], optional = true }
typed-path = { version = "0.11", default-features = false, optional = true }
typed-path = { version = "0.12", default-features = false, optional = true }
# config
globset = { version = "0.4", default-features = false, optional = true }
@@ -155,13 +155,13 @@ powerpc = { version = "0.4", optional = true }
rlwinmdec = { version = "1.1", optional = true }
# mips
rabbitizer = { version = "2.0.0-alpha.4", default-features = false, features = ["all_extensions"], optional = true }
rabbitizer = { version = "2.0.0-alpha.7", default-features = false, features = ["all_extensions"], optional = true }
# x86
iced-x86 = { version = "1.21", default-features = false, features = ["decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums", "no_std"], optional = true }
# arm
unarm = { version = "1.9", optional = true }
unarm = { version = "2.1", optional = true }
arm-attr = { version = "0.2", optional = true }
# arm64
@@ -169,17 +169,17 @@ yaxpeax-arch = { version = "0.3", default-features = false, optional = true }
yaxpeax-arm = { version = "0.3", default-features = false, optional = true }
# build
notify = { version = "8.1.0", optional = true }
notify-debouncer-full = { version = "0.5.0", optional = true }
notify = { version = "8.2.0", optional = true }
notify-debouncer-full = { version = "0.6.0", optional = true }
shell-escape = { version = "0.1", optional = true }
tempfile = { version = "3.20", optional = true }
tempfile = { version = "3.23", optional = true }
time = { version = "0.3", optional = true }
encoding_rs = { version = "0.8.35", optional = true }
# demangler
cpp_demangle = { version = "0.4", optional = true, default-features = false, features = ["alloc"] }
cpp_demangle = { version = "0.5", optional = true, default-features = false, features = ["alloc"] }
cwdemangle = { version = "1.0", optional = true }
gnuv2_demangle = { version = "0.1.0", optional = true }
gnuv2_demangle = { version = "0.4", optional = true }
msvc-demangler = { version = "0.11", optional = true }
[target.'cfg(windows)'.dependencies]
+46
View File
@@ -82,6 +82,27 @@
"name": "Space between args",
"description": "Adds a space between arguments in the diff output."
},
{
"id": "showSymbolSizes",
"type": "choice",
"default": "off",
"name": "Show symbol sizes",
"description": "Shows symbol sizes in the symbol view.",
"items": [
{
"value": "off",
"name": "Off"
},
{
"value": "hex",
"name": "Hex"
},
{
"value": "decimal",
"name": "Decimal"
}
]
},
{
"id": "combineDataSections",
"type": "boolean",
@@ -107,20 +128,43 @@
"value": "auto",
"name": "Auto"
},
{
"value": "v4",
"name": "ARMv4"
},
{
"value": "v4t",
"name": "ARMv4T (GBA)"
},
{
"value": "v5t",
"name": "ARMv5T"
},
{
"value": "v5te",
"name": "ARMv5TE (DS)"
},
{
"value": "v5tej",
"name": "ARMv5TEJ"
},
{
"value": "v6",
"name": "ARMv6"
},
{
"value": "v6k",
"name": "ARMv6K (3DS)"
}
]
},
{
"id": "arm.vfpV2",
"type": "boolean",
"default": true,
"name": "VFPv2 instructions",
"description": "Adds floating-point instructions from VFPv2."
},
{
"id": "arm.unifiedSyntax",
"type": "boolean",
@@ -296,6 +340,7 @@
"properties": [
"functionRelocDiffs",
"demangler",
"showSymbolSizes",
"spaceBetweenArgs",
"combineDataSections",
"combineTextSections"
@@ -306,6 +351,7 @@
"name": "ARM",
"properties": [
"arm.archVersion",
"arm.vfpV2",
"arm.unifiedSyntax",
"arm.avRegisters",
"arm.r9Usage",
File diff suppressed because it is too large Load Diff
+2
View File
@@ -293,6 +293,7 @@ impl Arch for ArchMips {
}
elf::R_MIPS_PC16 => 0, // PC-relative relocation
R_MIPS15_S3 => ((code & 0x001FFFC0) >> 3) as i64,
elf::R_MIPS_GPREL32 => (code as i32 as i64) + self.ri_gp_value as i64,
flags => bail!("Unsupported MIPS implicit relocation {flags:?}"),
};
Ok(Some(RelocationOverride { target: RelocationOverrideTarget::Keep, addend }))
@@ -318,6 +319,7 @@ impl Arch for ArchMips {
elf::R_MIPS_GOT16 => Some("R_MIPS_GOT16"),
elf::R_MIPS_PC16 => Some("R_MIPS_PC16"),
elf::R_MIPS_CALL16 => Some("R_MIPS_CALL16"),
elf::R_MIPS_GPREL32 => Some("R_MIPS_GPREL32"),
R_MIPS15_S3 => Some("R_MIPS15_S3"),
_ => None,
},
+57 -23
View File
@@ -7,12 +7,10 @@ use alloc::{
};
use core::{
any::Any,
ffi::CStr,
fmt::{self, Debug},
};
use anyhow::{Result, bail};
use encoding_rs::SHIFT_JIS;
use object::Endian as _;
use crate::{
@@ -44,6 +42,16 @@ pub mod x86;
pub const OPCODE_INVALID: u16 = u16::MAX;
pub const OPCODE_DATA: u16 = u16::MAX - 1;
const SUPPORTED_ENCODINGS: [(&encoding_rs::Encoding, &str); 7] = [
(encoding_rs::UTF_8, "UTF-8"),
(encoding_rs::SHIFT_JIS, "Shift JIS"),
(encoding_rs::UTF_16BE, "UTF-16BE"),
(encoding_rs::UTF_16LE, "UTF-16LE"),
(encoding_rs::WINDOWS_1252, "Windows-1252"),
(encoding_rs::EUC_JP, "EUC-JP"),
(encoding_rs::BIG5, "Big5"),
];
/// Represents the type of data associated with an instruction
#[derive(PartialEq)]
pub enum DataType {
@@ -75,9 +83,9 @@ impl fmt::Display for DataType {
impl DataType {
pub fn display_labels(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<String> {
let mut strs = Vec::new();
for (literal, label_override) in self.display_literals(endian, bytes) {
for (literal, label_override, _) in self.display_literals(endian, bytes) {
let label = label_override.unwrap_or_else(|| self.to_string());
strs.push(format!("{label}: {literal}"))
strs.push(format!("{label}: {literal:?}"))
}
strs
}
@@ -86,7 +94,7 @@ impl DataType {
&self,
endian: object::Endianness,
bytes: &[u8],
) -> Vec<(String, Option<String>)> {
) -> Vec<(String, Option<String>, Option<String>)> {
let mut strs = Vec::new();
if self.required_len().is_some_and(|l| bytes.len() < l) {
log::warn!(
@@ -110,34 +118,34 @@ impl DataType {
match self {
DataType::Int8 => {
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None));
strs.push((format!("{i:#x}"), None, None));
if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None));
strs.push((format!("{:#x}", ReallySigned(i)), None, None));
}
}
DataType::Int16 => {
let i = endian.read_i16_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None));
strs.push((format!("{i:#x}"), None, None));
if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None));
strs.push((format!("{:#x}", ReallySigned(i)), None, None));
}
}
DataType::Int32 => {
let i = endian.read_i32_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None));
strs.push((format!("{i:#x}"), None, None));
if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None));
strs.push((format!("{:#x}", ReallySigned(i)), None, None));
}
}
DataType::Int64 => {
let i = endian.read_i64_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None));
strs.push((format!("{i:#x}"), None, None));
if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None));
strs.push((format!("{:#x}", ReallySigned(i)), None, None));
}
}
DataType::Float => {
@@ -148,6 +156,7 @@ impl DataType {
object::Endianness::Big => f32::from_be_bytes(bytes),
}),
None,
None,
));
}
DataType::Double => {
@@ -158,22 +167,29 @@ impl DataType {
object::Endianness::Big => f64::from_be_bytes(bytes),
}),
None,
None,
));
}
DataType::Bytes => {
strs.push((format!("{bytes:#?}"), None));
strs.push((format!("{bytes:#?}"), None, None));
}
DataType::String => {
if let Ok(cstr) = CStr::from_bytes_until_nul(bytes) {
strs.push((format!("{cstr:?}"), None));
}
if let Some(nul_idx) = bytes.iter().position(|&c| c == b'\0') {
let (cow, _, had_errors) = SHIFT_JIS.decode(&bytes[..nul_idx]);
if !had_errors {
let str = format!("{cow:?}");
// Only add the Shift JIS string if it's different from the ASCII string.
if !strs.iter().any(|x| x.0 == str) {
strs.push((str, Some("Shift JIS".into())));
let str_bytes = &bytes[..nul_idx];
// Special case to display (ASCII) as the label for ASCII-only strings.
let (cow, _, had_errors) = encoding_rs::UTF_8.decode(str_bytes);
if !had_errors && cow.is_ascii() {
let string = format!("{cow}");
let copy_string = escape_special_ascii_characters(string.clone());
strs.push((string, Some("ASCII".into()), Some(copy_string)));
}
for (encoding, encoding_name) in SUPPORTED_ENCODINGS {
let (cow, _, had_errors) = encoding.decode(str_bytes);
// Avoid showing ASCII-only strings more than once if the encoding is ASCII-compatible.
if !had_errors && (!encoding.is_ascii_compatible() || !cow.is_ascii()) {
let string = format!("{cow}");
let copy_string = escape_special_ascii_characters(string.clone());
strs.push((string, Some(encoding_name.into()), Some(copy_string)));
}
}
}
@@ -489,3 +505,21 @@ pub struct RelocationOverride {
pub target: RelocationOverrideTarget,
pub addend: i64,
}
/// Escape ASCII characters such as \n or \t, but not Unicode characters such as \u{3000}.
/// Suitable for copying to clipboard.
fn escape_special_ascii_characters(value: String) -> String {
let mut escaped = String::new();
escaped.push('"');
for c in value.chars() {
if c.is_ascii() {
for e in c.escape_default() {
escaped.push(e);
}
} else {
escaped.push(c);
}
}
escaped.push('"');
escaped
}
+6 -3
View File
@@ -332,7 +332,6 @@ impl Arch for ArchPpc {
pe::IMAGE_REL_PPC_PAIR => Some("IMAGE_REL_PPC_PAIR"),
_ => None,
},
_ => None,
}
}
@@ -441,9 +440,13 @@ impl Arch for ArchPpc {
let simplified = ins.simplified().to_string();
let show_orig = orig != simplified;
let mut out = Vec::with_capacity(2);
out.push(ContextItem::Copy { value: simplified, label: None });
out.push(ContextItem::Copy { value: simplified, label: None, copy_string: None });
if show_orig {
out.push(ContextItem::Copy { value: orig, label: Some("original".to_string()) });
out.push(ContextItem::Copy {
value: orig,
label: Some("original".to_string()),
copy_string: None,
});
}
out
}
-2
View File
@@ -72,7 +72,6 @@ impl ArchX86 {
elf::R_386_16 => Some(2),
_ => None,
},
RelocationFlags::Omf() => Some(4), // TODO
},
Architecture::X86_64 => match flags {
RelocationFlags::Coff(typ) => match typ {
@@ -85,7 +84,6 @@ impl ArchX86 {
elf::R_X86_64_64 => Some(8),
_ => None,
},
RelocationFlags::Omf() => None, // TODO
},
}
}
+20 -5
View File
@@ -36,13 +36,27 @@ pub fn diff_bss_symbol(
}
pub fn symbol_name_matches(left_name: &str, right_name: &str) -> bool {
// Match Metrowerks symbol$1234 against symbol$2345
if let Some((prefix, suffix)) = left_name.split_once('$') {
if !suffix.chars().all(char::is_numeric) {
return false;
if let Some((left_prefix, left_suffix)) = left_name.split_once("@class$")
&& let Some((right_prefix, right_suffix)) = right_name.split_once("@class$")
{
// Match Metrowerks anonymous class symbol names, ignoring the unique ID.
// e.g. __dt__Q29dCamera_c23@class$3665d_camera_cppFv
if left_prefix == right_prefix
&& let Some(left_idx) = left_suffix.chars().position(|c| !c.is_numeric())
&& let Some(right_idx) = right_suffix.chars().position(|c| !c.is_numeric())
{
// e.g. d_camera_cppFv (after the unique ID)
left_suffix[left_idx..] == right_suffix[right_idx..]
} else {
false
}
} else if let Some((prefix, suffix)) = left_name.split_once(['$', '.'])
&& suffix.chars().all(char::is_numeric)
{
// Match Metrowerks symbol$1234 against symbol$2345
// and GCC symbol.1234 against symbol.2345
right_name
.split_once('$')
.split_once(['$', '.'])
.is_some_and(|(p, s)| p == prefix && s.chars().all(char::is_numeric))
} else {
left_name == right_name
@@ -587,6 +601,7 @@ fn symbols_matching_section(
s.section == Some(section_idx)
&& s.kind != SymbolKind::Section
&& s.size > 0
&& !s.flags.contains(SymbolFlag::Hidden)
&& !s.flags.contains(SymbolFlag::Ignored)
})
}
+2 -4
View File
@@ -34,14 +34,12 @@ impl Demangler {
fn demangle_itanium(name: &str) -> Option<String> {
let name = name.trim_start_matches('.');
cpp_demangle::Symbol::new(name)
.ok()
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
cpp_demangle::Symbol::new(name).ok().and_then(|s| s.demangle().ok())
}
fn demangle_gnu_legacy(name: &str) -> Option<String> {
let name = name.trim_start_matches('.');
gnuv2_demangle::demangle(name, &gnuv2_demangle::DemangleConfig::new_no_cfilt_mimics()).ok()
gnuv2_demangle::demangle(name, &gnuv2_demangle::DemangleConfig::new()).ok()
}
}
+40 -14
View File
@@ -37,6 +37,8 @@ pub enum DiffText<'a> {
Argument(InstructionArgValue<'a>),
/// Branch destination
BranchDest(u64),
/// Branch source/destination arrow, scrolls to a specific row when clicked
BranchArrow(u32),
/// Symbol name
Symbol(&'a Symbol),
/// Relocation addend
@@ -185,7 +187,13 @@ pub fn display_row(
pad_to: 5,
})?;
if let Some(branch) = &ins_row.branch_from {
cb(DiffTextSegment::basic(" ~> ", DiffTextColor::Rotating(branch.branch_idx as u8)))?;
// Pick the first branch source if there are multiple.
let ins_idx = branch.ins_idx[0];
cb(DiffTextSegment {
text: DiffText::BranchArrow(ins_idx),
color: DiffTextColor::Rotating(branch.branch_idx as u8),
pad_to: 0,
})?;
} else {
cb(DiffTextSegment::spacing(4))?;
}
@@ -307,7 +315,11 @@ pub fn display_row(
cb(DiffTextSegment::basic(">", base_color))?;
}
if let Some(branch) = &ins_row.branch_to {
cb(DiffTextSegment::basic(" ~>", DiffTextColor::Rotating(branch.branch_idx as u8)))?;
cb(DiffTextSegment {
text: DiffText::BranchArrow(branch.ins_idx),
color: DiffTextColor::Rotating(branch.branch_idx as u8),
pad_to: 0,
})?;
}
cb(EOL_SEGMENT)?;
Ok(())
@@ -354,7 +366,7 @@ impl From<&DiffText<'_>> for HighlightKind {
}
pub enum ContextItem {
Copy { value: String, label: Option<String> },
Copy { value: String, label: Option<String>, copy_string: Option<String> },
Navigate { label: String, symbol_index: usize, kind: SymbolNavigationKind },
Separator,
}
@@ -386,9 +398,9 @@ pub fn symbol_context(obj: &Object, symbol_index: usize) -> Vec<ContextItem> {
return Vec::new();
};
let mut out = Vec::new();
out.push(ContextItem::Copy { value: symbol.name.clone(), label: None });
out.push(ContextItem::Copy { value: symbol.name.clone(), label: None, copy_string: None });
if let Some(name) = &symbol.demangled_name {
out.push(ContextItem::Copy { value: name.clone(), label: None });
out.push(ContextItem::Copy { value: name.clone(), label: None, copy_string: None });
}
if symbol.section.is_some()
&& let Some(address) = symbol.virtual_address
@@ -396,6 +408,7 @@ pub fn symbol_context(obj: &Object, symbol_index: usize) -> Vec<ContextItem> {
out.push(ContextItem::Copy {
value: format!("{address:x}"),
label: Some("virtual address".to_string()),
copy_string: None,
});
}
out.append(&mut obj.arch.symbol_context(obj, symbol_index));
@@ -489,8 +502,8 @@ pub fn relocation_context(
let literals = display_ins_data_literals(obj, ins);
if !literals.is_empty() {
out.push(ContextItem::Separator);
for (literal, label_override) in literals {
out.push(ContextItem::Copy { value: literal, label: label_override });
for (literal, label_override, copy_string) in literals {
out.push(ContextItem::Copy { value: literal, label: label_override, copy_string });
}
}
}
@@ -586,24 +599,37 @@ pub fn instruction_context(
for byte in resolved.code {
hex_string.push_str(&format!("{byte:02x}"));
}
out.push(ContextItem::Copy { value: hex_string, label: Some("instruction bytes".to_string()) });
out.push(ContextItem::Copy {
value: hex_string,
label: Some("instruction bytes".to_string()),
copy_string: None,
});
out.append(&mut obj.arch.instruction_context(obj, resolved));
if let Some(virtual_address) = resolved.symbol.virtual_address {
let offset = resolved.ins_ref.address - resolved.symbol.address;
out.push(ContextItem::Copy {
value: format!("{:x}", virtual_address + offset),
label: Some("virtual address".to_string()),
copy_string: None,
});
}
for arg in &ins.args {
if let InstructionArg::Value(arg) = arg {
out.push(ContextItem::Copy { value: arg.to_string(), label: None });
out.push(ContextItem::Copy { value: arg.to_string(), label: None, copy_string: None });
match arg {
InstructionArgValue::Signed(v) => {
out.push(ContextItem::Copy { value: v.to_string(), label: None });
out.push(ContextItem::Copy {
value: v.to_string(),
label: None,
copy_string: None,
});
}
InstructionArgValue::Unsigned(v) => {
out.push(ContextItem::Copy { value: v.to_string(), label: None });
out.push(ContextItem::Copy {
value: v.to_string(),
label: None,
copy_string: None,
});
}
_ => {}
}
@@ -665,10 +691,10 @@ pub fn instruction_hover(
let literals = display_ins_data_literals(obj, resolved);
if !literals.is_empty() {
out.push(HoverItem::Separator);
for (literal, label_override) in literals {
for (literal, label_override, _) in literals {
out.push(HoverItem::Text {
label: label_override.unwrap_or_else(|| ty.to_string()),
value: literal,
value: format!("{literal:?}"),
color: HoverItemColor::Normal,
});
}
@@ -859,7 +885,7 @@ pub fn display_ins_data_labels(obj: &Object, resolved: ResolvedInstructionRef) -
pub fn display_ins_data_literals(
obj: &Object,
resolved: ResolvedInstructionRef,
) -> Vec<(String, Option<String>)> {
) -> Vec<(String, Option<String>, Option<String>)> {
let Some(reloc) = resolved.relocation else {
return Vec::new();
};
+11 -12
View File
@@ -7,6 +7,7 @@ use alloc::{
use core::{num::NonZeroU32, ops::Range};
use anyhow::Result;
use itertools::Itertools;
use crate::{
diff::{
@@ -759,18 +760,16 @@ fn find_symbol(
return closest_match_symbol_idx;
}
// Try to find an exact name match
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|(_, symbol)| {
symbol.name == in_symbol.name && symbol_section_kind(obj, symbol) == section_kind
}) {
return Some(symbol_idx);
}
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|&(_, symbol)| {
symbol_name_matches(&in_symbol.name, &symbol.name)
&& symbol_section_kind(obj, symbol) == section_kind
&& symbol_section(obj, symbol).is_some_and(|(name, _)| name == section_name)
}) {
// Try to find a symbol with a matching name
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used)
.filter(|&(_, symbol)| {
symbol_name_matches(&in_symbol.name, &symbol.name)
&& symbol_section_kind(obj, symbol) == section_kind
&& symbol_section(obj, symbol).is_some_and(|(name, _)| name == section_name)
})
.sorted_unstable_by_key(|&(_, symbol)| (symbol.section, symbol.address))
.next()
{
return Some(symbol_idx);
}
+401
View File
@@ -0,0 +1,401 @@
use alloc::vec::Vec;
use anyhow::{Context, Result, bail, ensure};
use object::{Endianness, Object, ObjectSection};
use super::{Section, SectionKind};
const HDRR_SIZE: usize = 0x60;
const FDR_SIZE: usize = 0x48;
const PDR_SIZE: usize = 0x34;
const SYMR_SIZE: usize = 0x0c;
const ST_PROC: u8 = 6;
const ST_STATICPROC: u8 = 14;
const ST_END: u8 = 8;
pub(super) fn parse_line_info_mdebug(
obj_file: &object::File,
sections: &mut [Section],
) -> Result<()> {
let Some(section) = obj_file.section_by_name(".mdebug") else {
return Ok(());
};
let data = section.data().context("failed to read .mdebug contents")?;
if data.len() < HDRR_SIZE {
return Ok(());
}
let section_file_offset = section.file_range().map(|(offset, _)| offset as usize);
let endianness = obj_file.endianness();
let header = Header::parse(data, endianness)?;
let symbols_data = slice_at(
data,
header.cb_sym_offset,
header.isym_max.checked_mul(SYMR_SIZE as u32).context("symbol table size overflow")?,
section_file_offset,
)?;
let symbols = parse_symbols(symbols_data, endianness)?;
let fdr_data = slice_at(
data,
header.cb_fd_offset,
header
.ifd_max
.checked_mul(FDR_SIZE as u32)
.context("file descriptor table size overflow")?,
section_file_offset,
)?;
for fdr_index in 0..header.ifd_max as usize {
let fdr_offset = fdr_index * FDR_SIZE;
let fdr = FileDescriptor::parse(&fdr_data[fdr_offset..fdr_offset + FDR_SIZE], endianness)?;
if fdr.cpd == 0 || fdr.csym == 0 {
continue;
}
let sym_base = fdr.isym_base as usize;
let sym_end = sym_base + fdr.csym as usize;
if sym_end > symbols.len() {
continue;
}
let Some(line_file_offset) = header.cb_line_offset.checked_add(fdr.cb_line_offset) else {
continue;
};
let Some(line_file_base) =
resolve_offset(line_file_offset, data.len(), section_file_offset)
else {
continue;
};
let Some(line_file_end) = line_file_base.checked_add(fdr.cb_line as usize) else {
continue;
};
if line_file_end > data.len() {
continue;
}
for local_proc_index in 0..fdr.cpd as usize {
let pdr_index = fdr.ipd_first as usize + local_proc_index;
let pdr_offset = header
.cb_pd_offset
.checked_add((pdr_index as u32) * PDR_SIZE as u32)
.context("procedure descriptor offset overflow")?;
let pdr_data = match slice_at(data, pdr_offset, PDR_SIZE as u32, section_file_offset) {
Ok(data) => data,
Err(_) => continue,
};
let pdr = ProcDescriptor::parse(pdr_data, endianness)?;
if pdr.isym as usize >= fdr.csym as usize {
continue;
}
let global_sym_index = sym_base + pdr.isym as usize;
let Some(start_symbol) = symbols.get(global_sym_index) else {
continue;
};
if start_symbol.st != ST_PROC && start_symbol.st != ST_STATICPROC {
continue;
}
let local_index = pdr.isym as u32;
let mut end_address = None;
for sym in &symbols[global_sym_index..sym_end] {
if sym.st == ST_END && sym.index == local_index {
end_address = Some(sym.value);
break;
}
}
let Some(end_address) = end_address else {
continue;
};
let Some(size) = end_address.checked_sub(start_symbol.value) else {
continue;
};
if size == 0 || size % 4 != 0 {
continue;
}
let word_count = (size / 4) as usize;
if word_count == 0 {
continue;
}
let Some(mut cursor) = line_file_base.checked_add(pdr.cb_line_offset as usize) else {
continue;
};
if cursor >= line_file_end {
continue;
}
let mut line_number = pdr.ln_low as i32;
let mut lines = Vec::with_capacity(word_count);
while lines.len() < word_count && cursor < line_file_end {
let b0 = data[cursor];
cursor += 1;
let count = (b0 & 0x0f) as usize + 1;
let delta = decode_delta(endianness, b0 >> 4, data, &mut cursor, line_file_end)?;
line_number = line_number.wrapping_add(delta as i32);
for _ in 0..count {
if lines.len() == word_count {
break;
}
lines.push(line_number);
}
}
if lines.len() != word_count {
continue;
}
assign_lines(sections, fdr.adr as u64 + pdr.addr as u64, &lines);
}
}
Ok(())
}
fn assign_lines(sections: &mut [Section], base_address: u64, lines: &[i32]) {
let mut address = base_address;
for &line in lines {
if line >= 0
&& let Some(section) = find_code_section(sections, address)
{
section.line_info.insert(address, line as u32);
}
address = address.wrapping_add(4);
}
}
fn find_code_section(sections: &mut [Section], address: u64) -> Option<&mut Section> {
sections.iter_mut().find(|section| {
section.kind == SectionKind::Code
&& address >= section.address
&& address < section.address + section.size
})
}
fn decode_delta(
endianness: Endianness,
nibble: u8,
data: &[u8],
cursor: &mut usize,
end: usize,
) -> Result<i32> {
if nibble == 8 {
ensure!(*cursor + 2 <= end, "extended delta out of range");
let bytes: [u8; 2] = data[*cursor..*cursor + 2].try_into().unwrap();
*cursor += 2;
Ok(match endianness {
Endianness::Big => i16::from_be_bytes(bytes) as i32,
Endianness::Little => i16::from_le_bytes(bytes) as i32,
})
} else {
let mut value = (nibble & 0x0f) as i32;
if value & 0x8 != 0 {
value -= 0x10;
}
Ok(value)
}
}
fn slice_at(
data: &[u8],
offset: u32,
size: u32,
section_file_offset: Option<usize>,
) -> Result<&[u8]> {
let size = size as usize;
if size == 0 {
ensure!(
resolve_offset(offset, data.len(), section_file_offset).is_some(),
"offset outside of .mdebug section"
);
return Ok(&data[0..0]);
}
let Some(offset) = resolve_offset(offset, data.len(), section_file_offset) else {
bail!("offset outside of .mdebug section");
};
let end = offset.checked_add(size).context("range overflow")?;
ensure!(end <= data.len(), "range exceeds .mdebug size");
Ok(&data[offset..end])
}
fn resolve_offset(
offset: u32,
data_len: usize,
section_file_offset: Option<usize>,
) -> Option<usize> {
let offset = offset as usize;
if offset <= data_len {
Some(offset)
} else if let Some(file_offset) = section_file_offset {
offset.checked_sub(file_offset).filter(|rel| *rel <= data_len)
} else {
None
}
}
#[derive(Clone, Copy)]
struct Header {
cb_line_offset: u32,
cb_pd_offset: u32,
cb_sym_offset: u32,
cb_fd_offset: u32,
isym_max: u32,
ifd_max: u32,
}
impl Header {
fn parse(data: &[u8], endianness: Endianness) -> Result<Self> {
ensure!(HDRR_SIZE <= data.len(), ".mdebug header truncated");
let mut cursor = 0;
let magic = read_u16(data, &mut cursor, endianness)?;
let _vstamp = read_u16(data, &mut cursor, endianness)?;
ensure!(magic == 0x7009, "unexpected .mdebug magic: {magic:#x}");
let _iline_max = read_u32(data, &mut cursor, endianness)?;
let _cb_line = read_u32(data, &mut cursor, endianness)?;
let cb_line_offset = read_u32(data, &mut cursor, endianness)?;
let _idn_max = read_u32(data, &mut cursor, endianness)?;
let _cb_dn_offset = read_u32(data, &mut cursor, endianness)?;
let _ipd_max = read_u32(data, &mut cursor, endianness)?;
let cb_pd_offset = read_u32(data, &mut cursor, endianness)?;
let isym_max = read_u32(data, &mut cursor, endianness)?;
let cb_sym_offset = read_u32(data, &mut cursor, endianness)?;
let _iopt_max = read_u32(data, &mut cursor, endianness)?;
let _cb_opt_offset = read_u32(data, &mut cursor, endianness)?;
let _iaux_max = read_u32(data, &mut cursor, endianness)?;
let _cb_aux_offset = read_u32(data, &mut cursor, endianness)?;
let _iss_max = read_u32(data, &mut cursor, endianness)?;
let _cb_ss_offset = read_u32(data, &mut cursor, endianness)?;
let _iss_ext_max = read_u32(data, &mut cursor, endianness)?;
let _cb_ss_ext_offset = read_u32(data, &mut cursor, endianness)?;
let ifd_max = read_u32(data, &mut cursor, endianness)?;
let cb_fd_offset = read_u32(data, &mut cursor, endianness)?;
let _crfd = read_u32(data, &mut cursor, endianness)?;
let _cb_rfd_offset = read_u32(data, &mut cursor, endianness)?;
let _iext_max = read_u32(data, &mut cursor, endianness)?;
let _cb_ext_offset = read_u32(data, &mut cursor, endianness)?;
Ok(Header { cb_line_offset, cb_pd_offset, cb_sym_offset, cb_fd_offset, isym_max, ifd_max })
}
}
#[derive(Clone, Copy)]
struct FileDescriptor {
adr: u32,
isym_base: u32,
csym: u32,
ipd_first: u16,
cpd: u16,
cb_line_offset: u32,
cb_line: u32,
}
impl FileDescriptor {
fn parse(data: &[u8], endianness: Endianness) -> Result<Self> {
ensure!(data.len() >= FDR_SIZE, "FDR truncated");
let mut cursor = 0;
let adr = read_u32(data, &mut cursor, endianness)?;
let _rss = read_u32(data, &mut cursor, endianness)?;
let _iss_base = read_u32(data, &mut cursor, endianness)?;
let _cb_ss = read_u32(data, &mut cursor, endianness)?;
let isym_base = read_u32(data, &mut cursor, endianness)?;
let csym = read_u32(data, &mut cursor, endianness)?;
let _iline_base = read_u32(data, &mut cursor, endianness)?;
let _cline = read_u32(data, &mut cursor, endianness)?;
let _iopt_base = read_u32(data, &mut cursor, endianness)?;
let _copt = read_u32(data, &mut cursor, endianness)?;
let ipd_first = read_u16(data, &mut cursor, endianness)?;
let cpd = read_u16(data, &mut cursor, endianness)?;
let _iaux_base = read_u32(data, &mut cursor, endianness)?;
let _caux = read_u32(data, &mut cursor, endianness)?;
let _rfd_base = read_u32(data, &mut cursor, endianness)?;
let _crfd = read_u32(data, &mut cursor, endianness)?;
let _bits = read_u32(data, &mut cursor, endianness)?;
let cb_line_offset = read_u32(data, &mut cursor, endianness)?;
let cb_line = read_u32(data, &mut cursor, endianness)?;
Ok(FileDescriptor { adr, isym_base, csym, ipd_first, cpd, cb_line_offset, cb_line })
}
}
#[derive(Clone, Copy)]
struct ProcDescriptor {
addr: u32,
isym: u32,
ln_low: i32,
cb_line_offset: u32,
}
impl ProcDescriptor {
fn parse(data: &[u8], endianness: Endianness) -> Result<Self> {
ensure!(data.len() >= PDR_SIZE, "PDR truncated");
let mut cursor = 0;
let addr = read_u32(data, &mut cursor, endianness)?;
let isym = read_u32(data, &mut cursor, endianness)?;
let _iline = read_u32(data, &mut cursor, endianness)?;
let _regmask = read_u32(data, &mut cursor, endianness)?;
let _regoffset = read_u32(data, &mut cursor, endianness)?;
let _iopt = read_u32(data, &mut cursor, endianness)?;
let _fregmask = read_u32(data, &mut cursor, endianness)?;
let _fregoffset = read_u32(data, &mut cursor, endianness)?;
let _frameoffset = read_u32(data, &mut cursor, endianness)?;
let _framereg = read_u16(data, &mut cursor, endianness)?;
let _pcreg = read_u16(data, &mut cursor, endianness)?;
let ln_low = read_i32(data, &mut cursor, endianness)?;
let _ln_high = read_i32(data, &mut cursor, endianness)?;
let cb_line_offset = read_u32(data, &mut cursor, endianness)?;
Ok(ProcDescriptor { addr, isym, ln_low, cb_line_offset })
}
}
#[derive(Clone, Copy)]
struct SymbolEntry {
value: u32,
st: u8,
index: u32,
}
fn parse_symbols(data: &[u8], endianness: Endianness) -> Result<Vec<SymbolEntry>> {
ensure!(data.len().is_multiple_of(SYMR_SIZE), "symbol table misaligned");
let mut symbols = Vec::with_capacity(data.len() / SYMR_SIZE);
let mut cursor = 0;
while cursor + SYMR_SIZE <= data.len() {
let _iss = read_u32(data, &mut cursor, endianness)?;
let value = read_u32(data, &mut cursor, endianness)?;
let bits = read_u32(data, &mut cursor, endianness)?;
let (st, index) = match endianness {
Endianness::Big => (((bits >> 26) & 0x3f) as u8, bits & 0x000f_ffff),
Endianness::Little => (((bits & 0x3f) as u8), (bits >> 12) & 0x000f_ffff),
};
symbols.push(SymbolEntry { value, st, index });
}
Ok(symbols)
}
fn read_u16(data: &[u8], cursor: &mut usize, endianness: Endianness) -> Result<u16> {
ensure!(*cursor + 2 <= data.len(), "unexpected EOF while reading u16");
let bytes: [u8; 2] = data[*cursor..*cursor + 2].try_into().unwrap();
*cursor += 2;
Ok(match endianness {
Endianness::Big => u16::from_be_bytes(bytes),
Endianness::Little => u16::from_le_bytes(bytes),
})
}
fn read_u32(data: &[u8], cursor: &mut usize, endianness: Endianness) -> Result<u32> {
ensure!(*cursor + 4 <= data.len(), "unexpected EOF while reading u32");
let bytes: [u8; 4] = data[*cursor..*cursor + 4].try_into().unwrap();
*cursor += 4;
Ok(match endianness {
Endianness::Big => u32::from_be_bytes(bytes),
Endianness::Little => u32::from_le_bytes(bytes),
})
}
fn read_i32(data: &[u8], cursor: &mut usize, endianness: Endianness) -> Result<i32> {
Ok(read_u32(data, cursor, endianness)? as i32)
}
+1 -1
View File
@@ -1,5 +1,6 @@
#[cfg(feature = "dwarf")]
mod dwarf2;
mod mdebug;
pub mod read;
pub mod split_meta;
@@ -370,7 +371,6 @@ pub struct Relocation {
pub enum RelocationFlags {
Elf(u32),
Coff(u16),
Omf(/* TODO */),
}
#[derive(Debug, Copy, Clone)]
+11 -9
View File
@@ -510,7 +510,6 @@ fn map_section_relocations(
let flags = match reloc.flags() {
object::RelocationFlags::Elf { r_type } => RelocationFlags::Elf(r_type),
object::RelocationFlags::Coff { typ } => RelocationFlags::Coff(typ),
object::RelocationFlags::Omf { .. } => RelocationFlags::Omf(/* TODO */),
flags => bail!("Unhandled relocation flags: {:?}", flags),
};
let target_symbol = match symbol_indices.get(symbol_index.0).copied() {
@@ -655,6 +654,10 @@ fn parse_line_info(
log::warn!("Failed to parse COFF line info: {e}");
}
if let Err(e) = super::mdebug::parse_line_info_mdebug(obj_file, sections) {
log::warn!("Failed to parse MIPS mdebug line info: {e}");
}
Ok(())
}
@@ -821,19 +824,16 @@ fn combine_sections(
config: &DiffObjConfig,
) -> Result<()> {
let mut data_sections = BTreeMap::<String, Vec<usize>>::new();
let mut text_sections = Vec::<usize>::new();
let mut text_sections = BTreeMap::<String, Vec<usize>>::new();
for (i, section) in sections.iter().enumerate() {
let base_name =
if let Some(i) = section.name.rfind('$') { &section.name[..i] } else { &section.name };
match section.kind {
SectionKind::Data | SectionKind::Bss => {
let base_name = if let Some(i) = section.name.rfind('$') {
&section.name[..i]
} else {
&section.name
};
data_sections.entry(base_name.to_string()).or_default().push(i);
}
SectionKind::Code => {
text_sections.push(i);
text_sections.entry(base_name.to_string()).or_default().push(i);
}
_ => {}
}
@@ -844,7 +844,9 @@ fn combine_sections(
}
}
if config.combine_text_sections {
do_combine_sections(sections, symbols, &mut text_sections, ".text".to_string())?;
for (combined_name, mut section_indices) in text_sections {
do_combine_sections(sections, symbols, &mut section_indices, combined_name)?;
}
}
Ok(())
}

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