From afd367e546e46276273293d83d0925e55ce36c89 Mon Sep 17 00:00:00 2001 From: freeqaz <00free@gmail.com> Date: Tue, 3 Feb 2026 14:35:23 +0000 Subject: [PATCH 1/2] Detect and merge tail blocks, preserve global-scope symbols Add tail block detection to XEX function analysis. When pdata reports a function end but disassembly reveals out-of-line code after it (small blocks with backward branches + blr), these are merged back into the preceding function rather than treated as separate symbols. Additionally, skip merging when the candidate has a global-scope symbol (from user symbols.txt, PDB, or map file), since these represent intentionally defined functions that should not be absorbed. This prevents symbols.txt regeneration from dropping user-defined functions that happen to look like tail blocks. --- src/analysis/cfa.rs | 296 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 291 insertions(+), 5 deletions(-) diff --git a/src/analysis/cfa.rs b/src/analysis/cfa.rs index 058362c..4e9d865 100644 --- a/src/analysis/cfa.rs +++ b/src/analysis/cfa.rs @@ -7,9 +7,11 @@ use std::{ use anyhow::{bail, ensure, Context, Result}; use itertools::Itertools; +use powerpc::Opcode; use crate::{ analysis::{ + disassemble, executor::{ExecCbData, ExecCbResult, Executor}, skip_alignment, slices::{FunctionSlices, TailCallResult}, @@ -17,8 +19,8 @@ use crate::{ RelocationTarget, }, obj::{ - ObjInfo, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind, - SectionIndex, + ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, + ObjSymbolKind, ObjSymbolScope, SectionIndex, }, }; @@ -124,6 +126,12 @@ pub struct AnalyzerState { pub jump_tables: BTreeMap, pub known_symbols: BTreeMap>, pub known_sections: BTreeMap, + /// Functions that were merged as tail blocks into their predecessors. + /// These need to be removed from obj.symbols during apply(). + pub merged_tail_blocks: Vec, + /// Functions whose ends were extended by absorbing tail blocks. + /// These need their symbol size updated in apply(). + pub extended_functions: Vec, } impl AnalyzerState { @@ -131,6 +139,52 @@ impl AnalyzerState { for (§ion_index, section_name) in &self.known_sections { obj.sections[section_index].rename(section_name.clone())?; } + // Remove symbols for functions that were merged as tail blocks + for addr in &self.merged_tail_blocks { + if let Ok(Some((index, _))) = obj.symbols.kind_at_section_address( + addr.section, + addr.address, + ObjSymbolKind::Function, + ) { + let existing = &obj.symbols[index]; + let symbol = ObjSymbol { + name: format!("__DELETED_{}", existing.name), + kind: ObjSymbolKind::Unknown, + size: 0, + flags: ObjSymbolFlagSet( + ObjSymbolFlags::RelocationIgnore + | ObjSymbolFlags::NoWrite + | ObjSymbolFlags::NoExport + | ObjSymbolFlags::Stripped, + ), + ..existing.clone() + }; + obj.symbols.replace(index, symbol)?; + } + } + // Update sizes for functions that absorbed tail blocks + for addr in &self.extended_functions { + if let Some(info) = self.functions.get(addr) { + if let Some(end) = info.end { + let new_size = (end.address - addr.address) as u64; + if let Ok(Some((index, _))) = obj.symbols.kind_at_section_address( + addr.section, + addr.address, + ObjSymbolKind::Function, + ) { + let existing = &obj.symbols[index]; + if existing.size != new_size { + let symbol = ObjSymbol { + size: new_size, + size_known: true, + ..existing.clone() + }; + obj.symbols.replace(index, symbol)?; + } + } + } + } + } for (&start, FunctionInfo { end, .. }) in self.functions.iter() { let Some(end) = end else { continue }; let section = &obj.sections[start.section]; @@ -286,9 +340,21 @@ impl AnalyzerState { let known_end = addr + *known_size; assert_eq!(func.end.is_some(), true, "Function at {} has no detected end rather than known end {}. There must be an error in processing!", addr, known_end); let func_end = func.end.unwrap(); - assert_eq!(func_end, known_end, - "Function at {} has known end addr {}, but during processing, ending was found to be {}!", - addr, known_end, func_end); + // pdata sizes are conservative and may not include + // out-of-line tail blocks, so allow func_end >= known_end + if func_end < known_end { + panic!( + "Function at {} has known end addr {}, but during processing, \ + ending was found to be {} (smaller than expected)!", + addr, known_end, func_end + ); + } else if func_end != known_end { + log::info!( + "Function at {} extends beyond pdata end {} to {} \ + (likely tail block inclusion)", + addr, known_end, func_end + ); + } } } else { unreachable!(); @@ -325,6 +391,11 @@ impl AnalyzerState { } bail!("Failed to finalize functions"); } + + // Merge tail blocks: small functions that are actually out-of-line code + // from the preceding function (e.g., loop exit paths placed after .pdata end) + self.merge_tail_blocks(obj)?; + Ok(()) } @@ -490,6 +561,221 @@ impl AnalyzerState { }) } + /// Post-pass to merge small functions that are actually tail blocks of their predecessor. + /// + /// After all functions are detected (from pdata, symbols, and gap-filling), this scans for + /// adjacent function pairs where the second function is a tail block of the first. This + /// handles cases where symbols.txt already has the fake function defined from a previous run. + fn merge_tail_blocks(&mut self, obj: &ObjInfo) -> Result<()> { + let mut merges: Vec<(SectionAddress, SectionAddress)> = vec![]; + + for (section_index, section) in obj.sections.by_kind(ObjSectionKind::Code) { + let section_start = SectionAddress::new(section_index, section.address as u32); + let section_end = section_start + section.size as u32; + let funcs_in_section: Vec<(SectionAddress, FunctionInfo)> = self + .functions + .range(section_start..section_end) + .map(|(&a, i)| (a, i.clone())) + .collect(); + + for window in funcs_in_section.windows(2) { + let (prev_addr, prev_info) = &window[0]; + let (func_addr, func_info) = &window[1]; + + let Some(prev_end) = prev_info.end else { continue }; + let Some(func_end) = func_info.end else { continue }; + + // Only consider the case where the candidate function starts right + // at the predecessor's end (no gap/alignment between them) + if *func_addr != prev_end { + continue; + } + + // Skip merging if the candidate has a global-scope symbol + // (user, PDB, or map file explicitly defined it as a real function) + if let Ok(Some((_, sym))) = obj.symbols.kind_at_section_address( + func_addr.section, + func_addr.address, + ObjSymbolKind::Function, + ) { + if sym.flags.scope() == ObjSymbolScope::Global { + log::info!( + "Skipping tail block merge of {:#010X} (global-scope symbol '{}')", + func_addr, sym.name, + ); + continue; + } + } + + // Check if this function is a tail block + if let Some(_tail_end) = Self::check_tail_block( + section, *func_addr, func_end, *prev_addr, prev_end, + ) { + log::info!( + "Merging tail block function {:#010X}-{:#010X} into {:#010X} (extending from {:#010X})", + func_addr, func_end, prev_addr, prev_end, + ); + merges.push((*prev_addr, *func_addr)); + } + } + } + + for (prev_addr, tail_addr) in &merges { + // Get the tail function's end before removing it + let tail_end = self.functions.get(tail_addr).and_then(|i| i.end).unwrap(); + // Remove the fake function + self.functions.remove(tail_addr); + // Track for symbol removal in apply() + self.merged_tail_blocks.push(*tail_addr); + // Extend the predecessor's end and track for size update in apply() + self.extended_functions.push(*prev_addr); + if let Some(info) = self.functions.get_mut(prev_addr) { + info.end = Some(tail_end); + // Mark for re-analysis with the new bounds + info.analyzed = false; + info.slices = None; + } + } + + if !merges.is_empty() { + log::info!("Merged {} tail block(s), re-analyzing affected functions", merges.len()); + // Re-analyze the extended functions + for (prev_addr, _) in &merges { + self.process_function_at(obj, *prev_addr)?; + } + } + + Ok(()) + } + + /// Check if code at `gap_start` (up to `gap_end`) is a tail block of the preceding function. + /// + /// A tail block is an out-of-line code fragment (typically a loop exit path) that the + /// compiler placed after the .pdata-reported function end. It's characterized by: + /// - Starting with an unconditional branch (`b`, not `bl`) back into the preceding function + /// - Or containing only a few instructions that all branch back into the preceding function + /// before ending with `blr` + /// + /// Returns `Some(block_end)` if this is a tail block, where `block_end` is the address + /// just past the last instruction in the tail block. + fn check_tail_block( + section: &ObjSection, + gap_start: SectionAddress, + gap_end: SectionAddress, + preceding_func_start: SectionAddress, + preceding_func_end: SectionAddress, + ) -> Option { + // Only consider small gaps (up to 64 bytes / 16 instructions) + let gap_size = gap_end.address - gap_start.address; + if gap_size > 64 { + return None; + } + + // Check the first instruction + let first_ins = disassemble(section, gap_start.address)?; + + // Case 1: First instruction is an unconditional branch (b, not bl) back into + // the preceding function. This is the classic out-of-line loop exit. + if first_ins.op == Opcode::B && !first_ins.field_lk() && !first_ins.field_aa() { + let target = first_ins.branch_dest(gap_start.address)?; + if target >= preceding_func_start.address && target < preceding_func_end.address { + // Scan forward to find the end of this tail block (up to blr or gap_end) + let mut addr = gap_start; + loop { + let Some(ins) = disassemble(section, addr.address) else { break }; + addr += 4; + // blr (unconditional return) or end of gap + if ins.op == Opcode::Bclr && !ins.field_lk() + && (ins.field_bo() & 0b10100 == 0b10100) + { + return Some(addr); + } + if addr >= gap_end { + return Some(gap_end); + } + } + } + } + + // Case 2: Block contains only backward branches into the preceding function + // and ends with blr. Common for multi-exit loops. + let mut has_backward_branch = false; + let mut ends_with_blr = false; + let mut addr = gap_start; + + while addr < gap_end { + let Some(ins) = disassemble(section, addr.address) else { break }; + addr += 4; + + // blr check (unconditional return) + if ins.op == Opcode::Bclr && !ins.field_lk() + && (ins.field_bo() & 0b10100 == 0b10100) + { + ends_with_blr = true; + // If the next instruction is still in the gap, keep scanning + // (there may be more code after this blr) + if addr >= gap_end { + break; + } + continue; + } + + // Unconditional branch (b, not bl) + if ins.op == Opcode::B && !ins.field_lk() && !ins.field_aa() { + if let Some(target) = ins.branch_dest(addr.address - 4) { + if target >= preceding_func_start.address + && target < preceding_func_end.address + { + has_backward_branch = true; + continue; + } + // Forward branch within the gap is OK + if target >= gap_start.address && target < gap_end.address { + continue; + } + } + // Branch outside both the gap and preceding function - not a tail block + return None; + } + + // Conditional branch (bc, not bcl) + if ins.op == Opcode::Bc && !ins.field_lk() && !ins.field_aa() { + if let Some(target) = ins.branch_dest(addr.address - 4) { + if target >= preceding_func_start.address + && target < preceding_func_end.address + { + has_backward_branch = true; + continue; + } + // Forward branch within the gap is OK + if target >= gap_start.address && target < gap_end.address { + continue; + } + } + // Branch outside both the gap and preceding function - not a tail block + return None; + } + + // bl (function call) - not a tail block characteristic + if ins.op == Opcode::B && ins.field_lk() { + return None; + } + + // bctr (indirect branch) - too complex to analyze + if ins.op == Opcode::Bcctr { + return None; + } + + // Other instructions are fine (arithmetic, loads, stores, etc.) + } + + if has_backward_branch && ends_with_blr { + Some(gap_end) + } else { + None + } + } + fn detect_new_functions(&mut self, obj: &ObjInfo) -> Result { let mut new_functions = vec![]; for (section_index, section) in obj.sections.by_kind(ObjSectionKind::Code) { From a02abfb489698688740896c7c4b4489008fb190b Mon Sep 17 00:00:00 2001 From: freeqaz <00free@gmail.com> Date: Tue, 3 Feb 2026 14:54:14 +0000 Subject: [PATCH 2/2] Refactor check_tail_block and add comprehensive tests - Extract MAX_TAIL_BLOCK_BYTES constant and helper functions (is_unconditional_blr, branch_into_range) - Split check_tail_block into three methods: dispatcher, check_tail_block_backward_branch (case 1), and check_tail_block_scan_block (case 2) - Optimize merge_tail_blocks to collect only (addr, end) tuples instead of cloning full FunctionInfo with slices - Replace unwrap() with expect() for better panic messages - Add 18 unit tests in separate cfa_tests.rs covering: helper functions, check_tail_block cases, merge_tail_blocks merging/skipping, apply() symbol deletion and size extension, global-scope preservation --- src/analysis/cfa.rs | 224 ++++++++++------ src/analysis/cfa_tests.rs | 551 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 687 insertions(+), 88 deletions(-) create mode 100644 src/analysis/cfa_tests.rs diff --git a/src/analysis/cfa.rs b/src/analysis/cfa.rs index 4e9d865..854bd4c 100644 --- a/src/analysis/cfa.rs +++ b/src/analysis/cfa.rs @@ -572,22 +572,24 @@ impl AnalyzerState { for (section_index, section) in obj.sections.by_kind(ObjSectionKind::Code) { let section_start = SectionAddress::new(section_index, section.address as u32); let section_end = section_start + section.size as u32; - let funcs_in_section: Vec<(SectionAddress, FunctionInfo)> = self + + // Collect only the addresses and ends we need — avoid cloning FunctionInfo/slices. + let func_bounds: Vec<(SectionAddress, Option)> = self .functions .range(section_start..section_end) - .map(|(&a, i)| (a, i.clone())) + .map(|(&addr, info)| (addr, info.end)) .collect(); - for window in funcs_in_section.windows(2) { - let (prev_addr, prev_info) = &window[0]; - let (func_addr, func_info) = &window[1]; + for window in func_bounds.windows(2) { + let (prev_addr, prev_end) = window[0]; + let (func_addr, func_end) = window[1]; - let Some(prev_end) = prev_info.end else { continue }; - let Some(func_end) = func_info.end else { continue }; + let Some(prev_end) = prev_end else { continue }; + let Some(func_end) = func_end else { continue }; // Only consider the case where the candidate function starts right // at the predecessor's end (no gap/alignment between them) - if *func_addr != prev_end { + if func_addr != prev_end { continue; } @@ -609,20 +611,23 @@ impl AnalyzerState { // Check if this function is a tail block if let Some(_tail_end) = Self::check_tail_block( - section, *func_addr, func_end, *prev_addr, prev_end, + section, func_addr, func_end, prev_addr, prev_end, ) { log::info!( "Merging tail block function {:#010X}-{:#010X} into {:#010X} (extending from {:#010X})", func_addr, func_end, prev_addr, prev_end, ); - merges.push((*prev_addr, *func_addr)); + merges.push((prev_addr, func_addr)); } } } for (prev_addr, tail_addr) in &merges { - // Get the tail function's end before removing it - let tail_end = self.functions.get(tail_addr).and_then(|i| i.end).unwrap(); + let tail_end = self + .functions + .get(tail_addr) + .and_then(|i| i.end) + .expect("tail function must exist in map"); // Remove the fake function self.functions.remove(tail_addr); // Track for symbol removal in apply() @@ -648,6 +653,29 @@ impl AnalyzerState { Ok(()) } + /// Maximum size of a tail block candidate in bytes (16 instructions). + const MAX_TAIL_BLOCK_BYTES: u32 = 64; + + /// Check whether `ins` is an unconditional `blr` (return from function). + fn is_unconditional_blr(ins: &powerpc::Ins) -> bool { + ins.op == Opcode::Bclr && !ins.field_lk() && (ins.field_bo() & 0b10100 == 0b10100) + } + + /// If `ins` at `ins_addr` is a non-link, non-absolute branch whose target falls within + /// `func_start..func_end`, return `Some(target)`. Otherwise return `None`. + fn branch_into_range( + ins: &powerpc::Ins, + ins_addr: u32, + func_start: u32, + func_end: u32, + ) -> Option { + if ins.field_lk() || ins.field_aa() { + return None; + } + let target = ins.branch_dest(ins_addr)?; + if target >= func_start && target < func_end { Some(target) } else { None } + } + /// Check if code at `gap_start` (up to `gap_end`) is a tail block of the preceding function. /// /// A tail block is an out-of-line code fragment (typically a loop exit path) that the @@ -665,115 +693,131 @@ impl AnalyzerState { preceding_func_start: SectionAddress, preceding_func_end: SectionAddress, ) -> Option { - // Only consider small gaps (up to 64 bytes / 16 instructions) let gap_size = gap_end.address - gap_start.address; - if gap_size > 64 { + if gap_size > Self::MAX_TAIL_BLOCK_BYTES { return None; } - // Check the first instruction + // Case 1: First instruction is an unconditional branch back into the predecessor. + if let Some(result) = Self::check_tail_block_backward_branch( + section, + gap_start, + gap_end, + preceding_func_start, + preceding_func_end, + ) { + return Some(result); + } + + // Case 2: All branches target the predecessor, block ends with blr. + Self::check_tail_block_scan_block( + section, + gap_start, + gap_end, + preceding_func_start, + preceding_func_end, + ) + } + + /// Case 1: First instruction is an unconditional branch (`b`, not `bl`) back into + /// the preceding function. This is the classic out-of-line loop exit. + /// + /// Scans forward from `gap_start` until a `blr` or `gap_end` to determine the + /// tail block extent. + fn check_tail_block_backward_branch( + section: &ObjSection, + gap_start: SectionAddress, + gap_end: SectionAddress, + preceding_func_start: SectionAddress, + preceding_func_end: SectionAddress, + ) -> Option { let first_ins = disassemble(section, gap_start.address)?; - // Case 1: First instruction is an unconditional branch (b, not bl) back into - // the preceding function. This is the classic out-of-line loop exit. - if first_ins.op == Opcode::B && !first_ins.field_lk() && !first_ins.field_aa() { - let target = first_ins.branch_dest(gap_start.address)?; - if target >= preceding_func_start.address && target < preceding_func_end.address { - // Scan forward to find the end of this tail block (up to blr or gap_end) - let mut addr = gap_start; - loop { - let Some(ins) = disassemble(section, addr.address) else { break }; - addr += 4; - // blr (unconditional return) or end of gap - if ins.op == Opcode::Bclr && !ins.field_lk() - && (ins.field_bo() & 0b10100 == 0b10100) - { - return Some(addr); - } - if addr >= gap_end { - return Some(gap_end); - } - } + if first_ins.op != Opcode::B { + return None; + } + // Must branch back into the preceding function + Self::branch_into_range( + &first_ins, + gap_start.address, + preceding_func_start.address, + preceding_func_end.address, + )?; + + // Scan forward to find the end of this tail block (up to blr or gap_end) + let mut addr = gap_start; + loop { + let ins = disassemble(section, addr.address)?; + addr += 4; + if Self::is_unconditional_blr(&ins) { + return Some(addr); + } + if addr >= gap_end { + return Some(gap_end); } } + } - // Case 2: Block contains only backward branches into the preceding function - // and ends with blr. Common for multi-exit loops. + /// Case 2: Scan the entire gap block — if every branch instruction targets back + /// into the preceding function (no outward calls or forward jumps to other functions), + /// and the block ends with `blr`, treat it as a tail block. + fn check_tail_block_scan_block( + section: &ObjSection, + gap_start: SectionAddress, + gap_end: SectionAddress, + preceding_func_start: SectionAddress, + preceding_func_end: SectionAddress, + ) -> Option { let mut has_backward_branch = false; let mut ends_with_blr = false; let mut addr = gap_start; while addr < gap_end { - let Some(ins) = disassemble(section, addr.address) else { break }; + let ins = disassemble(section, addr.address)?; addr += 4; - // blr check (unconditional return) - if ins.op == Opcode::Bclr && !ins.field_lk() - && (ins.field_bo() & 0b10100 == 0b10100) - { + if Self::is_unconditional_blr(&ins) { ends_with_blr = true; - // If the next instruction is still in the gap, keep scanning - // (there may be more code after this blr) if addr >= gap_end { break; } continue; } - // Unconditional branch (b, not bl) - if ins.op == Opcode::B && !ins.field_lk() && !ins.field_aa() { - if let Some(target) = ins.branch_dest(addr.address - 4) { - if target >= preceding_func_start.address - && target < preceding_func_end.address + match ins.op { + // Unconditional or conditional branch (not link) + Opcode::B | Opcode::Bc if !ins.field_lk() && !ins.field_aa() => { + let ins_addr = addr.address - 4; + if Self::branch_into_range( + &ins, + ins_addr, + preceding_func_start.address, + preceding_func_end.address, + ) + .is_some() { has_backward_branch = true; continue; } // Forward branch within the gap is OK - if target >= gap_start.address && target < gap_end.address { - continue; - } - } - // Branch outside both the gap and preceding function - not a tail block - return None; - } - - // Conditional branch (bc, not bcl) - if ins.op == Opcode::Bc && !ins.field_lk() && !ins.field_aa() { - if let Some(target) = ins.branch_dest(addr.address - 4) { - if target >= preceding_func_start.address - && target < preceding_func_end.address - { - has_backward_branch = true; - continue; - } - // Forward branch within the gap is OK - if target >= gap_start.address && target < gap_end.address { - continue; + if let Some(target) = ins.branch_dest(ins_addr) { + if target >= gap_start.address && target < gap_end.address { + continue; + } } + // Branch outside both the gap and preceding function + return None; } - // Branch outside both the gap and preceding function - not a tail block - return None; - } - - // bl (function call) - not a tail block characteristic - if ins.op == Opcode::B && ins.field_lk() { - return None; - } - - // bctr (indirect branch) - too complex to analyze - if ins.op == Opcode::Bcctr { - return None; + // bl (function call) — tail blocks don't call other functions + Opcode::B if ins.field_lk() => return None, + // bctr (indirect branch) — too complex to analyze + Opcode::Bcctr => return None, + // Other instructions are fine (arithmetic, loads, stores, etc.) + _ => {} } - - // Other instructions are fine (arithmetic, loads, stores, etc.) } - if has_backward_branch && ends_with_blr { - Some(gap_end) - } else { - None - } + if has_backward_branch && ends_with_blr { Some(gap_end) } else { None } } fn detect_new_functions(&mut self, obj: &ObjInfo) -> Result { @@ -959,3 +1003,7 @@ pub fn locate_bss_memsets(obj: &mut ObjInfo) -> Result> { )?; Ok(bss_sections) } + +#[cfg(test)] +#[path = "cfa_tests.rs"] +mod cfa_tests; diff --git a/src/analysis/cfa_tests.rs b/src/analysis/cfa_tests.rs new file mode 100644 index 0000000..b02d35b --- /dev/null +++ b/src/analysis/cfa_tests.rs @@ -0,0 +1,551 @@ +use super::*; +use crate::obj::{ + ObjArchitecture, ObjInfo, ObjKind, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, + ObjSymbolKind, ObjSymbolScope, +}; + +// ========================================================================= +// PPC instruction encoding helpers +// ========================================================================= + +const BLR: u32 = 0x4E800020; +const NOP: u32 = 0x60000000; +const ADDI_R3: u32 = 0x38630001; // addi r3, r3, 1 + +/// Encode `b offset` (unconditional relative branch, not link, not absolute) +fn ppc_b(offset: i32) -> u32 { 0x48000000 | (offset as u32 & 0x03FFFFFC) } + +/// Encode `bne offset` (conditional branch, CR0 not-equal) +fn ppc_bne(offset: i32) -> u32 { 0x40820000 | (offset as u32 & 0x0000FFFC) } + +/// Encode `bl offset` (branch and link) +fn ppc_bl(offset: i32) -> u32 { 0x48000001 | (offset as u32 & 0x03FFFFFC) } + +/// `bctr` (branch to count register) +const BCTR: u32 = 0x4E800420; + +/// Build a minimal code section from instruction words at the given base address. +fn make_code_section(base_addr: u32, instructions: &[u32]) -> ObjSection { + let data: Vec = instructions.iter().flat_map(|w| w.to_be_bytes()).collect(); + ObjSection { + name: ".text".into(), + kind: ObjSectionKind::Code, + address: base_addr as u64, + size: data.len() as u64, + data, + align: 4, + ..Default::default() + } +} + +/// Build a minimal ObjInfo with one code section from instruction words. +fn make_test_obj(base_addr: u32, instructions: &[u32]) -> ObjInfo { + let section = make_code_section(base_addr, instructions); + ObjInfo::new( + ObjKind::Executable, + ObjArchitecture::PowerPc, + "test".to_string(), + vec![], + vec![section], + ) +} + +/// Add a function symbol to `obj` at the given address with given size. +fn add_func_symbol(obj: &mut ObjInfo, name: &str, addr: u32, size: u32, scope: ObjSymbolScope) { + let mut flags = ObjSymbolFlagSet::default(); + flags.set_scope(scope); + obj.add_symbol( + ObjSymbol { + name: name.to_string(), + address: addr as u64, + section: Some(0), + size: size as u64, + size_known: true, + kind: ObjSymbolKind::Function, + flags, + ..Default::default() + }, + false, + ) + .unwrap(); +} + +// ========================================================================= +// Helper function tests +// ========================================================================= + +#[test] +fn test_is_unconditional_blr() { + use powerpc::{Extensions, Ins}; + let blr = Ins::new(BLR, Extensions::xenon()); + assert!(AnalyzerState::is_unconditional_blr(&blr)); + + let nop = Ins::new(NOP, Extensions::xenon()); + assert!(!AnalyzerState::is_unconditional_blr(&nop)); + + // blrl (link bit set) should not count + let blrl = Ins::new(0x4E800021, Extensions::xenon()); + assert!(!AnalyzerState::is_unconditional_blr(&blrl)); +} + +#[test] +fn test_branch_into_range() { + use powerpc::{Extensions, Ins}; + // b -0xC at address 0x1010 -> target 0x1004 + let ins = Ins::new(ppc_b(-0xC), Extensions::xenon()); + let result = AnalyzerState::branch_into_range(&ins, 0x1010, 0x1000, 0x1010); + assert_eq!(result, Some(0x1004)); + + // Same branch but range doesn't contain target + let result = AnalyzerState::branch_into_range(&ins, 0x1010, 0x1008, 0x1010); + assert_eq!(result, None); + + // bl (link bit) should return None + let bl_ins = Ins::new(ppc_bl(-0xC), Extensions::xenon()); + let result = AnalyzerState::branch_into_range(&bl_ins, 0x1010, 0x1000, 0x1010); + assert_eq!(result, None); +} + +// ========================================================================= +// check_tail_block tests +// ========================================================================= + +/// Case 1: Classic tail block -- starts with `b` back into preceding function, ends with blr. +#[test] +fn test_tail_block_case1_backward_branch_then_blr() { + let section = make_code_section(0x1000, &[ + NOP, NOP, NOP, NOP, // preceding func body + ppc_b(-0xC), // b 0x1004 (back into preceding) + ADDI_R3, // addi r3, r3, 1 + BLR, // blr + ]); + + let result = AnalyzerState::check_tail_block( + §ion, + SectionAddress::new(0, 0x1010), + SectionAddress::new(0, 0x101C), + SectionAddress::new(0, 0x1000), + SectionAddress::new(0, 0x1010), + ); + assert_eq!(result, Some(SectionAddress::new(0, 0x101C))); +} + +/// Case 2: Conditional backward branch + blr. +#[test] +fn test_tail_block_case2_conditional_backward_branch_with_blr() { + let section = make_code_section(0x1000, &[ + NOP, NOP, NOP, NOP, // preceding func + ADDI_R3, // 0x1010 + ppc_bne(-0x14), // 0x1014: bne -> 0x1004 + BLR, // 0x1018: blr + ]); + + let result = AnalyzerState::check_tail_block( + §ion, + SectionAddress::new(0, 0x1010), + SectionAddress::new(0, 0x101C), + SectionAddress::new(0, 0x1000), + SectionAddress::new(0, 0x1010), + ); + assert_eq!(result, Some(SectionAddress::new(0, 0x101C))); +} + +/// Not a tail block: gap contains a function call (bl). +#[test] +fn test_not_tail_block_contains_call() { + let section = make_code_section(0x1000, &[ + NOP, NOP, NOP, NOP, + ppc_bl(0x100), // bl (function call) + BLR, + ]); + + let result = AnalyzerState::check_tail_block( + §ion, + SectionAddress::new(0, 0x1010), + SectionAddress::new(0, 0x1018), + SectionAddress::new(0, 0x1000), + SectionAddress::new(0, 0x1010), + ); + assert_eq!(result, None); +} + +/// Not a tail block: forward branch to another function. +#[test] +fn test_not_tail_block_forward_branch() { + let section = make_code_section(0x1000, &[ + NOP, NOP, NOP, NOP, + ppc_b(0x100), // b 0x1110 (forward) + BLR, + ]); + + let result = AnalyzerState::check_tail_block( + §ion, + SectionAddress::new(0, 0x1010), + SectionAddress::new(0, 0x1018), + SectionAddress::new(0, 0x1000), + SectionAddress::new(0, 0x1010), + ); + assert_eq!(result, None); +} + +/// Not a tail block: gap too large (> 64 bytes). +#[test] +fn test_not_tail_block_too_large() { + let mut insns = vec![NOP; 4]; // preceding func + insns.extend(std::iter::repeat(NOP).take(20)); // 80 bytes + let section = make_code_section(0x1000, &insns); + + let result = AnalyzerState::check_tail_block( + §ion, + SectionAddress::new(0, 0x1010), + SectionAddress::new(0, 0x1060), // 80 bytes > 64 + SectionAddress::new(0, 0x1000), + SectionAddress::new(0, 0x1010), + ); + assert_eq!(result, None); +} + +/// Not a tail block: backward branch but no blr. +#[test] +fn test_not_tail_block_no_blr() { + let section = make_code_section(0x1000, &[ + NOP, NOP, NOP, NOP, + ADDI_R3, + ppc_bne(-0x14), // bne -> 0x1004 + NOP, // no blr + ]); + + let result = AnalyzerState::check_tail_block( + §ion, + SectionAddress::new(0, 0x1010), + SectionAddress::new(0, 0x101C), + SectionAddress::new(0, 0x1000), + SectionAddress::new(0, 0x1010), + ); + assert_eq!(result, None); +} + +/// Not a tail block: contains bctr. +#[test] +fn test_not_tail_block_indirect_branch() { + let section = make_code_section(0x1000, &[ + NOP, NOP, NOP, NOP, + BCTR, + ]); + + let result = AnalyzerState::check_tail_block( + §ion, + SectionAddress::new(0, 0x1010), + SectionAddress::new(0, 0x1014), + SectionAddress::new(0, 0x1000), + SectionAddress::new(0, 0x1010), + ); + assert_eq!(result, None); +} + +/// Case 1 variant: blr found before gap_end (tail block shorter than gap). +#[test] +fn test_tail_block_case1_blr_before_gap_end() { + let section = make_code_section(0x1000, &[ + NOP, NOP, NOP, NOP, + ppc_b(-0xC), // b 0x1004 + BLR, // blr at 0x1014 + NOP, // padding + ]); + + let result = AnalyzerState::check_tail_block( + §ion, + SectionAddress::new(0, 0x1010), + SectionAddress::new(0, 0x101C), // gap extends past blr + SectionAddress::new(0, 0x1000), + SectionAddress::new(0, 0x1010), + ); + assert_eq!(result, Some(SectionAddress::new(0, 0x1018))); +} + +/// Exactly at MAX_TAIL_BLOCK_BYTES boundary (64 bytes = 16 instructions). +#[test] +fn test_tail_block_at_max_size_boundary() { + let mut insns = vec![NOP; 4]; // preceding func (0x1000..0x1010) + // 16-instruction tail block (exactly 64 bytes, 0x1010..0x1050) + for _ in 0..14 { + insns.push(NOP); + } + insns.push(ppc_b(-0x40)); // b back into preceding func + insns.push(BLR); + + let section = make_code_section(0x1000, &insns); + + let result = AnalyzerState::check_tail_block( + §ion, + SectionAddress::new(0, 0x1010), + SectionAddress::new(0, 0x1050), // exactly 64 bytes + SectionAddress::new(0, 0x1000), + SectionAddress::new(0, 0x1010), + ); + assert_eq!(result, Some(SectionAddress::new(0, 0x1050))); +} + +// ========================================================================= +// merge_tail_blocks tests +// ========================================================================= + +/// Test that merge_tail_blocks merges a simple tail block into its predecessor. +#[test] +fn test_merge_tail_blocks_basic() { + let mut obj = make_test_obj(0x1000, &[ + NOP, NOP, NOP, NOP, + ppc_b(-0xC), + ADDI_R3, + BLR, + ]); + + add_func_symbol(&mut obj, "fn_00001000", 0x1000, 0x10, ObjSymbolScope::Local); + add_func_symbol(&mut obj, "fn_00001010", 0x1010, 0x0C, ObjSymbolScope::Local); + + let mut state = AnalyzerState::default(); + state.functions.insert( + SectionAddress::new(0, 0x1000), + FunctionInfo { + analyzed: true, + end: Some(SectionAddress::new(0, 0x1010)), + slices: None, + }, + ); + state.functions.insert( + SectionAddress::new(0, 0x1010), + FunctionInfo { + analyzed: true, + end: Some(SectionAddress::new(0, 0x101C)), + slices: None, + }, + ); + + state.merge_tail_blocks(&obj).unwrap(); + + assert!(!state.functions.contains_key(&SectionAddress::new(0, 0x1010))); + let func1 = state.functions.get(&SectionAddress::new(0, 0x1000)).unwrap(); + assert_eq!(func1.end, Some(SectionAddress::new(0, 0x101C))); + assert!(state.merged_tail_blocks.contains(&SectionAddress::new(0, 0x1010))); + assert!(state.extended_functions.contains(&SectionAddress::new(0, 0x1000))); +} + +/// Test that merge_tail_blocks skips functions with global-scope symbols. +#[test] +fn test_merge_tail_blocks_preserves_global_scope() { + let mut obj = make_test_obj(0x1000, &[ + NOP, NOP, NOP, NOP, + ppc_b(-0xC), + ADDI_R3, + BLR, + ]); + + add_func_symbol(&mut obj, "fn_00001000", 0x1000, 0x10, ObjSymbolScope::Local); + add_func_symbol(&mut obj, "RealFunction", 0x1010, 0x0C, ObjSymbolScope::Global); + + let mut state = AnalyzerState::default(); + state.functions.insert( + SectionAddress::new(0, 0x1000), + FunctionInfo { + analyzed: true, + end: Some(SectionAddress::new(0, 0x1010)), + slices: None, + }, + ); + state.functions.insert( + SectionAddress::new(0, 0x1010), + FunctionInfo { + analyzed: true, + end: Some(SectionAddress::new(0, 0x101C)), + slices: None, + }, + ); + + state.merge_tail_blocks(&obj).unwrap(); + + // Both functions should still exist + assert!(state.functions.contains_key(&SectionAddress::new(0, 0x1000))); + assert!(state.functions.contains_key(&SectionAddress::new(0, 0x1010))); + assert!(state.merged_tail_blocks.is_empty()); + assert!(state.extended_functions.is_empty()); +} + +/// Test that apply() marks merged tail block symbols as deleted. +#[test] +fn test_apply_removes_merged_tail_block_symbols() { + let mut obj = make_test_obj(0x1000, &[ + NOP, NOP, NOP, NOP, + ppc_b(-0xC), + ADDI_R3, + BLR, + ]); + + add_func_symbol(&mut obj, "fn_00001010", 0x1010, 0x0C, ObjSymbolScope::Local); + + let mut state = AnalyzerState::default(); + state.functions.insert( + SectionAddress::new(0, 0x1000), + FunctionInfo { + analyzed: true, + end: Some(SectionAddress::new(0, 0x101C)), + slices: None, + }, + ); + state.merged_tail_blocks.push(SectionAddress::new(0, 0x1010)); + + state.apply(&mut obj).unwrap(); + + let result = + obj.symbols.kind_at_section_address(0, 0x1010, ObjSymbolKind::Function).unwrap(); + assert!(result.is_none(), "Function symbol should be stripped/deleted after apply"); +} + +/// Test that apply() updates symbol sizes for extended functions. +#[test] +fn test_apply_extends_function_size() { + let mut obj = make_test_obj(0x1000, &[ + NOP, NOP, NOP, NOP, + ppc_b(-0xC), + ADDI_R3, + BLR, + ]); + + add_func_symbol(&mut obj, "fn_00001000", 0x1000, 0x10, ObjSymbolScope::Local); + + let mut state = AnalyzerState::default(); + state.functions.insert( + SectionAddress::new(0, 0x1000), + FunctionInfo { + analyzed: true, + end: Some(SectionAddress::new(0, 0x101C)), + slices: None, + }, + ); + state.extended_functions.push(SectionAddress::new(0, 0x1000)); + + state.apply(&mut obj).unwrap(); + + let (_, sym) = obj + .symbols + .kind_at_section_address(0, 0x1000, ObjSymbolKind::Function) + .unwrap() + .expect("function symbol should exist"); + assert_eq!(sym.size, 0x1C, "symbol size should be extended to 0x1C"); + assert!(sym.size_known); +} + +/// Test merging multiple sequential tail blocks into one predecessor. +#[test] +fn test_merge_multiple_sequential_tail_blocks() { + let mut obj = make_test_obj(0x1000, &[ + NOP, NOP, NOP, NOP, // func1 body (0x1000..0x1010) + ppc_b(-0xC), // 0x1010: b 0x1004 + ADDI_R3, // 0x1014 + BLR, // 0x1018: blr + ppc_b(-0x18), // 0x101C: b 0x1008 (back into func1) + BLR, // 0x1020: blr + ]); + + add_func_symbol(&mut obj, "fn_00001000", 0x1000, 0x10, ObjSymbolScope::Local); + add_func_symbol(&mut obj, "fn_00001010", 0x1010, 0x0C, ObjSymbolScope::Local); + add_func_symbol(&mut obj, "fn_0000101C", 0x101C, 0x08, ObjSymbolScope::Local); + + let mut state = AnalyzerState::default(); + state.functions.insert( + SectionAddress::new(0, 0x1000), + FunctionInfo { + analyzed: true, + end: Some(SectionAddress::new(0, 0x1010)), + slices: None, + }, + ); + state.functions.insert( + SectionAddress::new(0, 0x1010), + FunctionInfo { + analyzed: true, + end: Some(SectionAddress::new(0, 0x101C)), + slices: None, + }, + ); + state.functions.insert( + SectionAddress::new(0, 0x101C), + FunctionInfo { + analyzed: true, + end: Some(SectionAddress::new(0, 0x1024)), + slices: None, + }, + ); + + state.merge_tail_blocks(&obj).unwrap(); + + assert!(!state.functions.contains_key(&SectionAddress::new(0, 0x1010))); + assert!(state.merged_tail_blocks.contains(&SectionAddress::new(0, 0x1010))); + + let func1 = state.functions.get(&SectionAddress::new(0, 0x1000)).unwrap(); + assert!(func1.end.unwrap().address >= 0x101C); +} + +/// Test that non-adjacent functions are not merged. +#[test] +fn test_merge_tail_blocks_skips_non_adjacent() { + let mut obj = make_test_obj(0x1000, &[ + NOP, NOP, NOP, NOP, // func1 (0x1000..0x1010) + NOP, // gap (0x1010..0x1014) + ppc_b(-0x10), // 0x1014: b 0x1004 + BLR, // 0x1018 + ]); + + add_func_symbol(&mut obj, "fn_00001000", 0x1000, 0x10, ObjSymbolScope::Local); + add_func_symbol(&mut obj, "fn_00001014", 0x1014, 0x08, ObjSymbolScope::Local); + + let mut state = AnalyzerState::default(); + state.functions.insert( + SectionAddress::new(0, 0x1000), + FunctionInfo { + analyzed: true, + end: Some(SectionAddress::new(0, 0x1010)), + slices: None, + }, + ); + state.functions.insert( + SectionAddress::new(0, 0x1014), + FunctionInfo { + analyzed: true, + end: Some(SectionAddress::new(0, 0x101C)), + slices: None, + }, + ); + + state.merge_tail_blocks(&obj).unwrap(); + + assert!(state.functions.contains_key(&SectionAddress::new(0, 0x1000))); + assert!(state.functions.contains_key(&SectionAddress::new(0, 0x1014))); + assert!(state.merged_tail_blocks.is_empty()); +} + +/// Test FunctionInfo state detection methods. +#[test] +fn test_function_info_states() { + let default_info = FunctionInfo::default(); + assert!(!default_info.is_analyzed()); + assert!(!default_info.is_function()); + assert!(!default_info.is_non_function()); + assert!(!default_info.is_unfinalized()); + + let non_function = FunctionInfo { analyzed: true, end: None, slices: None }; + assert!(non_function.is_non_function()); + + let unfinalized = FunctionInfo { + analyzed: true, + end: None, + slices: Some(FunctionSlices::default()), + }; + assert!(unfinalized.is_unfinalized()); + + let complete = FunctionInfo { + analyzed: true, + end: Some(SectionAddress::new(0, 0x100)), + slices: Some(FunctionSlices::default()), + }; + assert!(complete.is_function()); +}