diff --git a/objdiff-cli/src/views/function_diff.rs b/objdiff-cli/src/views/function_diff.rs index 3366eef..a0472e5 100644 --- a/objdiff-cli/src/views/function_diff.rs +++ b/objdiff-cli/src/views/function_diff.rs @@ -570,6 +570,7 @@ impl FunctionDiffUi { DiffTextColor::Normal => Color::Gray, DiffTextColor::Dim => Color::DarkGray, DiffTextColor::Bright => Color::White, + DiffTextColor::DataFlow => Color::LightCyan, DiffTextColor::Replace => Color::Cyan, DiffTextColor::Delete => Color::Red, DiffTextColor::Insert => Color::Green, diff --git a/objdiff-core/Cargo.toml b/objdiff-core/Cargo.toml index c485750..6a12d5d 100644 --- a/objdiff-core/Cargo.toml +++ b/objdiff-core/Cargo.toml @@ -175,7 +175,7 @@ time = { version = "0.3", optional = true } encoding_rs = { version = "0.8.35", optional = true } [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3", optional = true } +winapi = { version = "0.3", optional = true, features = ["winbase"] } # For Linux static binaries, use rustls [target.'cfg(target_os = "linux")'.dependencies] diff --git a/objdiff-core/config-schema.json b/objdiff-core/config-schema.json index d35d12b..2a307ab 100644 --- a/objdiff-core/config-schema.json +++ b/objdiff-core/config-schema.json @@ -25,6 +25,20 @@ } ] }, + { + "id": "analyzeDataFlow", + "type": "boolean", + "default": false, + "name": "(Experimental) Perform data flow analysis", + "description": "Use data flow analysis to display known information about register contents where possible" + }, + { + "id": "showDataFlow", + "type": "boolean", + "default": true, + "name": "Show data flow", + "description": "Show data flow analysis results in place of register name where present" + }, { "id": "spaceBetweenArgs", "type": "boolean", @@ -264,7 +278,8 @@ "id": "ppc", "name": "PowerPC", "properties": [ - "ppc.calculatePoolRelocations" + "ppc.calculatePoolRelocations", + "analyzeDataFlow" ] }, { diff --git a/objdiff-core/src/arch/mod.rs b/objdiff-core/src/arch/mod.rs index 5cedba8..321edc0 100644 --- a/objdiff-core/src/arch/mod.rs +++ b/objdiff-core/src/arch/mod.rs @@ -1,5 +1,5 @@ use alloc::{borrow::Cow, boxed::Box, format, string::String, vec::Vec}; -use core::{ffi::CStr, fmt, fmt::Debug}; +use core::{ffi::CStr, fmt::{self, Debug}}; use anyhow::{Result, bail}; use encoding_rs::SHIFT_JIS; @@ -7,12 +7,10 @@ use object::Endian as _; use crate::{ diff::{ - DiffObjConfig, - display::{ContextItem, HoverItem, InstructionPart}, + display::{ContextItem, HoverItem, InstructionPart}, DiffObjConfig }, obj::{ - InstructionArg, InstructionRef, Object, ParsedInstruction, Relocation, RelocationFlags, - ResolvedInstructionRef, ResolvedSymbol, Section, Symbol, SymbolFlagSet, SymbolKind, + FlowAnalysisResult, InstructionArg, InstructionRef, Object, ParsedInstruction, Relocation, RelocationFlags, ResolvedInstructionRef, ResolvedSymbol, Section, Symbol, SymbolFlagSet, SymbolKind }, util::ReallySigned, }; @@ -31,6 +29,7 @@ pub mod superh; pub mod x86; /// Represents the type of data associated with an instruction +#[derive(PartialEq)] pub enum DataType { Int8, Int16, @@ -335,6 +334,17 @@ pub trait Arch: Send + Sync + Debug { Vec::new() } + // Perform detailed data flow analysis + fn data_flow_analysis( + &self, + _obj: &Object, + _symbol: &Symbol, + _code: &[u8], + _relocations: &[Relocation], + ) -> Option> { + None + } + fn implcit_addend( &self, file: &object::File<'_>, diff --git a/objdiff-core/src/arch/ppc/flow_analysis.rs b/objdiff-core/src/arch/ppc/flow_analysis.rs new file mode 100644 index 0000000..2c81c67 --- /dev/null +++ b/objdiff-core/src/arch/ppc/flow_analysis.rs @@ -0,0 +1,634 @@ +use crate::{ + arch::DataType, + obj::{FlowAnalysisResult, FlowAnalysisValue, Object, Relocation, Symbol}, + util::{RawDouble, RawFloat}, +}; +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::{boxed::Box, format, string::String, vec::Vec}; +use core::ffi::CStr; +use core::ops::{Index, IndexMut}; +use itertools::Itertools; +use ppc750cl::Simm; + +fn is_store_instruction(op: ppc750cl::Opcode) -> bool { + use ppc750cl::Opcode; + matches!( + op, + Opcode::Stbux + | Opcode::Stbx + | Opcode::Stfdux + | Opcode::Stfdx + | Opcode::Stfiwx + | Opcode::Stfsux + | Opcode::Stfsx + | Opcode::Sthbrx + | Opcode::Sthux + | Opcode::Sthx + | Opcode::Stswi + | Opcode::Stswx + | Opcode::Stwbrx + | Opcode::Stwcx_ + | Opcode::Stwux + | Opcode::Stwx + | Opcode::Stwu + | Opcode::Stb + | Opcode::Stbu + | Opcode::Sth + | Opcode::Sthu + | Opcode::Stmw + | Opcode::Stfs + | Opcode::Stfsu + | Opcode::Stfd + | Opcode::Stfdu + ) +} + +pub fn guess_data_type_from_load_store_inst_op(inst_op: ppc750cl::Opcode) -> Option { + use ppc750cl::Opcode; + match inst_op { + Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8), + Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16), + Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16), + Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32), + Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float), + Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double), + + Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8), + Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16), + Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32), + Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float), + Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double), + _ => None, + } +} + +#[derive(Default, PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord)] +enum RegisterContent { + #[default] + Unknown, + Variable, // Multiple potential values + FloatConstant(RawFloat), + DoubleConstant(RawDouble), + IntConstant(i32), + InputRegister(u8), + Symbol(usize), +} + +impl core::fmt::Display for RegisterContent { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RegisterContent::Unknown => write!(f, "unknown"), + RegisterContent::Variable => write!(f, "variable"), + RegisterContent::IntConstant(i) => + // -i is safe because it's at most a 16 bit constant in the i32 + { + if *i >= 0 { + write!(f, "0x{:x}", i) + } else { + write!(f, "-0x{:x}", -i) + } + } + RegisterContent::FloatConstant(RawFloat(fp)) => write!(f, "{fp:?}f"), + RegisterContent::DoubleConstant(RawDouble(fp)) => write!(f, "{fp:?}d"), + RegisterContent::InputRegister(p) => write!(f, "input{p}"), + RegisterContent::Symbol(_u) => write!(f, "relocation"), + } + } +} + +#[derive(Clone, PartialEq, Eq, Ord, PartialOrd)] +struct RegisterState { + gpr: [RegisterContent; 32], + fpr: [RegisterContent; 32], +} + +impl RegisterState { + fn new() -> Self { + RegisterState { gpr: [RegisterContent::Unknown; 32], fpr: [RegisterContent::Unknown; 32] } + } + + // During a function call, these registers must be assumed trashed. + fn clear_volatile(&mut self) { + self[ppc750cl::GPR(0)] = RegisterContent::Unknown; + for i in 0..=13 { + self[ppc750cl::GPR(i)] = RegisterContent::Unknown; + } + for i in 0..=13 { + self[ppc750cl::FPR(i)] = RegisterContent::Unknown; + } + } + + // Mark potential input values. + // Subsequent flow analysis will "realize" that they are not actually inputs if + // they get overwritten with another value before getting read. + fn set_potential_inputs(&mut self) { + for g_reg in 3..=13 { + self[ppc750cl::GPR(g_reg)] = RegisterContent::InputRegister(g_reg); + } + for f_reg in 1..=13 { + self[ppc750cl::FPR(f_reg)] = RegisterContent::InputRegister(f_reg); + } + } + + // If the there is no value, we can take the new known value. + // If there's a known value different than the new value, the content + // must is variable. + // Returns whether the current value was updated. + fn unify_values(current: &mut RegisterContent, new: &RegisterContent) -> bool { + if *current == *new { + false + } else if *current == RegisterContent::Unknown { + *current = *new; + true + } else if *current == RegisterContent::Variable { + // Already variable + false + } else { + *current = RegisterContent::Variable; + true + } + } + + // Unify currently known register contents in a give situation with new + // information about the register contents in that situation. + // Currently unknown register contents can be filled, but if there are + // conflicting contents, we go back to unknown. + fn unify(&mut self, other: &RegisterState) -> bool { + let mut updated = false; + for i in 0..32 { + updated |= Self::unify_values(&mut self.gpr[i], &other.gpr[i]); + updated |= Self::unify_values(&mut self.fpr[i], &other.fpr[i]); + } + updated + } +} + +impl Index for RegisterState { + type Output = RegisterContent; + fn index(&self, gpr: ppc750cl::GPR) -> &Self::Output { + &self.gpr[gpr.0 as usize] + } +} +impl IndexMut for RegisterState { + fn index_mut(&mut self, gpr: ppc750cl::GPR) -> &mut Self::Output { + &mut self.gpr[gpr.0 as usize] + } +} + +impl Index for RegisterState { + type Output = RegisterContent; + fn index(&self, fpr: ppc750cl::FPR) -> &Self::Output { + &self.fpr[fpr.0 as usize] + } +} +impl IndexMut for RegisterState { + fn index_mut(&mut self, fpr: ppc750cl::FPR) -> &mut Self::Output { + &mut self.fpr[fpr.0 as usize] + } +} + +fn execute_instruction( + registers: &mut RegisterState, + op: &ppc750cl::Opcode, + args: &[ppc750cl::Argument; 5], +) { + use ppc750cl::{Argument, GPR, Opcode}; + match (op, args[0], args[1], args[2]) { + (Opcode::Or, Argument::GPR(a), Argument::GPR(b), Argument::GPR(c)) => { + // Move is implemented as or with self for ints + if b == c { + registers[a] = registers[b]; + } else { + registers[a] = RegisterContent::Unknown; + } + } + (Opcode::Fmr, Argument::FPR(a), Argument::FPR(b), _) => { + registers[a] = registers[b]; + } + (Opcode::Addi, Argument::GPR(a), Argument::GPR(GPR(0)), Argument::Simm(c)) => { + // Load immidiate implemented as addi with addend = r0 + // Let Addi with other addends fall through to the case which + // overwrites the destination + registers[a] = RegisterContent::IntConstant(c.0 as i32); + } + (Opcode::Bcctr, _, _, _) => { + // Called a function pointer, may have erased volatile registers + registers.clear_volatile(); + } + (Opcode::B, _, _, _) => { + if get_branch_offset(args) == 0 { + // Call to another function + registers.clear_volatile(); + } + } + ( + Opcode::Stbu | Opcode::Sthu | Opcode::Stwu | Opcode::Stfsu | Opcode::Stfdu, + _, + _, + Argument::GPR(rel), + ) => { + // Storing with update, clear updated register (third arg) + registers[rel] = RegisterContent::Unknown; + } + ( + Opcode::Stbux | Opcode::Sthux | Opcode::Stwux | Opcode::Stfsux | Opcode::Stfdux, + _, + Argument::GPR(rel), + _, + ) => { + // Storing indexed with update, clear updated register (second arg) + registers[rel] = RegisterContent::Unknown; + } + ( + Opcode::Stb + | Opcode::Sth + | Opcode::Stw + | Opcode::Stbx + | Opcode::Sthx + | Opcode::Stwx + | Opcode::Stfs + | Opcode::Stfd, + _, + _, + _, + ) => { + // Storing, does not change registers + } + (Opcode::Lmw, Argument::GPR(target), _, _) => { + // `lmw` overwrites all registers from rd to r31. + for reg in target.0..31 { + registers[GPR(reg)] = RegisterContent::Unknown; + } + } + (_, Argument::GPR(a), _, _) => { + // Other operations which write to GPR a + registers[a] = RegisterContent::Unknown; + } + (_, Argument::FPR(a), _, _) => { + // Other operations which write to FPR a + registers[a] = RegisterContent::Unknown; + } + (_, _, _, _) => {} + } +} + +fn get_branch_offset(args: &[ppc750cl::Argument; 5]) -> i32 { + for arg in args.iter() { + if let ppc750cl::Argument::BranchDest(dest) = arg { + return dest.0 / 4; + } + } + 0 +} + +#[derive(Debug, Default)] +struct PPCFlowAnalysisResult { + argument_contents: BTreeMap<(u64, u8), FlowAnalysisValue>, +} + +impl PPCFlowAnalysisResult { + fn set_argument_value_at_address( + &mut self, + address: u64, + argument: u8, + value: FlowAnalysisValue, + ) { + self.argument_contents.insert((address, argument), value); + } + + fn new() -> Self { + PPCFlowAnalysisResult { argument_contents: Default::default() } + } +} + +impl FlowAnalysisResult for PPCFlowAnalysisResult { + fn get_argument_value_at_address( + &self, + address: u64, + argument: u8, + ) -> Option<&FlowAnalysisValue> { + self.argument_contents.get(&(address, argument)) + } +} + +fn clamp_text_length(s: String, max: usize) -> String { + if s.len() <= max { s } else { format!("{}…", s.chars().take(max - 3).collect::()) } +} + +// Executing op with args at cur_address, update current_state with symbols that +// come from relocations. That is, references to globals, floating point +// constants, string constants, etc. +fn fill_registers_from_relocation( + reloc: &Relocation, + current_state: &mut RegisterState, + obj: &Object, + op: ppc750cl::Opcode, + args: &[ppc750cl::Argument; 5], +) { + let content = if let Some(bytes) = obj.symbol_data(reloc.target_symbol) { + match guess_data_type_from_load_store_inst_op(op) { + Some(DataType::Float) => { + RegisterContent::FloatConstant(RawFloat(match obj.endianness { + object::Endianness::Little => { + f32::from_le_bytes(bytes.try_into().unwrap_or([0; 4])) + } + object::Endianness::Big => { + f32::from_be_bytes(bytes.try_into().unwrap_or([0; 4])) + } + })) + } + Some(DataType::Double) => { + RegisterContent::DoubleConstant(RawDouble(match obj.endianness { + object::Endianness::Little => { + f64::from_le_bytes(bytes.try_into().unwrap_or([0; 8])) + } + object::Endianness::Big => { + f64::from_be_bytes(bytes.try_into().unwrap_or([0; 8])) + } + })) + } + _ => RegisterContent::Symbol(reloc.target_symbol), + } + } else { + RegisterContent::Symbol(reloc.target_symbol) + }; + // Only update the register state for loads. We may store to a reloc + // address but that doesn't update register contents. + if !is_store_instruction(op) { + match (op, args[0]) { + // Everything else is a load of some sort + (_, ppc750cl::Argument::GPR(gpr)) => { + current_state[gpr] = content; + } + (_, ppc750cl::Argument::FPR(fpr)) => { + current_state[fpr] = content; + } + _ => {} + } + } +} + +// Special helper fragments generated by MWCC. +// See: https://github.com/encounter/decomp-toolkit/blob/main/src/analysis/pass.rs +const SLEDS: [&str; 6] = ["_savefpr_", "_restfpr_", "_savegpr_", "_restgpr_", "_savev", "_restv"]; + +fn is_sled_function(name: &str) -> bool { + SLEDS.iter().any(|sled| name.starts_with(sled)) +} + +pub fn ppc_data_flow_analysis( + obj: &Object, + func_symbol: &Symbol, + code: &[u8], + relocations: &[Relocation], +) -> Box { + use alloc::collections::VecDeque; + use ppc750cl::InsIter; + let instructions = InsIter::new(code, func_symbol.address as u32) + .map(|(_addr, ins)| (ins.op, ins.basic().args)) + .collect_vec(); + + let func_address = func_symbol.address; + + // Get initial register values from function parameters + let mut initial_register_state = RegisterState::new(); + initial_register_state.set_potential_inputs(); + + let mut execution_queue = VecDeque::<(usize, RegisterState)>::new(); + execution_queue.push_back((0, initial_register_state)); + + // Execute the instructions against abstract data + let mut failsafe_counter = 0; + let mut taken_branches = BTreeSet::<(usize, RegisterState)>::new(); + let mut register_state_at = Vec::::new(); + let mut completed_first_pass = false; + register_state_at.resize_with(instructions.len(), RegisterState::new); + while let Some((mut index, mut current_state)) = execution_queue.pop_front() { + while let Some((op, args)) = instructions.get(index) { + // Record the state at this index + // If recording does not result in any changes to the known values + // we're done, because the subsequent values are a function of the + // current values so we'll get the same result as the last time + // we went down this path. + // Don't break out if we haven't even completed the first pass + // through the function though. + if !register_state_at[index].unify(¤t_state) && completed_first_pass { + break; + } + + // Get symbol used in this instruction + let cur_addr = (func_address as u32) + ((index * 4) as u32); + let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr); + + // Is this a branch to a compiler generated helper? These helpers + // do not trash registers like normal function calls, so we don't + // want to treat this as normal execution. + let symbol = reloc.and_then(|r| obj.symbols.get(r.target_symbol)); + let is_sled_invocation = symbol.is_some_and(|x| is_sled_function(&x.name)); + + // Execute the instruction to update the state + // Since sled invocations are only used to save / restore registers + // as part of prelude / cleanup in a function call we don't have to + // do any execution for them. + if !is_sled_invocation { + execute_instruction(&mut current_state, op, args); + } + + // Fill in register state coming from relocations at this line. This + // handles references to global variables, floating point constants, + // etc. + if let Some(reloc) = reloc { + fill_registers_from_relocation(reloc, &mut current_state, obj, *op, args); + } + + // Add conditional branches to execution queue + // Only take a given (address, register state) combination once. If + // the known register state is different we have to take the branch + // again to stabilize the known values for backwards branches. + if op == &ppc750cl::Opcode::Bc { + let branch_state = (index, current_state.clone()); + if !taken_branches.contains(&branch_state) { + let offset = get_branch_offset(args); + let target_index = ((index as i32) + offset) as usize; + execution_queue.push_back((target_index, current_state.clone())); + taken_branches.insert(branch_state); + + // We should never hit this case, but avoid getting stuck in + // an infinite loop if we hit some kind of bad behavior. + failsafe_counter += 1; + if failsafe_counter > 256 { + //println!("Analysis of {} failed to stabilize", func_symbol.name); + return Box::new(PPCFlowAnalysisResult::new()); + } + } + } + + // Update index + if op == &ppc750cl::Opcode::B { + // Unconditional branch + let offset = get_branch_offset(args); + if offset > 0 { + // Jump table or branch to over else clause. + index += offset as usize; + } else if offset == 0 { + // Function call with relocation. We'll return to + // the next instruction. + index += 1; + } else { + // Unconditional branch (E.g.: loop { ... }) + // Also some compilations of loops put the conditional at + // the end and B to it for the check of the first iteration. + let branch_state = (index, current_state.clone()); + if taken_branches.contains(&branch_state) { + break; + } + taken_branches.insert(branch_state); + index = ((index as i32) + offset) as usize; + } + } else { + // Normal execution of next instruction + index += 1; + } + } + + // Mark that we've completed at least one pass over the function, at + // this point we can break out if the code we're running doesn't change + // any register outcomes. + completed_first_pass = true; + } + + // Store the relevant data flow values for simplified instructions + generate_flow_analysis_result(obj, func_address, code, register_state_at, relocations) +} + +fn get_string_data(obj: &Object, symbol_index: usize, offset: Simm) -> Option<&str> { + if let Some(sym) = obj.symbols.get(symbol_index) { + if sym.name.starts_with("@stringBase") && offset.0 != 0 { + if let Some(data) = obj.symbol_data(symbol_index) { + let bytes = &data[offset.0 as usize..]; + if let Ok(Ok(str)) = CStr::from_bytes_until_nul(bytes).map(|x| x.to_str()) { + return Some(str); + } + } + } + } + None +} + +// Write the relevant part of the flow analysis out into the FlowAnalysisResult +// the rest of the application will use to query results of the flow analysis. +// Flow analysis will compute the known contents of every register at every +// line, but we only need to record the values of registers that are actually +// referenced at each line. +fn generate_flow_analysis_result( + obj: &Object, + base_address: u64, + code: &[u8], + register_state_at: Vec, + relocations: &[Relocation], +) -> Box { + use ppc750cl::{Argument, GPR, InsIter, Offset}; + let mut analysis_result = PPCFlowAnalysisResult::new(); + let default_register_state = RegisterState::new(); + for (addr, ins) in InsIter::new(code, 0) { + let ins_address = base_address + (addr as u64); + let index = addr / 4; + let ppc750cl::ParsedIns { mnemonic: _, args } = ins.simplified(); + + // Special case to show float and double constants on the line where + // they are being loaded. + // We need to do this before we break out on showing relocations in the + // subsequent if statement. + if ins.op == ppc750cl::Opcode::Lfs || ins.op == ppc750cl::Opcode::Lfd { + // The value is set on the line AFTER the load, get it from there + if let Some(next_state) = register_state_at.get(index as usize + 1) { + // When loading from SDA it will be a relocation so Reg+Offset will both be zero + if let (Argument::FPR(fpr), Argument::Offset(Offset(0)), Argument::GPR(GPR(0))) = + (args[0], args[1], args[2]) + { + if let RegisterContent::Symbol(_index) = next_state[fpr] { + // We loaded a global variable, not a constant, + // don't do anything for this case. + } else { + analysis_result.set_argument_value_at_address( + ins_address, + 1, + FlowAnalysisValue::Text(format!("{}", next_state[fpr])), + ); + continue; + } + } + } + } + + // Special case to show string constants on the line where they are + // being indexed to. This will typically be "addi t, stringbase, offset" + let registers = register_state_at.get(index as usize).unwrap_or(&default_register_state); + if let (ppc750cl::Opcode::Addi, Argument::GPR(rel), Argument::Simm(offset)) = + (ins.op, args[1], args[2]) + { + if let RegisterContent::Symbol(sym_index) = registers[rel] { + if let Some(str) = get_string_data(obj, sym_index, offset) { + // Show the string constant in the analysis result + let formatted = format!("\"{}\"", str); + analysis_result.set_argument_value_at_address( + ins_address, + 2, + FlowAnalysisValue::Text(clamp_text_length(formatted, 20)), + ); + // Don't continue, we want to show the stringbase value as well + } + } + } + + // If we're already showing relocations on a line don't also show data flow + if relocations.iter().any(|r| (r.address & !3) == ins_address) { + continue; + } + + let is_store = is_store_instruction(ins.op); + for (arg_index, arg) in args.into_iter().enumerate() { + // Hacky shorthand for determining which arguments are sources, + // We only want to show data flow for source registers, not target + // registers. Technically there are some non-"st_" operations which + // read from their first argument but they're rare. + if (arg_index == 0) && !is_store { + continue; + } + + let content = match arg { + Argument::GPR(gpr) => Some(registers[gpr]), + Argument::FPR(fpr) => Some(registers[fpr]), + _ => None, + }; + let analysis_value = match content { + Some(RegisterContent::Symbol(s)) => obj.symbols.get(s).map(|sym| { + FlowAnalysisValue::Text(clamp_text_length( + sym.demangled_name.as_ref().unwrap_or(&sym.name).clone(), + 20, + )) + }), + Some(RegisterContent::InputRegister(reg)) => { + let reg_name = match arg { + Argument::GPR(_) => format!("input_r{reg}"), + Argument::FPR(_) => format!("input_f{reg}"), + _ => panic!("Register content should only be in a register"), + }; + Some(FlowAnalysisValue::Text(reg_name)) + } + Some(RegisterContent::Unknown) | Some(RegisterContent::Variable) => None, + Some(value) => Some(FlowAnalysisValue::Text(format!("{value}"))), + None => None, + }; + if let Some(analysis_value) = analysis_value { + analysis_result.set_argument_value_at_address( + ins_address, + arg_index as u8, + analysis_value, + ); + } + } + } + + Box::new(analysis_result) +} diff --git a/objdiff-core/src/arch/ppc.rs b/objdiff-core/src/arch/ppc/mod.rs similarity index 95% rename from objdiff-core/src/arch/ppc.rs rename to objdiff-core/src/arch/ppc/mod.rs index f62b38f..0eaf37b 100644 --- a/objdiff-core/src/arch/ppc.rs +++ b/objdiff-core/src/arch/ppc/mod.rs @@ -3,6 +3,7 @@ use alloc::{ string::{String, ToString}, vec, vec::Vec, + boxed::Box, }; use anyhow::{Result, bail, ensure}; @@ -18,11 +19,13 @@ use crate::{ display::{ContextItem, HoverItem, HoverItemColor, InstructionPart, SymbolNavigationKind}, }, obj::{ - InstructionRef, Object, Relocation, RelocationFlags, ResolvedInstructionRef, - ResolvedRelocation, Symbol, SymbolFlag, SymbolFlagSet, + FlowAnalysisResult, InstructionRef, Object, Relocation, RelocationFlags, + ResolvedInstructionRef, ResolvedRelocation, Symbol, SymbolFlag, SymbolFlagSet }, }; +mod flow_analysis; + // Relative relocation, can be Simm, Offset or BranchDest fn is_relative_arg(arg: &ppc750cl::Argument) -> bool { matches!( @@ -157,6 +160,7 @@ impl Arch for ArchPpc { Ok(()) } + // Could be replaced by data_flow_analysis once that feature stabilizes fn generate_pooled_relocations( &self, address: u64, @@ -166,6 +170,16 @@ impl Arch for ArchPpc { ) -> Vec { generate_fake_pool_relocations_for_function(address, code, relocations, symbols) } + + fn data_flow_analysis( + &self, + obj: &Object, + symbol: &Symbol, + code: &[u8], + relocations: &[Relocation], + ) -> Option> { + Some(flow_analysis::ppc_data_flow_analysis(obj, symbol, code, relocations)) + } fn implcit_addend( &self, @@ -225,7 +239,7 @@ impl Arch for ArchPpc { return Some(DataType::String); } let opcode = ppc750cl::Opcode::from(resolved.ins_ref.opcode as u8); - if let Some(ty) = guess_data_type_from_load_store_inst_op(opcode) { + if let Some(ty) = flow_analysis::guess_data_type_from_load_store_inst_op(opcode) { // Numeric type. return Some(ty); } @@ -501,25 +515,6 @@ fn make_symbol_ref(symbol: &object::Symbol) -> Result { Ok(ExtabSymbolRef { original_index: symbol.index().0 - 1, name, demangled_name }) } -fn guess_data_type_from_load_store_inst_op(inst_op: ppc750cl::Opcode) -> Option { - use ppc750cl::Opcode; - match inst_op { - Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8), - Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16), - Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16), - Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32), - Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float), - Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double), - - Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8), - Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16), - Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32), - Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float), - Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double), - _ => None, - } -} - #[derive(Debug)] struct PoolReference { addr_src_gpr: ppc750cl::GPR, @@ -536,7 +531,7 @@ fn get_pool_reference_for_inst( ) -> Option { use ppc750cl::{Argument, Opcode}; let args = &simplified.args; - if guess_data_type_from_load_store_inst_op(opcode).is_some() { + if flow_analysis::guess_data_type_from_load_store_inst_op(opcode).is_some() { match (args[1], args[2]) { (Argument::Offset(offset), Argument::GPR(addr_src_gpr)) => { // e.g. lwz. Immediate offset. @@ -668,7 +663,7 @@ fn make_fake_pool_reloc( // and returns a Vec of "fake pool relocations" that simulate what a relocation for that instruction // would look like if data hadn't been pooled. // This method tries to follow the function's proper control flow. It keeps track of a queue of -// states it hasn't traversed yet, where each state holds an instruction address and a HashMap of +// states it hasn't traversed yet, where each state holds an instruction address and a map of // which registers hold which pool relocations at that point. // When a conditional or unconditional branch is encountered, the destination of the branch is added // to the queue. Conditional branches will traverse both the path where the branch is taken and the diff --git a/objdiff-core/src/diff/display.rs b/objdiff-core/src/diff/display.rs index 7f928e3..1690e98 100644 --- a/objdiff-core/src/diff/display.rs +++ b/objdiff-core/src/diff/display.rs @@ -12,11 +12,9 @@ use itertools::Itertools; use regex::Regex; use crate::{ - diff::{DiffObjConfig, InstructionDiffKind, InstructionDiffRow, ObjectDiff, SymbolDiff}, - obj::{ - InstructionArg, InstructionArgValue, Object, ParsedInstruction, ResolvedInstructionRef, - ResolvedRelocation, SectionFlag, SectionKind, Symbol, SymbolFlag, SymbolKind, - }, + diff::{DiffObjConfig, InstructionDiffKind, InstructionDiffRow, ObjectDiff, SymbolDiff}, obj::{ + FlowAnalysisValue, InstructionArg, InstructionArgValue, Object, ParsedInstruction, ResolvedInstructionRef, ResolvedRelocation, SectionFlag, SectionKind, Symbol, SymbolFlag, SymbolKind + } }; #[derive(Debug, Clone)] @@ -46,12 +44,13 @@ pub enum DiffText<'a> { #[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash)] pub enum DiffTextColor { #[default] - Normal, // Grey - Dim, // Dark grey - Bright, // White - Replace, // Blue - Delete, // Red - Insert, // Green + Normal, // Grey + Dim, // Dark grey + Bright, // White + DataFlow, // Light blue + Replace, // Blue + Delete, // Red + Insert, // Green Rotating(u8), } @@ -186,6 +185,11 @@ pub fn display_row( } let mut arg_idx = 0; let mut displayed_relocation = false; + let analysis_result = if diff_config.show_data_flow { + obj.flow_analysis_results.get(&resolved.symbol.address) + } else { + None + }; obj.arch.display_instruction(resolved, diff_config, &mut |part| match part { InstructionPart::Basic(text) => { if text.chars().all(|c| c == ' ') { @@ -208,15 +212,30 @@ pub fn display_row( if arg == InstructionArg::Reloc { displayed_relocation = true; } - match (arg, resolved.ins_ref.branch_dest) { - (InstructionArg::Value(value), _) => cb(DiffTextSegment { - text: DiffText::Argument(value), - color: diff_index + let data_flow_value = + analysis_result.and_then(|result| + result.as_ref().get_argument_value_at_address( + ins_ref.address, (arg_idx - 1) as u8)); + match (arg, data_flow_value, resolved.ins_ref.branch_dest) { + // If we have a flow analysis result, always use that over anything else. + (InstructionArg::Value(_) | InstructionArg::Reloc, Some(FlowAnalysisValue::Text(text)), _) => { + cb(DiffTextSegment { + text: DiffText::Argument(InstructionArgValue::Opaque(Cow::Borrowed(text))), + color: DiffTextColor::DataFlow, + pad_to: 0, + }) + }, + (InstructionArg::Value(value), None, _) => { + let color = diff_index .get() - .map_or(base_color, |i| DiffTextColor::Rotating(i as u8)), - pad_to: 0, - }), - (InstructionArg::Reloc, None) => { + .map_or(base_color, |i| DiffTextColor::Rotating(i as u8)); + cb(DiffTextSegment { + text: DiffText::Argument(value), + color, + pad_to: 0, + }) + }, + (InstructionArg::Reloc, _, None) => { let resolved = resolved.relocation.unwrap(); let color = diff_index .get() @@ -235,9 +254,9 @@ pub fn display_row( } Ok(()) } - (InstructionArg::BranchDest(dest), _) | + (InstructionArg::BranchDest(dest), _, _) | // If the relocation was resolved to a branch destination, emit that instead. - (InstructionArg::Reloc, Some(dest)) => { + (InstructionArg::Reloc, _, Some(dest)) => { if let Some(addr) = dest.checked_sub(resolved.symbol.address) { cb(DiffTextSegment { text: DiffText::BranchDest(addr), diff --git a/objdiff-core/src/obj/mod.rs b/objdiff-core/src/obj/mod.rs index e182ed9..061103e 100644 --- a/objdiff-core/src/obj/mod.rs +++ b/objdiff-core/src/obj/mod.rs @@ -233,6 +233,15 @@ pub enum SymbolKind { Section, } +#[derive(Debug)] +pub enum FlowAnalysisValue { + Text(String), +} + +pub trait FlowAnalysisResult : core::fmt::Debug + Send { + fn get_argument_value_at_address(&self, address: u64, argument: u8) -> Option<&FlowAnalysisValue>; +} + #[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] pub struct Symbol { pub name: String, @@ -260,6 +269,7 @@ pub struct Object { pub path: Option, #[cfg(feature = "std")] pub timestamp: Option, + pub flow_analysis_results: BTreeMap>, } impl Default for Object { @@ -274,6 +284,7 @@ impl Default for Object { path: None, #[cfg(feature = "std")] timestamp: None, + flow_analysis_results: BTreeMap::>::new(), } } } @@ -312,6 +323,10 @@ impl Object { pub fn symbol_by_name(&self, name: &str) -> Option { self.symbols.iter().position(|symbol| symbol.section.is_some() && symbol.name == name) } + + pub fn has_flow_analysis_result(&self) -> bool { + !self.flow_analysis_results.is_empty() + } } #[derive(Debug, Clone, Eq, PartialEq, Hash)] diff --git a/objdiff-core/src/obj/read.rs b/objdiff-core/src/obj/read.rs index b5cef3b..3bd28f1 100644 --- a/objdiff-core/src/obj/read.rs +++ b/objdiff-core/src/obj/read.rs @@ -432,17 +432,18 @@ fn map_relocations( Ok(()) } -fn calculate_pooled_relocations( - arch: &dyn Arch, - sections: &mut [Section], - symbols: &[Symbol], -) -> Result<()> { - for (section_index, section) in sections.iter_mut().enumerate() { +fn perform_data_flow_analysis(obj: &mut Object, config: &DiffObjConfig) -> Result<()> { + // If neither of these settings are on, no flow analysis to perform + if !config.analyze_data_flow && !config.ppc_calculate_pool_relocations { + return Ok(()); + } + + let mut generated_relocations = Vec::<(usize, Vec)>::new(); + for (section_index, section) in obj.sections.iter().enumerate() { if section.kind != SectionKind::Code { continue; } - let mut fake_pool_relocs = Vec::new(); - for symbol in symbols { + for symbol in obj.symbols.iter() { if symbol.section != Some(section_index) { continue; } @@ -457,14 +458,36 @@ fn calculate_pooled_relocations( symbol.address + symbol.size ) })?; - fake_pool_relocs.append(&mut arch.generate_pooled_relocations( - symbol.address, - code, - §ion.relocations, - symbols, - )); + + // Optional pooled relocation computation + // Long view: This could be replaced by the full data flow analysis + // once that feature has stabilized. + if config.ppc_calculate_pool_relocations { + let relocations = obj.arch.generate_pooled_relocations( + symbol.address, + code, + §ion.relocations, + &obj.symbols); + generated_relocations.push((section_index, relocations)); + } + + // Optional full data flow analysis + if config.analyze_data_flow { + obj.arch.data_flow_analysis( + &obj, + symbol, + code, + §ion.relocations, + ).and_then(|flow_result| { + obj.flow_analysis_results.insert(symbol.address, flow_result) + }); + } } - section.relocations.append(&mut fake_pool_relocs); + } + for (section_index, mut relocations) in generated_relocations { + obj.sections[section_index].relocations.append(&mut relocations); + } + for section in obj.sections.iter_mut() { section.relocations.sort_by_key(|r| r.address); } Ok(()) @@ -865,15 +888,12 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result { let (mut symbols, symbol_indices) = map_symbols(arch.as_ref(), &obj_file, §ions, §ion_indices, split_meta.as_ref())?; map_relocations(arch.as_ref(), &obj_file, &mut sections, §ion_indices, &symbol_indices)?; - if config.ppc_calculate_pool_relocations { - calculate_pooled_relocations(arch.as_ref(), &mut sections, &symbols)?; - } parse_line_info(&obj_file, &mut sections, §ion_indices, data)?; if config.combine_data_sections || config.combine_text_sections { combine_sections(&mut sections, &mut symbols, config)?; } arch.post_init(§ions, &symbols); - Ok(Object { + let mut obj = Object { arch, endianness: obj_file.endianness(), symbols, @@ -883,7 +903,14 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result { path: None, #[cfg(feature = "std")] timestamp: None, - }) + flow_analysis_results: Default::default(), + }; + + // Need to construct the obj first so that we have a convinient package to + // pass to flow analysis. Then the flow analysis will mutate obj adding + // additional data to it. + perform_data_flow_analysis(&mut obj, config)?; + Ok(obj) } #[cfg(feature = "std")] diff --git a/objdiff-core/src/util.rs b/objdiff-core/src/util.rs index 81995f1..3c81b73 100644 --- a/objdiff-core/src/util.rs +++ b/objdiff-core/src/util.rs @@ -59,3 +59,45 @@ pub fn align_u64_to(len: u64, align: u64) -> u64 { len + ((align - (len % align) pub fn align_data_slice_to(data: &mut Vec, align: u64) { data.resize(align_u64_to(data.len() as u64, align) as usize, 0); } + +// Float where we specifically care about comparing the raw bits rather than +// caring about IEEE semantics. +#[derive(Copy, Clone, Debug)] +pub struct RawFloat(pub f32); +impl PartialEq for RawFloat { + fn eq(&self, other: &Self) -> bool { + self.0.to_bits() == other.0.to_bits() + } +} +impl Eq for RawFloat {} +impl Ord for RawFloat { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.0.to_bits().cmp(&other.0.to_bits()) + } +} +impl PartialOrd for RawFloat { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// Double where we specifically care about comparing the raw bits rather than +// caring about IEEE semantics. +#[derive(Copy, Clone, Debug)] +pub struct RawDouble(pub f64); +impl PartialEq for RawDouble { + fn eq(&self, other: &Self) -> bool { + self.0.to_bits() == other.0.to_bits() + } +} +impl Eq for RawDouble {} +impl Ord for RawDouble { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.0.to_bits().cmp(&other.0.to_bits()) + } +} +impl PartialOrd for RawDouble { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/objdiff-core/tests/snapshots/arch_arm__read_arm.snap b/objdiff-core/tests/snapshots/arch_arm__read_arm.snap index ec9917a..f3df1fe 100644 --- a/objdiff-core/tests/snapshots/arch_arm__read_arm.snap +++ b/objdiff-core/tests/snapshots/arch_arm__read_arm.snap @@ -1954,4 +1954,5 @@ Object { split_meta: None, path: None, timestamp: None, + flow_analysis_results: {}, } diff --git a/objdiff-core/tests/snapshots/arch_arm__read_thumb.snap b/objdiff-core/tests/snapshots/arch_arm__read_thumb.snap index 94eb210..9e86193 100644 --- a/objdiff-core/tests/snapshots/arch_arm__read_thumb.snap +++ b/objdiff-core/tests/snapshots/arch_arm__read_thumb.snap @@ -3826,4 +3826,5 @@ Object { split_meta: None, path: None, timestamp: None, + flow_analysis_results: {}, } diff --git a/objdiff-core/tests/snapshots/arch_mips__read_mips.snap b/objdiff-core/tests/snapshots/arch_mips__read_mips.snap index b24dd19..b67679a 100644 --- a/objdiff-core/tests/snapshots/arch_mips__read_mips.snap +++ b/objdiff-core/tests/snapshots/arch_mips__read_mips.snap @@ -1490,4 +1490,5 @@ Object { split_meta: None, path: None, timestamp: None, + flow_analysis_results: {}, } diff --git a/objdiff-core/tests/snapshots/arch_ppc__read_extab.snap b/objdiff-core/tests/snapshots/arch_ppc__read_extab.snap index 0753fd7..909b2a3 100644 --- a/objdiff-core/tests/snapshots/arch_ppc__read_extab.snap +++ b/objdiff-core/tests/snapshots/arch_ppc__read_extab.snap @@ -548,4 +548,5 @@ Object { split_meta: None, path: None, timestamp: None, + flow_analysis_results: {}, } diff --git a/objdiff-core/tests/snapshots/arch_ppc__read_ppc.snap b/objdiff-core/tests/snapshots/arch_ppc__read_ppc.snap index 6c73593..32c49c1 100644 --- a/objdiff-core/tests/snapshots/arch_ppc__read_ppc.snap +++ b/objdiff-core/tests/snapshots/arch_ppc__read_ppc.snap @@ -581,4 +581,5 @@ Object { ), path: None, timestamp: None, + flow_analysis_results: {}, } diff --git a/objdiff-core/tests/snapshots/arch_x86__read_x86.snap b/objdiff-core/tests/snapshots/arch_x86__read_x86.snap index dfba455..92dc5cc 100644 --- a/objdiff-core/tests/snapshots/arch_x86__read_x86.snap +++ b/objdiff-core/tests/snapshots/arch_x86__read_x86.snap @@ -207,4 +207,5 @@ Object { split_meta: None, path: None, timestamp: None, + flow_analysis_results: {}, } diff --git a/objdiff-core/tests/snapshots/arch_x86__read_x86_64.snap b/objdiff-core/tests/snapshots/arch_x86__read_x86_64.snap index 3c684df..cc95831 100644 --- a/objdiff-core/tests/snapshots/arch_x86__read_x86_64.snap +++ b/objdiff-core/tests/snapshots/arch_x86__read_x86_64.snap @@ -1574,4 +1574,5 @@ Object { split_meta: None, path: None, timestamp: None, + flow_analysis_results: {}, } diff --git a/objdiff-core/tests/snapshots/arch_x86__read_x86_jumptable.snap b/objdiff-core/tests/snapshots/arch_x86__read_x86_jumptable.snap index c77cc99..6ef775b 100644 --- a/objdiff-core/tests/snapshots/arch_x86__read_x86_jumptable.snap +++ b/objdiff-core/tests/snapshots/arch_x86__read_x86_jumptable.snap @@ -311,4 +311,5 @@ Object { split_meta: None, path: None, timestamp: None, + flow_analysis_results: {}, } diff --git a/objdiff-core/tests/snapshots/arch_x86__read_x86_local_labels.snap b/objdiff-core/tests/snapshots/arch_x86__read_x86_local_labels.snap index 95929d7..3e5131f 100644 --- a/objdiff-core/tests/snapshots/arch_x86__read_x86_local_labels.snap +++ b/objdiff-core/tests/snapshots/arch_x86__read_x86_local_labels.snap @@ -160,4 +160,5 @@ Object { split_meta: None, path: None, timestamp: None, + flow_analysis_results: {}, } diff --git a/objdiff-gui/src/views/appearance.rs b/objdiff-gui/src/views/appearance.rs index 805d4b9..6a106f5 100644 --- a/objdiff-gui/src/views/appearance.rs +++ b/objdiff-gui/src/views/appearance.rs @@ -23,6 +23,8 @@ pub struct Appearance { #[serde(skip)] pub highlight_color: Color32, // WHITE #[serde(skip)] + pub dataflow_color: Color32, // + #[serde(skip)] pub replace_color: Color32, // LIGHT_BLUE #[serde(skip)] pub insert_color: Color32, // GREEN @@ -61,6 +63,7 @@ impl Default for Appearance { emphasized_text_color: Color32::LIGHT_GRAY, deemphasized_text_color: Color32::DARK_GRAY, highlight_color: Color32::WHITE, + dataflow_color: Color32::from_rgb(0, 128, 128), replace_color: Color32::LIGHT_BLUE, insert_color: Color32::GREEN, delete_color: Color32::from_rgb(200, 40, 41), @@ -104,6 +107,7 @@ impl Appearance { self.emphasized_text_color = Color32::LIGHT_GRAY; self.deemphasized_text_color = Color32::DARK_GRAY; self.highlight_color = Color32::WHITE; + self.dataflow_color = Color32::from_rgb(0, 128, 128); self.replace_color = Color32::LIGHT_BLUE; self.insert_color = Color32::GREEN; self.delete_color = Color32::from_rgb(200, 40, 41); @@ -114,6 +118,7 @@ impl Appearance { self.emphasized_text_color = Color32::DARK_GRAY; self.deemphasized_text_color = Color32::LIGHT_GRAY; self.highlight_color = Color32::BLACK; + self.dataflow_color = Color32::from_rgb(0, 128, 128); self.replace_color = Color32::DARK_BLUE; self.insert_color = Color32::DARK_GREEN; self.delete_color = Color32::from_rgb(200, 40, 41); diff --git a/objdiff-gui/src/views/diff.rs b/objdiff-gui/src/views/diff.rs index 49dedee..5266d8b 100644 --- a/objdiff-gui/src/views/diff.rs +++ b/objdiff-gui/src/views/diff.rs @@ -281,6 +281,19 @@ pub fn diff_view_ui( }) }); } + + // Only need to check the first Object. Technically the first could not have a flow analysis + // result while the second does but we don't want to waste space on two separate checkboxes. + if result.first_obj.as_ref().is_some_and(|(first, _)| first.has_flow_analysis_result()) { + let mut placeholder = diff_config.show_data_flow; + if ui + .checkbox(&mut placeholder, "Show data flow") + .on_hover_text("Show data flow analysis results in place of register names") + .clicked() + { + ret = Some(DiffViewAction::ToggleShowDataFlow); + } + } } else if column == 1 { // Right column diff --git a/objdiff-gui/src/views/function_diff.rs b/objdiff-gui/src/views/function_diff.rs index dff2c15..34e0817 100644 --- a/objdiff-gui/src/views/function_diff.rs +++ b/objdiff-gui/src/views/function_diff.rs @@ -174,6 +174,7 @@ fn diff_text_ui( DiffTextColor::Normal => appearance.text_color, DiffTextColor::Dim => appearance.deemphasized_text_color, DiffTextColor::Bright => appearance.emphasized_text_color, + DiffTextColor::DataFlow => appearance.dataflow_color, DiffTextColor::Replace => appearance.replace_color, DiffTextColor::Delete => appearance.delete_color, DiffTextColor::Insert => appearance.insert_color, diff --git a/objdiff-gui/src/views/symbol_diff.rs b/objdiff-gui/src/views/symbol_diff.rs index cfed70f..ca6038b 100644 --- a/objdiff-gui/src/views/symbol_diff.rs +++ b/objdiff-gui/src/views/symbol_diff.rs @@ -79,6 +79,8 @@ pub enum DiffViewAction { SetMapping(usize, usize), /// Set the show_mapped_symbols flag SetShowMappedSymbols(bool), + /// Toggle the show_data_flow flag + ToggleShowDataFlow, } #[derive(Debug, Clone, Default, Eq, PartialEq)] @@ -350,6 +352,12 @@ impl DiffViewState { DiffViewAction::SetShowMappedSymbols(value) => { self.symbol_state.show_mapped_symbols = value; } + DiffViewAction::ToggleShowDataFlow => { + let Ok(mut state) = state.write() else { + return; + }; + state.config.diff_obj_config.show_data_flow = !state.config.diff_obj_config.show_data_flow; + } } }