Skip to main content

objdiff_core/diff/
code.rs

1use alloc::{
2    collections::{BTreeMap, btree_map},
3    string::{String, ToString},
4    vec,
5    vec::Vec,
6};
7
8use anyhow::{Context, Result, anyhow, ensure};
9
10use super::{
11    DiffObjConfig, FunctionRelocDiffs, InstructionArgDiffIndex, InstructionBranchFrom,
12    InstructionBranchTo, InstructionDiffKind, InstructionDiffRow, PreferredStringEncoding,
13    SymbolDiff, display::display_ins_data_literals,
14};
15use crate::obj::{
16    InstructionArg, InstructionArgValue, InstructionRef, Object, ResolvedInstructionRef,
17    ResolvedRelocation, ResolvedSymbol, SymbolKind,
18};
19
20pub fn no_diff_code(
21    obj: &Object,
22    symbol_index: usize,
23    diff_config: &DiffObjConfig,
24) -> Result<SymbolDiff> {
25    let symbol = &obj.symbols[symbol_index];
26    let section_index = symbol.section.ok_or_else(|| anyhow!("Missing section for symbol"))?;
27    let section = &obj.sections[section_index];
28    let data = section.data_range(symbol.address, symbol.size as usize).ok_or_else(|| {
29        anyhow!(
30            "Symbol data out of bounds: {:#x}..{:#x}",
31            symbol.address,
32            symbol.address + symbol.size
33        )
34    })?;
35    let ops = obj.arch.scan_instructions(
36        ResolvedSymbol { obj, symbol_index, symbol, section_index, section, data },
37        diff_config,
38    )?;
39    let mut instruction_rows = Vec::<InstructionDiffRow>::new();
40    for i in &ops {
41        instruction_rows.push(InstructionDiffRow { ins_ref: Some(*i), ..Default::default() });
42    }
43    resolve_branches(&ops, &mut instruction_rows);
44    Ok(SymbolDiff {
45        target_symbol: None,
46        match_percent: None,
47        diff_score: None,
48        instruction_rows,
49        ..Default::default()
50    })
51}
52
53const PENALTY_IMM_DIFF: u64 = 1;
54const PENALTY_REG_DIFF: u64 = 5;
55const PENALTY_REPLACE: u64 = 60;
56const PENALTY_INSERT_DELETE: u64 = 100;
57
58pub fn diff_code(
59    left_obj: &Object,
60    right_obj: &Object,
61    left_symbol_idx: usize,
62    right_symbol_idx: usize,
63    diff_config: &DiffObjConfig,
64) -> Result<(SymbolDiff, SymbolDiff)> {
65    let left_symbol = &left_obj.symbols[left_symbol_idx];
66    let right_symbol = &right_obj.symbols[right_symbol_idx];
67    let left_section = left_symbol
68        .section
69        .and_then(|i| left_obj.sections.get(i))
70        .ok_or_else(|| anyhow!("Missing section for symbol"))?;
71    let right_section = right_symbol
72        .section
73        .and_then(|i| right_obj.sections.get(i))
74        .ok_or_else(|| anyhow!("Missing section for symbol"))?;
75    let left_data = left_section
76        .data_range(left_symbol.address, left_symbol.size as usize)
77        .ok_or_else(|| {
78            anyhow!(
79                "Symbol data out of bounds: {:#x}..{:#x}",
80                left_symbol.address,
81                left_symbol.address + left_symbol.size
82            )
83        })?;
84    let right_data = right_section
85        .data_range(right_symbol.address, right_symbol.size as usize)
86        .ok_or_else(|| {
87            anyhow!(
88                "Symbol data out of bounds: {:#x}..{:#x}",
89                right_symbol.address,
90                right_symbol.address + right_symbol.size
91            )
92        })?;
93
94    let left_section_idx = left_symbol.section.unwrap();
95    let right_section_idx = right_symbol.section.unwrap();
96    let left_ops = left_obj.arch.scan_instructions(
97        ResolvedSymbol {
98            obj: left_obj,
99            symbol_index: left_symbol_idx,
100            symbol: left_symbol,
101            section_index: left_section_idx,
102            section: left_section,
103            data: left_data,
104        },
105        diff_config,
106    )?;
107    let right_ops = right_obj.arch.scan_instructions(
108        ResolvedSymbol {
109            obj: right_obj,
110            symbol_index: right_symbol_idx,
111            symbol: right_symbol,
112            section_index: right_section_idx,
113            section: right_section,
114            data: right_data,
115        },
116        diff_config,
117    )?;
118    let (mut left_rows, mut right_rows) = diff_instructions(&left_ops, &right_ops)?;
119    resolve_branches(&left_ops, &mut left_rows);
120    resolve_branches(&right_ops, &mut right_rows);
121
122    let mut diff_state = InstructionDiffState::default();
123    for (left_row, right_row) in left_rows.iter_mut().zip(right_rows.iter_mut()) {
124        let result = diff_instruction(
125            left_obj,
126            right_obj,
127            left_symbol_idx,
128            right_symbol_idx,
129            left_row.ins_ref,
130            right_row.ins_ref,
131            left_row,
132            right_row,
133            diff_config,
134            &mut diff_state,
135        )?;
136        left_row.kind = result.kind;
137        right_row.kind = result.kind;
138        left_row.arg_diff = result.left_args_diff;
139        right_row.arg_diff = result.right_args_diff;
140    }
141
142    let max_score = left_ops.len() as u64 * PENALTY_INSERT_DELETE;
143    let diff_score = diff_state.diff_score.min(max_score);
144    let match_percent = if max_score == 0 {
145        100.0
146    } else {
147        ((1.0 - (diff_score as f64 / max_score as f64)) * 100.0) as f32
148    };
149
150    Ok((
151        SymbolDiff {
152            target_symbol: Some(right_symbol_idx),
153            match_percent: Some(match_percent),
154            diff_score: Some((diff_score, max_score)),
155            instruction_rows: left_rows,
156            ..Default::default()
157        },
158        SymbolDiff {
159            target_symbol: Some(left_symbol_idx),
160            match_percent: Some(match_percent),
161            diff_score: Some((diff_score, max_score)),
162            instruction_rows: right_rows,
163            ..Default::default()
164        },
165    ))
166}
167
168fn diff_instructions(
169    left_insts: &[InstructionRef],
170    right_insts: &[InstructionRef],
171) -> Result<(Vec<InstructionDiffRow>, Vec<InstructionDiffRow>)> {
172    let left_ops = left_insts.iter().map(|i| i.opcode).collect::<Vec<_>>();
173    let right_ops = right_insts.iter().map(|i| i.opcode).collect::<Vec<_>>();
174    let ops = similar::capture_diff_slices(similar::Algorithm::Patience, &left_ops, &right_ops);
175    if ops.is_empty() {
176        ensure!(left_insts.len() == right_insts.len());
177        let left_diff = left_insts
178            .iter()
179            .map(|i| InstructionDiffRow { ins_ref: Some(*i), ..Default::default() })
180            .collect();
181        let right_diff = right_insts
182            .iter()
183            .map(|i| InstructionDiffRow { ins_ref: Some(*i), ..Default::default() })
184            .collect();
185        return Ok((left_diff, right_diff));
186    }
187
188    let row_count = ops
189        .iter()
190        .map(|op| match *op {
191            similar::DiffOp::Equal { len, .. } => len,
192            similar::DiffOp::Delete { old_len, .. } => old_len,
193            similar::DiffOp::Insert { new_len, .. } => new_len,
194            similar::DiffOp::Replace { old_len, new_len, .. } => old_len.max(new_len),
195        })
196        .sum();
197    let mut left_diff = Vec::<InstructionDiffRow>::with_capacity(row_count);
198    let mut right_diff = Vec::<InstructionDiffRow>::with_capacity(row_count);
199    for op in ops {
200        let (_tag, left_range, right_range) = op.as_tag_tuple();
201        let len = left_range.len().max(right_range.len());
202        left_diff.extend(
203            left_range
204                .clone()
205                .map(|i| InstructionDiffRow { ins_ref: Some(left_insts[i]), ..Default::default() }),
206        );
207        right_diff.extend(
208            right_range.clone().map(|i| InstructionDiffRow {
209                ins_ref: Some(right_insts[i]),
210                ..Default::default()
211            }),
212        );
213        if left_range.len() < len {
214            left_diff.extend((left_range.len()..len).map(|_| InstructionDiffRow::default()));
215        }
216        if right_range.len() < len {
217            right_diff.extend((right_range.len()..len).map(|_| InstructionDiffRow::default()));
218        }
219    }
220    Ok((left_diff, right_diff))
221}
222
223fn arg_to_string(arg: &InstructionArg, reloc: Option<ResolvedRelocation>) -> String {
224    match arg {
225        InstructionArg::Value(arg) => arg.to_string(),
226        InstructionArg::Reloc => {
227            reloc.as_ref().map_or_else(|| "<unknown>".to_string(), |r| r.symbol.name.clone())
228        }
229        InstructionArg::BranchDest(arg) => arg.to_string(),
230    }
231}
232
233fn resolve_branches(ops: &[InstructionRef], rows: &mut [InstructionDiffRow]) {
234    let mut branch_idx = 0u32;
235    // Map addresses to indices
236    let mut addr_map = BTreeMap::<u64, u32>::new();
237    for (i, ins_diff) in rows.iter().enumerate() {
238        if let Some(ins) = ins_diff.ins_ref {
239            addr_map.insert(ins.address, i as u32);
240        }
241    }
242    // Generate branches
243    let mut branches = BTreeMap::<u32, InstructionBranchFrom>::new();
244    for ((i, ins_diff), ins) in
245        rows.iter_mut().enumerate().filter(|(_, row)| row.ins_ref.is_some()).zip(ops)
246    {
247        if let Some(ins_idx) = ins.branch_dest.and_then(|a| addr_map.get(&a).copied()) {
248            match branches.entry(ins_idx) {
249                btree_map::Entry::Vacant(e) => {
250                    ins_diff.branch_to = Some(InstructionBranchTo { ins_idx, branch_idx });
251                    e.insert(InstructionBranchFrom { ins_idx: vec![i as u32], branch_idx });
252                    branch_idx += 1;
253                }
254                btree_map::Entry::Occupied(e) => {
255                    let branch = e.into_mut();
256                    ins_diff.branch_to =
257                        Some(InstructionBranchTo { ins_idx, branch_idx: branch.branch_idx });
258                    branch.ins_idx.push(i as u32);
259                }
260            }
261        }
262    }
263    // Store branch from
264    for (i, branch) in branches {
265        rows[i as usize].branch_from = Some(branch);
266    }
267}
268
269pub(crate) fn address_eq(left: ResolvedRelocation, right: ResolvedRelocation) -> bool {
270    if right.symbol.size == 0 && left.symbol.size != 0 {
271        // The base relocation is against a pool but the target relocation isn't.
272        // This can happen in rare cases where the compiler will generate a pool+addend relocation
273        // in the base's data, but the one detected in the target is direct with no addend.
274        // Just check that the final address is the same so these count as a match.
275        left.symbol.address as i64 + left.relocation.addend
276            == right.symbol.address as i64 + right.relocation.addend
277    } else {
278        // But otherwise, if the compiler isn't using a pool, we're more strict and check that the
279        // target symbol address and relocation addend both match exactly.
280        left.symbol.address == right.symbol.address
281            && left.relocation.addend == right.relocation.addend
282    }
283}
284
285pub(crate) fn section_name_eq(
286    left_obj: &Object,
287    right_obj: &Object,
288    left_section_index: usize,
289    right_section_index: usize,
290) -> bool {
291    left_obj.sections.get(left_section_index).is_some_and(|left_section| {
292        right_obj
293            .sections
294            .get(right_section_index)
295            .is_some_and(|right_section| left_section.name == right_section.name)
296    })
297}
298
299fn ins_data_literals_eq(
300    left_obj: &Object,
301    right_obj: &Object,
302    left_ins: ResolvedInstructionRef,
303    right_ins: ResolvedInstructionRef,
304    diff_config: &DiffObjConfig,
305) -> bool {
306    let mut left_literals = display_ins_data_literals(left_obj, left_ins);
307    let mut right_literals = display_ins_data_literals(right_obj, right_ins);
308    if left_literals == right_literals {
309        return true;
310    }
311    if diff_config.preferred_string_encoding == PreferredStringEncoding::Auto {
312        return left_literals == right_literals;
313    }
314    left_literals.retain(|lit_info| !lit_info.hidden(Some(diff_config)));
315    right_literals.retain(|lit_info| !lit_info.hidden(Some(diff_config)));
316    left_literals == right_literals
317}
318
319fn reloc_eq(
320    left_obj: &Object,
321    right_obj: &Object,
322    left_ins: ResolvedInstructionRef,
323    right_ins: ResolvedInstructionRef,
324    diff_config: &DiffObjConfig,
325) -> bool {
326    let relax_reloc_diffs = diff_config.function_reloc_diffs == FunctionRelocDiffs::None;
327    let (left_reloc, right_reloc) = match (left_ins.relocation, right_ins.relocation) {
328        (Some(left_reloc), Some(right_reloc)) => (left_reloc, right_reloc),
329        // If relocations are relaxed, match if left is missing a reloc
330        (None, Some(_)) => return relax_reloc_diffs,
331        (None, None) => return true,
332        _ => return false,
333    };
334    if left_reloc.relocation.flags != right_reloc.relocation.flags {
335        return false;
336    }
337    if relax_reloc_diffs {
338        return true;
339    }
340
341    let symbol_name_addend_matches = left_reloc.symbol.name == right_reloc.symbol.name
342        && left_reloc.relocation.addend == right_reloc.relocation.addend;
343    match (&left_reloc.symbol.section, &right_reloc.symbol.section) {
344        (Some(sl), Some(sr)) => {
345            // Match if section and name or address match
346            section_name_eq(left_obj, right_obj, *sl, *sr)
347                && (diff_config.function_reloc_diffs == FunctionRelocDiffs::DataValue
348                    || symbol_name_addend_matches
349                    || address_eq(left_reloc, right_reloc))
350                && (diff_config.function_reloc_diffs == FunctionRelocDiffs::NameAddress
351                    || left_reloc.symbol.kind != SymbolKind::Object
352                    || right_reloc.symbol.size == 0 // Likely a pool symbol like ...data, don't treat this as a diff
353                    || ins_data_literals_eq(left_obj, right_obj, left_ins, right_ins, diff_config))
354        }
355        (Some(_), None) | (None, Some(_)) | (None, None) => symbol_name_addend_matches,
356    }
357}
358
359fn arg_eq(
360    left_obj: &Object,
361    right_obj: &Object,
362    left_row: &InstructionDiffRow,
363    right_row: &InstructionDiffRow,
364    left_arg: &InstructionArg,
365    right_arg: &InstructionArg,
366    left_ins: ResolvedInstructionRef,
367    right_ins: ResolvedInstructionRef,
368    diff_config: &DiffObjConfig,
369) -> bool {
370    match left_arg {
371        InstructionArg::Value(l) => match right_arg {
372            InstructionArg::Value(r) => l.loose_eq(r),
373            // If relocations are relaxed, match if left is a constant and right is a reloc
374            // Useful for instances where the target object is created without relocations
375            InstructionArg::Reloc => diff_config.function_reloc_diffs == FunctionRelocDiffs::None,
376            _ => false,
377        },
378        InstructionArg::Reloc => {
379            matches!(right_arg, InstructionArg::Reloc)
380                && reloc_eq(left_obj, right_obj, left_ins, right_ins, diff_config)
381        }
382        InstructionArg::BranchDest(_) => match right_arg {
383            // Compare dest instruction idx after diffing
384            InstructionArg::BranchDest(_) => {
385                left_row.branch_to.as_ref().map(|b| b.ins_idx)
386                    == right_row.branch_to.as_ref().map(|b| b.ins_idx)
387            }
388            // If relocations are relaxed, match if left is a constant and right is a reloc
389            // Useful for instances where the target object is created without relocations
390            InstructionArg::Reloc => diff_config.function_reloc_diffs == FunctionRelocDiffs::None,
391            _ => false,
392        },
393    }
394}
395
396#[derive(Default)]
397struct InstructionDiffState {
398    diff_score: u64,
399    left_arg_idx: u32,
400    right_arg_idx: u32,
401    left_args_idx: BTreeMap<String, u32>,
402    right_args_idx: BTreeMap<String, u32>,
403}
404
405#[derive(Default)]
406struct InstructionDiffResult {
407    kind: InstructionDiffKind,
408    left_args_diff: Vec<InstructionArgDiffIndex>,
409    right_args_diff: Vec<InstructionArgDiffIndex>,
410}
411
412impl InstructionDiffResult {
413    #[inline]
414    const fn new(kind: InstructionDiffKind) -> Self {
415        Self { kind, left_args_diff: Vec::new(), right_args_diff: Vec::new() }
416    }
417}
418
419fn diff_instruction(
420    left_obj: &Object,
421    right_obj: &Object,
422    left_symbol_idx: usize,
423    right_symbol_idx: usize,
424    l: Option<InstructionRef>,
425    r: Option<InstructionRef>,
426    left_row: &InstructionDiffRow,
427    right_row: &InstructionDiffRow,
428    diff_config: &DiffObjConfig,
429    state: &mut InstructionDiffState,
430) -> Result<InstructionDiffResult> {
431    let (l, r) = match (l, r) {
432        (Some(l), Some(r)) => (l, r),
433        (Some(_), None) => {
434            state.diff_score += PENALTY_INSERT_DELETE;
435            return Ok(InstructionDiffResult::new(InstructionDiffKind::Delete));
436        }
437        (None, Some(_)) => {
438            state.diff_score += PENALTY_INSERT_DELETE;
439            return Ok(InstructionDiffResult::new(InstructionDiffKind::Insert));
440        }
441        (None, None) => return Ok(InstructionDiffResult::new(InstructionDiffKind::None)),
442    };
443
444    // If opcodes don't match, replace
445    if l.opcode != r.opcode {
446        state.diff_score += PENALTY_REPLACE;
447        return Ok(InstructionDiffResult::new(InstructionDiffKind::Replace));
448    }
449
450    let left_resolved = left_obj
451        .resolve_instruction_ref(left_symbol_idx, l)
452        .context("Failed to resolve left instruction")?;
453    let right_resolved = right_obj
454        .resolve_instruction_ref(right_symbol_idx, r)
455        .context("Failed to resolve right instruction")?;
456
457    if left_resolved.code != right_resolved.code
458        || !reloc_eq(left_obj, right_obj, left_resolved, right_resolved, diff_config)
459    {
460        // If either the raw code bytes or relocations don't match, process instructions and compare args
461        let left_ins = left_obj.arch.process_instruction(left_resolved, diff_config)?;
462        let right_ins = right_obj.arch.process_instruction(right_resolved, diff_config)?;
463        if left_ins.args.len() != right_ins.args.len() {
464            state.diff_score += PENALTY_REPLACE;
465            return Ok(InstructionDiffResult::new(InstructionDiffKind::Replace));
466        }
467        let mut result = InstructionDiffResult::new(InstructionDiffKind::None);
468        if left_ins.mnemonic != right_ins.mnemonic {
469            state.diff_score += PENALTY_REG_DIFF;
470            result.kind = InstructionDiffKind::OpMismatch;
471        }
472        for (a, b) in left_ins.args.iter().zip(right_ins.args.iter()) {
473            if arg_eq(
474                left_obj,
475                right_obj,
476                left_row,
477                right_row,
478                a,
479                b,
480                left_resolved,
481                right_resolved,
482                diff_config,
483            ) {
484                result.left_args_diff.push(InstructionArgDiffIndex::NONE);
485                result.right_args_diff.push(InstructionArgDiffIndex::NONE);
486            } else {
487                state.diff_score += if let InstructionArg::Value(
488                    InstructionArgValue::Signed(_) | InstructionArgValue::Unsigned(_),
489                ) = a
490                {
491                    PENALTY_IMM_DIFF
492                } else {
493                    PENALTY_REG_DIFF
494                };
495                if result.kind == InstructionDiffKind::None {
496                    result.kind = InstructionDiffKind::ArgMismatch;
497                }
498                let a_str = arg_to_string(a, left_resolved.relocation);
499                let a_diff = match state.left_args_idx.entry(a_str) {
500                    btree_map::Entry::Vacant(e) => {
501                        let idx = state.left_arg_idx;
502                        state.left_arg_idx = idx + 1;
503                        e.insert(idx);
504                        idx
505                    }
506                    btree_map::Entry::Occupied(e) => *e.get(),
507                };
508                let b_str = arg_to_string(b, right_resolved.relocation);
509                let b_diff = match state.right_args_idx.entry(b_str) {
510                    btree_map::Entry::Vacant(e) => {
511                        let idx = state.right_arg_idx;
512                        state.right_arg_idx = idx + 1;
513                        e.insert(idx);
514                        idx
515                    }
516                    btree_map::Entry::Occupied(e) => *e.get(),
517                };
518                result.left_args_diff.push(InstructionArgDiffIndex::new(a_diff));
519                result.right_args_diff.push(InstructionArgDiffIndex::new(b_diff));
520            }
521        }
522        if result.kind == InstructionDiffKind::None
523            && left_resolved.code.len() != right_resolved.code.len()
524        {
525            // If everything else matches but the raw code length differs (e.g. x86 instructions
526            // with same disassembly but different encoding), mark as op mismatch
527            result.kind = InstructionDiffKind::OpMismatch;
528            state.diff_score += PENALTY_REG_DIFF;
529        }
530        return Ok(result);
531    }
532
533    Ok(InstructionDiffResult::new(InstructionDiffKind::None))
534}