Compare commits

...

16 Commits

Author SHA1 Message Date
LagoLunatic 02daae417c Version 3.7.2 2026-05-31 23:25:27 -04:00
LagoLunatic 8b1b4a9a79 Add config option to limit which string literal encodings are used (#356)
* Fix annoying rust-analyzer error with a cfg

* Refactor literal info into a struct

* Add field hide a literal from the GUI but still use for diffing

* Add config option to limit string literal diffing

* Fix deprecation warning `float_literal_f32_fallback`

* Hide non-preferred string encodings from the GUI

* Fix new cargo check warning

* Clippy again

* Rename "None" -> "Auto"
2026-05-31 23:23:10 -04:00
LagoLunatic 13f1267eac Fix several issues with string literal detection (#351)
* Support detecting more symbol names for strings

* Fix string detectiong being broken for encodings that use null terminators

* Add test cases for string detection and decoding

* Fix test helper function sometimes being considered dead code

* Clippy fixes
2026-05-03 21:27:12 -04:00
Luke Street 3cebee6766 Version 3.7.1 2026-03-21 09:54:09 -06:00
Amber Brault ab0538e9a4 Fix extab hover window display issue (#344)
* Bleh

* Fix formatting

* Update arch_ppc__read_extab.snap

* Put code in separate function

* Make compiler shut up

* Update mod.rs
2026-03-21 09:38:01 -06:00
Luke Street b643265809 Disable advisories check in CI 2026-03-20 23:29:54 -06:00
Bruno Macabeus 7a65e07e9b Add support for R_ARM_THM_PC8 and R_ARM_THM_PC11 (#342)
* Add support for `R_ARM_THM_JUMP8` and `R_ARM_THM_JUMP11`

* Fix `scan_instructions_internal` when reading 2-bytes instruction
2026-03-16 22:08:03 -06:00
Bruno Macabeus 3c4092b965 Fix cache issue when parsing the same file with different side params (#341) 2026-03-15 20:21:05 -06:00
Luke Street 0f599bea2b Version 3.7.0 2026-03-08 13:30:17 -06:00
Luke Street 9927fc35b6 Update time and bytes crates (resolves advisories) 2026-03-08 13:07:20 -06:00
Sebastián Meljem 16fb6a491f Demangle function name on decomp.me scratch creation (#329) 2026-03-08 11:14:24 -06:00
Luke Street 2541aef71a Add symbol kind and structured flags to diff proto output (#337)
* Add symbol kind to diff proto output

Expose SymbolKind (Function, Object, Section, Unknown) in the
DiffSymbol proto message, allowing CLI oneshot diff consumers to
distinguish symbol types without inferring from instruction/data
presence.

* Stop filtering symbols from diff output, replace flags bitmask with message

Remove the symbol filter (size == 0 / Ignored) from DiffObject so all
symbols are emitted. This fixes target_symbol indices being invalid
after filtering.

Replace the DiffSymbolFlag bitmask enum + uint32 field with a
DiffSymbolFlags message of booleans, adding ignored and size_inferred
flags so consumers can filter on their end.
2026-03-08 11:13:40 -06:00
First Last 7ad1f95bf7 Fix symbol index-to-addend bug (#336) 2026-02-21 21:15:53 -07:00
LagoLunatic 66c879a95d Version 3.6.1 (#328) 2026-01-30 05:40:02 +00:00
rjkiv d53c358d2f automatically match MSVC symbols in anonymous namespaces (#325)
* automatically match MSVC symbols in anonymous namespaces

* tweak logic for detecting anonymous MSVC symbols

* fix package format

* address feedback

* lazylock the regex

* formatting

* use once_cell instead

* formatting

* remove regex
2026-01-30 00:13:10 -05:00
Avery Townsend f24c017545 merge gnu function and data sections (#327) 2026-01-29 16:58:52 -07:00
30 changed files with 500 additions and 144 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ jobs:
strategy:
matrix:
checks:
- advisories
# - advisories
- bans licenses sources
# Prevent new advisories from failing CI
continue-on-error: ${{ matrix.checks == 'advisories' }}
Generated
+15 -15
View File
@@ -598,9 +598,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "calloop"
@@ -3157,9 +3157,9 @@ dependencies = [
[[package]]
name = "num-conv"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-traits"
@@ -3491,7 +3491,7 @@ dependencies = [
[[package]]
name = "objdiff-cli"
version = "3.6.0"
version = "3.7.2"
dependencies = [
"anyhow",
"argp",
@@ -3514,7 +3514,7 @@ dependencies = [
[[package]]
name = "objdiff-core"
version = "3.6.0"
version = "3.7.2"
dependencies = [
"anyhow",
"arm-attr",
@@ -3570,7 +3570,7 @@ dependencies = [
[[package]]
name = "objdiff-gui"
version = "3.6.0"
version = "3.7.2"
dependencies = [
"anyhow",
"argp",
@@ -3608,7 +3608,7 @@ dependencies = [
[[package]]
name = "objdiff-wasm"
version = "3.6.0"
version = "3.7.2"
dependencies = [
"log",
"objdiff-core",
@@ -5327,9 +5327,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.44"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
@@ -5337,22 +5337,22 @@ dependencies = [
"num-conv",
"num_threads",
"powerfmt",
"serde",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.6"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.24"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
+1 -1
View File
@@ -14,7 +14,7 @@ default-members = [
resolver = "3"
[workspace.package]
version = "3.6.0"
version = "3.7.2"
authors = ["Luke Street <luke@street.dev>"]
edition = "2024"
license = "MIT OR Apache-2.0"
+2 -2
View File
@@ -198,8 +198,8 @@ fn run_oneshot(
.transpose()?;
let result =
diff::diff_objs(target.as_ref(), base.as_ref(), None, &diff_config, &mapping_config)?;
let left = target.as_ref().and_then(|o| result.left.as_ref().map(|d| (o, d)));
let right = base.as_ref().and_then(|o| result.right.as_ref().map(|d| (o, d)));
let left = target.as_ref().zip(result.left.as_ref());
let right = base.as_ref().zip(result.right.as_ref());
let diff_result = DiffResult::new(left, right, &diff_config)?;
write_output(&diff_result, Some(output), output_format)?;
Ok(())
+46
View File
@@ -117,6 +117,51 @@
"name": "Combine text sections",
"description": "Combines all text sections into one."
},
{
"id": "preferredStringEncoding",
"type": "choice",
"default": "auto",
"name": "Preferred string encoding",
"description": "Which encoding to use when diffing string literals used in functions.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "ascii",
"name": "ASCII"
},
{
"value": "utf_8",
"name": "UTF-8"
},
{
"value": "shift_jis",
"name": "Shift JIS"
},
{
"value": "windows_1252",
"name": "Windows-1252"
},
{
"value": "euc_jp",
"name": "EUC-JP"
},
{
"value": "big5",
"name": "Big5"
},
{
"value": "utf_16be",
"name": "UTF-16BE"
},
{
"value": "utf_16le",
"name": "UTF-16LE"
}
]
},
{
"id": "arm.archVersion",
"type": "choice",
@@ -338,6 +383,7 @@
"id": "general",
"name": "General",
"properties": [
"preferredStringEncoding",
"functionRelocDiffs",
"demangler",
"showSymbolSizes",
+18 -9
View File
@@ -39,7 +39,8 @@ message DiffSymbol {
optional string demangled_name = 2;
uint64 address = 3;
uint64 size = 4;
uint32 flags = 5;
DiffSymbolFlags flags = 5;
DiffSymbolKind kind = 10;
// The symbol index in the _other_ object that this symbol was diffed against
optional uint32 target_symbol = 6;
optional float match_percent = 7;
@@ -49,14 +50,22 @@ message DiffSymbol {
repeated DiffDataSegment data_diff = 9;
}
// Symbol visibility flags (bitmask)
enum DiffSymbolFlag {
SYMBOL_NONE = 0;
SYMBOL_GLOBAL = 1;
SYMBOL_LOCAL = 2;
SYMBOL_WEAK = 4;
SYMBOL_COMMON = 8;
SYMBOL_HIDDEN = 16;
enum DiffSymbolKind {
SYMBOL_UNKNOWN = 0;
SYMBOL_FUNCTION = 1;
SYMBOL_OBJECT = 2;
SYMBOL_SECTION = 3;
}
// Symbol flags
message DiffSymbolFlags {
bool global = 1;
bool local = 2;
bool weak = 3;
bool common = 4;
bool hidden = 5;
bool ignored = 6;
bool size_inferred = 7;
}
// A single instruction diff row
Binary file not shown.
+27 -3
View File
@@ -162,7 +162,7 @@ impl ArchArm {
}
impl Arch for ArchArm {
fn post_init(&mut self, sections: &[Section], symbols: &[Symbol]) {
fn post_init(&mut self, sections: &[Section], symbols: &[Symbol], _symbol_indices: &[usize]) {
self.disasm_modes = Self::get_mapping_symbols(sections, symbols);
}
@@ -224,7 +224,9 @@ impl Arch for ArchArm {
}
// Check how many bytes we can/should read
let num_code_bytes = if data.len() >= 4 {
let bytes_until_next_mapping =
next_mapping.map(|m| (m.address - address) as usize).unwrap_or(usize::MAX);
let num_code_bytes = if data.len() >= 4 && bytes_until_next_mapping >= 4 {
if mode == unarm::ParseMode::Data && address & 3 != 0 {
// 32-bit .word value should be aligned on a 4-byte boundary, otherwise use .hword
2
@@ -366,6 +368,22 @@ impl Arch for ArchArm {
(imm22 << 1) << 9 >> 9
}
// Thumb unconditional branch (B, 11-bit offset)
elf::R_ARM_THM_PC11 => {
let data = section_data[address..address + 2].try_into()?;
let insn = self.endianness.read_u16_bytes(data) as i32;
let imm11 = insn & 0x7ff;
(imm11 << 1) << 20 >> 20
}
// Thumb conditional branch (B<cond>, 8-bit offset)
elf::R_ARM_THM_PC9 => {
let data = section_data[address..address + 2].try_into()?;
let insn = self.endianness.read_u16_bytes(data) as i32;
let imm8 = insn & 0xff;
(imm8 << 1) << 23 >> 23
}
// Data
elf::R_ARM_ABS32 => {
let data = section_data[address..address + 4].try_into()?;
@@ -399,6 +417,8 @@ impl Arch for ArchArm {
elf::R_ARM_PC24 => Some("R_ARM_PC24"),
elf::R_ARM_XPC25 => Some("R_ARM_XPC25"),
elf::R_ARM_CALL => Some("R_ARM_CALL"),
elf::R_ARM_THM_PC11 => Some("R_ARM_THM_PC11"),
elf::R_ARM_THM_PC9 => Some("R_ARM_THM_PC9"),
_ => None,
},
_ => None,
@@ -418,6 +438,8 @@ impl Arch for ArchArm {
elf::R_ARM_PC24 => 4,
elf::R_ARM_XPC25 => 4,
elf::R_ARM_CALL => 4,
elf::R_ARM_THM_PC11 => 2,
elf::R_ARM_THM_PC9 => 2,
_ => 1,
},
_ => 1,
@@ -544,7 +566,9 @@ impl unarm::FormatIns for ArgsFormatter<'_> {
| RelocationFlags::Elf(elf::R_ARM_THM_PC22)
| RelocationFlags::Elf(elf::R_ARM_PC24)
| RelocationFlags::Elf(elf::R_ARM_XPC25)
| RelocationFlags::Elf(elf::R_ARM_CALL) => {
| RelocationFlags::Elf(elf::R_ARM_CALL)
| RelocationFlags::Elf(elf::R_ARM_THM_PC11)
| RelocationFlags::Elf(elf::R_ARM_THM_PC9) => {
return self.write(InstructionPart::reloc());
}
_ => {}
+104 -52
View File
@@ -15,7 +15,7 @@ use object::Endian as _;
use crate::{
diff::{
DiffObjConfig, DiffSide,
ConfigEnum, DiffObjConfig, DiffSide, PreferredStringEncoding,
display::{ContextItem, HoverItem, InstructionPart},
},
obj::{
@@ -42,15 +42,42 @@ 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] = [
const SUPPORTED_ENCODINGS_WITH_NULL_TERM: [(&encoding_rs::Encoding, &str); 5] = [
(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"),
];
const SUPPORTED_ENCODINGS_NO_NULL_TERM: [(&encoding_rs::Encoding, &str); 2] =
[(encoding_rs::UTF_16BE, "UTF-16BE"), (encoding_rs::UTF_16LE, "UTF-16LE")];
#[derive(Debug, Clone, Default, PartialEq)]
pub struct LiteralInfo {
pub literal: String,
pub label_override: Option<String>,
pub copy_string: Option<String>,
pub hidden: bool, // Only used when the user hasn't set a preferred string encoding
pub is_string: bool,
}
impl LiteralInfo {
pub fn hidden(&self, diff_config: Option<&DiffObjConfig>) -> bool {
let Some(diff_config) = diff_config else {
return self.hidden;
};
if !self.is_string {
return self.hidden;
}
if diff_config.preferred_string_encoding == PreferredStringEncoding::Auto {
return self.hidden;
}
let Some(ref label) = self.label_override else {
return self.hidden;
};
*label != diff_config.preferred_string_encoding.name()
}
}
/// Represents the type of data associated with an instruction
#[derive(PartialEq)]
@@ -83,18 +110,14 @@ 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) {
let label = label_override.unwrap_or_else(|| self.to_string());
strs.push(format!("{label}: {literal:?}"))
for lit_info in self.display_literals(endian, bytes) {
let label = lit_info.label_override.unwrap_or_else(|| self.to_string());
strs.push(format!("{}: {:?}", label, lit_info.literal))
}
strs
}
pub fn display_literals(
&self,
endian: object::Endianness,
bytes: &[u8],
) -> Vec<(String, Option<String>, Option<String>)> {
pub fn display_literals(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<LiteralInfo> {
let mut strs = Vec::new();
if self.required_len().is_some_and(|l| bytes.len() < l) {
log::warn!(
@@ -118,80 +141,106 @@ impl DataType {
match self {
DataType::Int8 => {
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None, None));
strs.push(LiteralInfo { literal: format!("{i:#x}"), ..Default::default() });
if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None, None));
strs.push(LiteralInfo {
literal: format!("{:#x}", ReallySigned(i)),
..Default::default()
});
}
}
DataType::Int16 => {
let i = endian.read_i16_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None, None));
strs.push(LiteralInfo { literal: format!("{i:#x}"), ..Default::default() });
if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None, None));
strs.push(LiteralInfo {
literal: format!("{:#x}", ReallySigned(i)),
..Default::default()
});
}
}
DataType::Int32 => {
let i = endian.read_i32_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None, None));
strs.push(LiteralInfo { literal: format!("{i:#x}"), ..Default::default() });
if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None, None));
strs.push(LiteralInfo {
literal: format!("{:#x}", ReallySigned(i)),
..Default::default()
});
}
}
DataType::Int64 => {
let i = endian.read_i64_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None, None));
strs.push(LiteralInfo { literal: format!("{i:#x}"), ..Default::default() });
if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None, None));
strs.push(LiteralInfo {
literal: format!("{:#x}", ReallySigned(i)),
..Default::default()
});
}
}
DataType::Float => {
let bytes: [u8; 4] = bytes.try_into().unwrap();
strs.push((
format!("{:?}f", match endian {
strs.push(LiteralInfo {
literal: format!("{:?}f", match endian {
object::Endianness::Little => f32::from_le_bytes(bytes),
object::Endianness::Big => f32::from_be_bytes(bytes),
}),
None,
None,
));
..Default::default()
});
}
DataType::Double => {
let bytes: [u8; 8] = bytes.try_into().unwrap();
strs.push((
format!("{:?}", match endian {
strs.push(LiteralInfo {
literal: format!("{:?}", match endian {
object::Endianness::Little => f64::from_le_bytes(bytes),
object::Endianness::Big => f64::from_be_bytes(bytes),
}),
None,
None,
));
..Default::default()
});
}
DataType::Bytes => {
strs.push((format!("{bytes:#?}"), None, None));
strs.push(LiteralInfo { literal: format!("{bytes:#?}"), ..Default::default() });
}
DataType::String => {
// Special case to display (ASCII) as the label for ASCII-only strings.
let mut is_ascii = false;
if bytes.is_ascii()
&& let Ok(str) = str::from_utf8(bytes)
{
let trimmed = str.trim_end_matches('\0');
if !trimmed.is_empty() {
let copy_string = escape_special_ascii_characters(trimmed);
strs.push((trimmed.to_string(), Some("ASCII".into()), Some(copy_string)));
is_ascii = true;
if let Some(nul_idx) = bytes.iter().position(|&c| c == b'\0') {
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);
strs.push(LiteralInfo {
literal: string,
label_override: Some("ASCII".into()),
copy_string: Some(copy_string),
hidden: false,
is_string: true,
});
}
for (encoding, encoding_name) in SUPPORTED_ENCODINGS_WITH_NULL_TERM {
let (cow, _, had_errors) = encoding.decode(str_bytes);
if !had_errors {
let string = format!("{cow}");
let copy_string = escape_special_ascii_characters(&string);
// Avoid showing ASCII-only strings more than once if the encoding is ASCII-compatible.
let hidden = encoding.is_ascii_compatible() && cow.is_ascii();
strs.push(LiteralInfo {
literal: string,
label_override: Some(encoding_name.into()),
copy_string: Some(copy_string),
hidden,
is_string: true,
});
}
}
}
for (encoding, encoding_name) in SUPPORTED_ENCODINGS {
// Avoid showing ASCII-only strings more than once if the encoding is ASCII-compatible.
if is_ascii && encoding.is_ascii_compatible() {
continue;
}
for (encoding, encoding_name) in SUPPORTED_ENCODINGS_NO_NULL_TERM {
let (cow, _, had_errors) = encoding.decode(bytes);
if had_errors {
continue;
@@ -199,11 +248,13 @@ impl DataType {
let trimmed = cow.trim_end_matches('\0');
if !trimmed.is_empty() {
let copy_string = escape_special_ascii_characters(trimmed);
strs.push((
trimmed.to_string(),
Some(encoding_name.into()),
Some(copy_string),
));
strs.push(LiteralInfo {
literal: trimmed.to_string(),
label_override: Some(encoding_name.into()),
copy_string: Some(copy_string),
hidden: false,
is_string: true,
});
}
}
}
@@ -340,7 +391,8 @@ impl dyn Arch {
pub trait Arch: Any + Debug + Send + Sync {
/// Finishes arch-specific initialization that must be done after sections have been combined.
fn post_init(&mut self, _sections: &[Section], _symbols: &[Symbol]) {}
fn post_init(&mut self, _sections: &[Section], _symbols: &[Symbol], _symbol_indices: &[usize]) {
}
/// Generate a list of instructions references (offset, size, opcode) from the given code.
///
@@ -513,6 +513,8 @@ pub fn ppc_data_flow_analysis(
)
}
// Note: This function only supports MWCC ASCII strings.
// Other encodings and other compilers are currently not supported.
fn get_string_data(obj: &Object, symbol_index: usize, offset: Simm) -> Option<&str> {
if let Some(sym) = obj.symbols.get(symbol_index)
&& sym.name.starts_with("@stringBase")
+22 -4
View File
@@ -238,9 +238,9 @@ impl Arch for ArchPpc {
addend: 0,
})),
|(_, reloc)| match reloc.target() {
object::RelocationTarget::Symbol(index) => Ok(Some(RelocationOverride {
object::RelocationTarget::Symbol(_) => Ok(Some(RelocationOverride {
target: RelocationOverrideTarget::Keep,
addend: index.0 as u16 as i16 as i64,
addend: 0,
})),
target => Err(anyhow!("Unsupported IMAGE_REL_PPC_PAIR target {target:?}")),
},
@@ -359,9 +359,12 @@ impl Arch for ArchPpc {
fn guess_data_type(&self, resolved: ResolvedInstructionRef, bytes: &[u8]) -> Option<DataType> {
if resolved.relocation.is_some_and(|r| {
r.symbol.name.starts_with("@stringBase") || r.symbol.name.starts_with("$SG")
r.symbol.name.starts_with("@stringBase")
|| r.symbol.name.starts_with("@wstringBase")
|| r.symbol.name.starts_with("$SG")
|| r.symbol.demangled_name == Some("`string'".to_string())
}) {
// Pooled string.
// Compiler-generated symbol name for a string or a pool of strings.
return Some(DataType::String);
}
let opcode = powerpc::Opcode::from(resolved.ins_ref.opcode);
@@ -472,12 +475,27 @@ impl Arch for ArchPpc {
}
Ok(next_address.saturating_sub(symbol.address))
}
fn post_init(&mut self, _sections: &[Section], _symbols: &[Symbol], symbol_indices: &[usize]) {
// Change the indices used as keys from the original symbol indices to the new symbol array indices
self.extab = Self::convert_extab_map_indices(self, symbol_indices);
}
}
impl ArchPpc {
pub fn extab_for_symbol(&self, symbol_index: usize) -> Option<&ExceptionInfo> {
self.extab.as_ref()?.get(&symbol_index)
}
pub fn convert_extab_map_indices(
&self,
symbol_indices: &[usize],
) -> Option<BTreeMap<usize, ExceptionInfo>> {
let new_map: BTreeMap<usize, ExceptionInfo> =
self.extab.as_ref()?.iter().map(|e| (symbol_indices[*e.0 + 1], e.1.clone())).collect();
Some(new_map)
}
}
fn zero_reloc(code: u32, reloc: &Relocation) -> u32 {
+21 -20
View File
@@ -43,9 +43,6 @@ impl DiffObject {
let mut symbols = Vec::with_capacity(obj.symbols.len());
for (symbol_idx, symbol) in obj.symbols.iter().enumerate() {
let symbol_diff = &diff.symbols[symbol_idx];
if symbol.size == 0 || symbol.flags.contains(SymbolFlag::Ignored) {
continue;
}
symbols.push(DiffSymbol::new(obj, symbol_idx, symbol, symbol_diff, diff_config)?);
}
@@ -112,6 +109,7 @@ impl DiffSymbol {
address: symbol.address,
size: symbol.size,
flags: symbol_flags(&symbol.flags),
kind: DiffSymbolKind::from(symbol.kind) as i32,
// Diff information
target_symbol: symbol_diff.target_symbol.map(|i| i as u32),
match_percent: symbol_diff.match_percent,
@@ -121,24 +119,27 @@ impl DiffSymbol {
}
}
fn symbol_flags(flags: &obj::SymbolFlagSet) -> u32 {
let mut result = 0u32;
if flags.contains(SymbolFlag::Global) {
result |= DiffSymbolFlag::SymbolGlobal as u32;
impl From<obj::SymbolKind> for DiffSymbolKind {
fn from(value: obj::SymbolKind) -> Self {
match value {
obj::SymbolKind::Unknown => DiffSymbolKind::SymbolUnknown,
obj::SymbolKind::Function => DiffSymbolKind::SymbolFunction,
obj::SymbolKind::Object => DiffSymbolKind::SymbolObject,
obj::SymbolKind::Section => DiffSymbolKind::SymbolSection,
}
}
if flags.contains(SymbolFlag::Local) {
result |= DiffSymbolFlag::SymbolLocal as u32;
}
if flags.contains(SymbolFlag::Weak) {
result |= DiffSymbolFlag::SymbolWeak as u32;
}
if flags.contains(SymbolFlag::Common) {
result |= DiffSymbolFlag::SymbolCommon as u32;
}
if flags.contains(SymbolFlag::Hidden) {
result |= DiffSymbolFlag::SymbolHidden as u32;
}
result
}
fn symbol_flags(flags: &obj::SymbolFlagSet) -> Option<DiffSymbolFlags> {
Some(DiffSymbolFlags {
global: flags.contains(SymbolFlag::Global),
local: flags.contains(SymbolFlag::Local),
weak: flags.contains(SymbolFlag::Weak),
common: flags.contains(SymbolFlag::Common),
hidden: flags.contains(SymbolFlag::Hidden),
ignored: flags.contains(SymbolFlag::Ignored),
size_inferred: flags.contains(SymbolFlag::SizeInferred),
})
}
impl DiffInstructionRow {
+23 -4
View File
@@ -9,8 +9,8 @@ use anyhow::{Context, Result, anyhow, ensure};
use super::{
DiffObjConfig, FunctionRelocDiffs, InstructionArgDiffIndex, InstructionBranchFrom,
InstructionBranchTo, InstructionDiffKind, InstructionDiffRow, SymbolDiff,
display::display_ins_data_literals,
InstructionBranchTo, InstructionDiffKind, InstructionDiffRow, PreferredStringEncoding,
SymbolDiff, display::display_ins_data_literals,
};
use crate::obj::{
InstructionArg, InstructionArgValue, InstructionRef, Object, ResolvedInstructionRef,
@@ -296,6 +296,26 @@ pub(crate) fn section_name_eq(
})
}
fn ins_data_literals_eq(
left_obj: &Object,
right_obj: &Object,
left_ins: ResolvedInstructionRef,
right_ins: ResolvedInstructionRef,
diff_config: &DiffObjConfig,
) -> bool {
let mut left_literals = display_ins_data_literals(left_obj, left_ins);
let mut right_literals = display_ins_data_literals(right_obj, right_ins);
if left_literals == right_literals {
return true;
}
if diff_config.preferred_string_encoding == PreferredStringEncoding::Auto {
return left_literals == right_literals;
}
left_literals.retain(|lit_info| !lit_info.hidden(Some(diff_config)));
right_literals.retain(|lit_info| !lit_info.hidden(Some(diff_config)));
left_literals == right_literals
}
fn reloc_eq(
left_obj: &Object,
right_obj: &Object,
@@ -330,8 +350,7 @@ fn reloc_eq(
&& (diff_config.function_reloc_diffs == FunctionRelocDiffs::NameAddress
|| left_reloc.symbol.kind != SymbolKind::Object
|| right_reloc.symbol.size == 0 // Likely a pool symbol like ...data, don't treat this as a diff
|| display_ins_data_literals(left_obj, left_ins)
== display_ins_data_literals(right_obj, right_ins))
|| ins_data_literals_eq(left_obj, right_obj, left_ins, right_ins, diff_config))
}
(Some(_), None) | (None, Some(_)) | (None, None) => symbol_name_addend_matches,
}
+20 -10
View File
@@ -12,6 +12,7 @@ use itertools::Itertools;
use regex::Regex;
use crate::{
arch::LiteralInfo,
diff::{
DataDiffKind, DataDiffRow, DiffObjConfig, InstructionDiffKind, InstructionDiffRow,
ObjectDiff, SymbolDiff, data::resolve_relocation,
@@ -494,15 +495,21 @@ pub fn relocation_context(
obj: &Object,
reloc: ResolvedRelocation,
ins: Option<ResolvedInstructionRef>,
diff_config: Option<&DiffObjConfig>,
) -> Vec<ContextItem> {
let mut out = Vec::new();
out.append(&mut symbol_context(obj, reloc.relocation.target_symbol));
if let Some(ins) = ins {
let literals = display_ins_data_literals(obj, ins);
let mut literals = display_ins_data_literals(obj, ins);
literals.retain(|lit_info| !lit_info.hidden(diff_config));
if !literals.is_empty() {
out.push(ContextItem::Separator);
for (literal, label_override, copy_string) in literals {
out.push(ContextItem::Copy { value: literal, label: label_override, copy_string });
for lit_info in literals {
out.push(ContextItem::Copy {
value: lit_info.literal,
label: lit_info.label_override,
copy_string: lit_info.copy_string,
});
}
}
}
@@ -555,7 +562,7 @@ pub fn data_row_context(obj: &Object, diff_row: &DataDiffRow) -> Vec<ContextItem
prev_reloc = Some(reloc);
let reloc = resolve_relocation(&obj.symbols, reloc);
out.append(&mut relocation_context(obj, reloc, None));
out.append(&mut relocation_context(obj, reloc, None, None));
}
out
}
@@ -592,6 +599,7 @@ pub fn instruction_context(
obj: &Object,
resolved: ResolvedInstructionRef,
ins: &ParsedInstruction,
diff_config: &DiffObjConfig,
) -> Vec<ContextItem> {
let mut out = Vec::new();
let mut hex_string = String::new();
@@ -636,7 +644,7 @@ pub fn instruction_context(
}
if let Some(reloc) = resolved.relocation {
out.push(ContextItem::Separator);
out.append(&mut relocation_context(obj, reloc, Some(resolved)));
out.append(&mut relocation_context(obj, reloc, Some(resolved), Some(diff_config)));
}
out
}
@@ -645,6 +653,7 @@ pub fn instruction_hover(
obj: &Object,
resolved: ResolvedInstructionRef,
ins: &ParsedInstruction,
diff_config: &DiffObjConfig,
) -> Vec<HoverItem> {
let mut out = Vec::new();
out.push(HoverItem::Text {
@@ -687,13 +696,14 @@ pub fn instruction_hover(
out.append(&mut relocation_hover(obj, reloc, None));
let bytes = obj.symbol_data(reloc.relocation.target_symbol).unwrap_or(&[]);
if let Some(ty) = obj.arch.guess_data_type(resolved, bytes) {
let literals = display_ins_data_literals(obj, resolved);
let mut literals = display_ins_data_literals(obj, resolved);
literals.retain(|lit_info| !lit_info.hidden(Some(diff_config)));
if !literals.is_empty() {
out.push(HoverItem::Separator);
for (literal, label_override, _) in literals {
for lit_info in literals {
out.push(HoverItem::Text {
label: label_override.unwrap_or_else(|| ty.to_string()),
value: format!("{literal:?}"),
label: lit_info.label_override.unwrap_or_else(|| ty.to_string()),
value: format!("{:?}", lit_info.literal),
color: HoverItemColor::Normal,
});
}
@@ -880,7 +890,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>, Option<String>)> {
) -> Vec<LiteralInfo> {
let Some(reloc) = resolved.relocation else {
return Vec::new();
};
+10 -1
View File
@@ -5,6 +5,7 @@ use typed_path::{Utf8PlatformPathBuf, Utf8UnixPathBuf};
use crate::{
build::{BuildConfig, BuildStatus, run_make},
diff::Demangler,
jobs::{Job, JobContext, JobResult, JobState, start_job, update_status},
};
@@ -21,6 +22,7 @@ pub struct CreateScratchConfig {
pub function_name: String,
pub target_obj: Utf8PlatformPathBuf,
pub preset_id: Option<u32>,
pub demangler: Demangler,
}
#[derive(Default, Debug, Clone)]
@@ -74,7 +76,14 @@ fn run_create_scratch(
.text("diff_label", config.function_name.clone())
.text("diff_flags", diff_flags)
.text("context", context.unwrap_or_default())
.text("source_code", "// Move related code from Context tab to here");
.text("source_code", "// Move related code from Context tab to here")
.text(
"name",
config
.demangler
.demangle(&config.function_name)
.unwrap_or_else(|| config.function_name.clone()),
);
if let Some(preset) = config.preset_id {
form = form.text("preset", preset.to_string());
}
+2 -2
View File
@@ -175,8 +175,8 @@ fn run_build(
Ok(Box::new(ObjDiffResult {
first_status,
second_status,
first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))),
second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))),
first_obj: first_obj.zip(result.left),
second_obj: second_obj.zip(result.right),
time,
}))
}
+28 -3
View File
@@ -40,6 +40,7 @@ fn map_section_kind(section: &object::Section) -> SectionKind {
/// e.g. symbol$1234 and symbol$2345 will both be replaced with symbol$0000 internally.
fn get_normalized_symbol_name(name: &str) -> Option<String> {
const DUMMY_UNIQUE_ID: &str = "0000";
const DUMMY_UNIQUE_MSVC_ID: &str = "00000000";
if let Some((prefix, suffix)) = name.split_once("@class$")
&& let Some(idx) = suffix.chars().position(|c| !c.is_numeric())
&& idx > 0
@@ -59,6 +60,26 @@ fn get_normalized_symbol_name(name: &str) -> Option<String> {
{
// Match GCC symbol.1234 against symbol.2345
Some(format!("{prefix}.{DUMMY_UNIQUE_ID}"))
} else if name.starts_with('?') {
// Match MSVC anonymous class symbol names, ignoring the unique ID.
// e.g. ?CheckContextOr@?A0x24773155@@YA_NPBVDataArray@@@Z
// and: ?CheckContextOr@?A0xddf6240c@@YA_NPBVDataArray@@@Z
let mut name_str = String::from(name);
let anon_indices: Vec<usize> = name_str.match_indices("?A0x").map(|(idx, _)| idx).collect();
if !anon_indices.is_empty() {
for idx in anon_indices {
// the str sequence we're looking for is: ?A0xXXXXXXXX@@
if u32::from_str_radix(&name_str[idx + 4..idx + 12], 16).is_ok()
&& &name_str[idx + 12..idx + 14] == "@@"
{
// if the two above checks passed, we're good to replace the hash
name_str.replace_range(idx + 4..idx + 12, DUMMY_UNIQUE_MSVC_ID);
}
}
Some(name_str)
} else {
None
}
} else {
None
}
@@ -874,8 +895,12 @@ fn combine_sections(
let mut data_sections = BTreeMap::<String, 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 };
let base_name = section
.name
.get(1..)
.and_then(|s| s.rfind(['$', '.']))
.and_then(|i| section.name.get(..i + 1))
.unwrap_or(&section.name);
match section.kind {
SectionKind::Data | SectionKind::Bss => {
data_sections.entry(base_name.to_string()).or_default().push(i);
@@ -1064,7 +1089,7 @@ pub fn parse(data: &[u8], config: &DiffObjConfig, diff_side: DiffSide) -> Result
combine_sections(&mut sections, &mut symbols, config)?;
}
add_section_symbols(&sections, &mut symbols);
arch.post_init(&sections, &symbols);
arch.post_init(&sections, &symbols, &symbol_indices);
let mut obj = Object {
arch,
endianness: obj_file.endianness(),
+25
View File
@@ -57,6 +57,31 @@ fn combine_text_sections() {
insta::assert_snapshot!(output);
}
#[test]
#[cfg(feature = "arm")]
fn thumb_short_data_mapping() {
// When a .2byte directive is used in Thumb code, the assembler emits
// $d/$t mapping symbols for a 2-byte data region. The disassembler must
// not read 4 bytes as a .word when the next mapping symbol limits the
// data region to 2 bytes.
let diff_config = diff::DiffObjConfig::default();
let obj = obj::read::parse(
include_object!("data/arm/code_1_vblank.o"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
let symbol_idx = obj.symbols.iter().position(|s| s.name == "VBlankDMA_Level1").unwrap();
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
// .2byte data followed by Thumb code must not be merged into a 4-byte .word
assert!(
!output.contains(".word"),
"2-byte data regions should not be decoded as 4-byte .word values.\n\
The disassembler must respect mapping symbol boundaries."
);
}
#[test]
#[cfg(feature = "arm")]
fn trim_trailing_hword() {
+87
View File
@@ -122,3 +122,90 @@ fn read_vmx128_coff() {
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
insta::assert_snapshot!(output);
}
#[test]
#[cfg(feature = "ppc")]
fn decode_sjis_pooled_strings() {
// Test multiple pooled Shift JIS strings separated by a single null byte between entries. (MWCC)
let diff_config = diff::DiffObjConfig { combine_data_sections: true, ..Default::default() };
let obj = obj::read::parse(
include_object!("data/ppc/m_Do_hostIO.o"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
common::assert_literal_value(
&obj,
&diff_config,
"createChild__16mDoHIO_subRoot_cFPCcP13JORReflexible",
15,
"Shift JIS",
"危険:既に登録されているホストIOをふたたび登録しようとしています<%s>\n",
);
common::assert_literal_value(
&obj,
&diff_config,
"createChild__16mDoHIO_subRoot_cFPCcP13JORReflexible",
42,
"Shift JIS",
"ホストIOの空きエントリがありません。登録できませんでした。\n",
);
}
#[test]
#[cfg(feature = "ppc")]
fn decode_utf16_unpooled_strings() {
// Test unpooled UTF-16BE wide strings with null bytes at the start, end, and in the middle of the string. (MSVC)
let diff_config = diff::DiffObjConfig { combine_data_sections: true, ..Default::default() };
let obj = obj::read::parse(
include_object!("data/ppc/KinectSharePanel.obj"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
common::assert_literal_value(
&obj,
&diff_config,
"?OnPostLink@KinectSharePanel@@AAA?AVDataNode@@PAVDataArray@@@Z",
84,
"UTF-16BE",
"Title Text",
);
common::assert_literal_value(
&obj,
&diff_config,
"?OnPostLink@KinectSharePanel@@AAA?AVDataNode@@PAVDataArray@@@Z",
120,
"UTF-16BE",
"http://www.dancecentral.com/content-assets/2012/06/2012E3LogoBox_tn.jpg",
);
}
#[test]
#[cfg(feature = "ppc")]
fn decode_ascii_strings_with_null_padding() {
// Test unpooled ASCII strings with more than one null byte at the end.
let diff_config = diff::DiffObjConfig { combine_data_sections: true, ..Default::default() };
let obj = obj::read::parse(
include_object!("data/ppc/vmx128.obj"),
&diff_config,
diff::DiffSide::Base,
)
.unwrap();
common::assert_literal_value(
&obj,
&diff_config,
"?PrintVector@@YAXPBDU__vector4@@@Z",
24,
"ASCII",
"%s: [%.2f, %.2f, %.2f, %.2f]\n",
);
common::assert_literal_value(
&obj,
&diff_config,
"?ReservedRegisterExample@@YAXXZ",
59,
"ASCII",
"Result from vr66",
);
}
+21
View File
@@ -50,3 +50,24 @@ macro_rules! include_object {
include_bytes_align_as!(u64, $path)
};
}
#[allow(dead_code)]
pub fn assert_literal_value(
obj: &Object,
diff_config: &DiffObjConfig,
function_name: &str,
ins_row_idx: usize,
literal_label: &str,
literal_value: &str,
) {
let symbol_idx = obj.symbols.iter().position(|s| s.name == function_name).unwrap();
let diff = objdiff_core::diff::code::no_diff_code(obj, symbol_idx, diff_config).unwrap();
let ins_ref = diff.instruction_rows[ins_row_idx].ins_ref.unwrap();
let resolved = obj.resolve_instruction_ref(symbol_idx, ins_ref).unwrap();
let literals = objdiff_core::diff::display::display_ins_data_literals(obj, resolved);
let target_literal =
literals.iter().find(|lit_info| lit_info.label_override == Some(literal_label.to_string()));
assert_ne!(target_literal, None);
let lit_info = target_literal.unwrap();
assert_eq!(lit_info.literal, literal_value);
}

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