Skip to main content

nabla_decompiler/
analysis.rs

1//! Function identification and analysis
2
3use anyhow::Result;
4use std::collections::HashSet;
5use uuid::Uuid;
6
7use crate::cfg_analyzer::{CfgAnalyzer, CfgAnalyzerRegistry};
8use crate::types::{
9    Address, BasicBlock, BlockType, Disassembly, Function, 
10    Instruction, InstructionGroup
11};
12
13/// Identify functions from disassembly using YARA rules for better accuracy
14pub fn identify_functions_with_yara(disasm: &Disassembly, arch: &str, format: &str) -> Result<Vec<Function>> {
15    let registry = CfgAnalyzerRegistry::new();
16    let analyzer = registry.get_analyzer(arch, format)
17        .ok_or_else(|| anyhow::anyhow!("No analyzer found for architecture: {} format: {}", arch, format))?;
18    
19    let mut functions = Vec::new();
20    let mut processed_addresses = HashSet::new();
21    
22    // Find function entry points using YARA rules
23    let entry_points = find_function_entry_points_with_yara(disasm, analyzer)?;
24    
25    for &entry_point in &entry_points {
26        if processed_addresses.contains(&entry_point) {
27            continue;
28        }
29        
30        if let Ok(function) = analyze_function_from_entry(disasm, entry_point, analyzer) {
31            // Mark all addresses in this function as processed
32            for &addr in &function.instructions {
33                processed_addresses.insert(addr);
34            }
35            functions.push(function);
36        }
37    }
38    
39    Ok(functions)
40}
41
42/// Identify functions from disassembly (fallback with default ARM analyzer)
43pub fn identify_functions(disasm: &Disassembly) -> Result<Vec<Function>> {
44    identify_functions_with_yara(disasm, "arm", "elf")
45}
46
47/// Analyze a single function starting from an address with architecture detection
48pub fn analyze_single_function_with_arch(disasm: &Disassembly, address: Address, arch: &str, format: &str) -> Result<Function> {
49    let registry = CfgAnalyzerRegistry::new();
50    let analyzer = registry.get_analyzer(arch, format)
51        .ok_or_else(|| anyhow::anyhow!("No analyzer found for architecture: {} format: {}", arch, format))?;
52    
53    analyze_function_from_entry(disasm, address, analyzer)
54}
55
56/// Analyze a single function starting from an address (fallback with default ARM analyzer)
57pub fn analyze_single_function(disasm: &Disassembly, address: Address) -> Result<Function> {
58    analyze_single_function_with_arch(disasm, address, "arm", "elf")
59}
60
61/// Find potential function entry points using YARA rules and architecture-specific analyzer
62fn find_function_entry_points_with_yara(disasm: &Disassembly, analyzer: &dyn CfgAnalyzer) -> Result<Vec<Address>> {
63    let mut entry_points = Vec::new();
64    
65    // Use YARA rules to classify symbols and filter for functions only
66    let function_symbols = classify_function_symbols_with_yara(&disasm.symbols)?;
67    entry_points.extend(function_symbols);
68    
69    // Find call targets using analyzer
70    for instruction in &disasm.instructions {
71        if analyzer.classify_instruction(instruction) == InstructionGroup::Call {
72            if let Some(target) = analyzer.extract_jump_target(instruction) {
73                entry_points.push(target);
74            }
75        }
76    }
77    
78    // Look for function prologues (common patterns)
79    for window in disasm.instructions.windows(2) {
80        if is_function_prologue(&window) {
81            entry_points.push(window[0].address);
82        }
83    }
84    
85    entry_points.sort();
86    entry_points.dedup();
87    Ok(entry_points)
88}
89
90/// Analyze function starting from entry point using architecture-specific analyzer
91fn analyze_function_from_entry(disasm: &Disassembly, entry_point: Address, analyzer: &dyn CfgAnalyzer) -> Result<Function> {
92    let mut instructions = Vec::new();
93    let mut exit_points = Vec::new();
94    let mut calls = Vec::new();
95    let _current_addr = entry_point;
96    
97    // Simple linear sweep to find function boundaries
98    let mut visited = HashSet::new();
99    let mut to_process = vec![entry_point];
100    
101    while let Some(addr) = to_process.pop() {
102        if visited.contains(&addr) {
103            continue;
104        }
105        visited.insert(addr);
106        
107        if let Some(instruction) = find_instruction_at_address(disasm, addr) {
108            instructions.push(instruction.address);
109            
110            let instruction_group = analyzer.classify_instruction(instruction);
111            match instruction_group {
112                InstructionGroup::Return => {
113                    exit_points.push(instruction.address);
114                    // Don't follow returns
115                }
116                InstructionGroup::Call => {
117                    if let Some(target) = analyzer.extract_jump_target(instruction) {
118                        calls.push(target);
119                    }
120                    // Continue to next instruction after call
121                    if let Some(next_addr) = analyzer.fall_through_address(instruction) {
122                        to_process.push(next_addr);
123                    }
124                }
125                InstructionGroup::Jump => {
126                    if let Some(target) = analyzer.extract_jump_target(instruction) {
127                        to_process.push(target);
128                    }
129                    // For conditional jumps, also follow fall-through
130                    if analyzer.is_conditional(instruction) {
131                        if let Some(next_addr) = analyzer.fall_through_address(instruction) {
132                            to_process.push(next_addr);
133                        }
134                    }
135                }
136                _ => {
137                    // Continue to next instruction
138                    if let Some(next_addr) = analyzer.fall_through_address(instruction) {
139                        to_process.push(next_addr);
140                    }
141                }
142            }
143        } else {
144            // No instruction found, probably end of function
145            break;
146        }
147        
148        // Limit function size to prevent runaway analysis
149        if instructions.len() > 1000 {
150            break;
151        }
152    }
153    
154    instructions.sort();
155    
156    // Build basic blocks with analyzer
157    let basic_blocks = build_basic_blocks_with_analyzer(&instructions, disasm, analyzer)?;
158    
159    let function_size = if let (Some(&first), Some(&last)) = (instructions.first(), instructions.last()) {
160        last - first + 4 // Approximate size
161    } else {
162        0
163    };
164    
165    // Try to get function name from symbols with fuzzy lookup
166    tracing::debug!("Looking for symbol at function address 0x{:x}, total symbols: {}", entry_point, disasm.symbols.len());
167    if disasm.symbols.len() > 0 && disasm.symbols.len() < 20 {
168        for (&addr, name) in &disasm.symbols {
169            tracing::debug!("  Available symbol: '{}' at 0x{:x}", name, addr);
170        }
171    }
172    let name = find_closest_symbol(&disasm.symbols, entry_point);
173    
174    Ok(Function {
175        address: entry_point,
176        name,
177        size: function_size,
178        instructions,
179        basic_blocks,
180        entry_point,
181        exit_points,
182        calls,
183        called_by: Vec::new(), // Will be filled in later pass
184    })
185}
186
187/// Build basic blocks for a function using architecture-specific analyzer
188fn build_basic_blocks_with_analyzer(instructions: &[Address], disasm: &Disassembly, analyzer: &dyn CfgAnalyzer) -> Result<Vec<BasicBlock>> {
189    if instructions.is_empty() {
190        tracing::debug!("No instructions provided for basic block analysis");
191        return Ok(Vec::new());
192    }
193    
194    tracing::debug!("Building basic blocks from {} instructions", instructions.len());
195    
196    // Find basic block boundaries
197    let mut block_starts = HashSet::new();
198    block_starts.insert(instructions[0]); // Function entry
199    
200    tracing::debug!("Initial block start at 0x{:x}", instructions[0]);
201    
202    // Add targets of jumps and branches using analyzer
203    let mut jump_targets_found = 0;
204    let mut valid_instruction_count = 0;
205    
206    for &addr in instructions {
207        if let Some(instruction) = find_instruction_at_address(disasm, addr) {
208            let instruction_group = analyzer.classify_instruction(instruction);
209            tracing::debug!("Analyzing instruction at 0x{:x}: {} {} (group: {:?})", 
210                      addr, instruction.mnemonic, instruction.operands, instruction_group);
211            
212            // Count valid instructions (filter out data/invalid instructions)
213            if !instruction.mnemonic.is_empty() && instruction.mnemonic != "???" {
214                valid_instruction_count += 1;
215            }
216            
217            match instruction_group {
218                InstructionGroup::Jump | InstructionGroup::Call => {
219                    // Next instruction after jump/call is a block start
220                    if let Some(next_addr) = analyzer.fall_through_address(instruction) {
221                        if instructions.contains(&next_addr) {
222                            tracing::debug!("  Adding fall-through block start at 0x{:x}", next_addr);
223                            block_starts.insert(next_addr);
224                        }
225                    }
226                    
227                    // Jump target is a block start
228                    if let Some(target) = analyzer.extract_jump_target(instruction) {
229                        if instructions.contains(&target) {
230                            tracing::debug!("  Adding jump target block start at 0x{:x}", target);
231                            block_starts.insert(target);
232                            jump_targets_found += 1;
233                        } else {
234                            tracing::debug!("  Jump target 0x{:x} is outside function", target);
235                        }
236                    }
237                }
238                InstructionGroup::Return => {
239                    // Next instruction after return is a block start (if it exists)
240                    if let Some(next_addr) = analyzer.fall_through_address(instruction) {
241                        if instructions.contains(&next_addr) {
242                            tracing::debug!("  Adding post-return block start at 0x{:x}", next_addr);
243                            block_starts.insert(next_addr);
244                        }
245                    }
246                }
247                _ => {}
248            }
249        }
250    }
251    
252    // If we have a very long sequence of instructions with no jumps detected,
253    // it might be data misinterpreted as instructions. Split it artificially for better visualization.
254    if jump_targets_found == 0 && valid_instruction_count > 20 {
255        tracing::debug!("Long instruction sequence detected ({} instructions), adding artificial splits", valid_instruction_count);
256        
257        // Split every 10-15 instructions for better CFG visualization
258        let chunk_size = 12;
259        for (i, &addr) in instructions.iter().enumerate() {
260            if i > 0 && i % chunk_size == 0 && i < instructions.len() - 1 {
261                tracing::debug!("  Adding artificial block split at 0x{:x} (position {})", addr, i);
262                block_starts.insert(addr);
263                jump_targets_found += 1; // Count as found to avoid warnings
264            }
265        }
266    }
267    
268    tracing::debug!("Found {} jump targets, total block starts: {}", jump_targets_found, block_starts.len());
269    
270    let mut block_starts: Vec<Address> = block_starts.into_iter().collect();
271    block_starts.sort();
272    
273    tracing::debug!("Block starts: {:?}", block_starts.iter().map(|&addr| format!("0x{:x}", addr)).collect::<Vec<_>>());
274    
275    // Create basic blocks
276    let mut basic_blocks = Vec::new();
277    
278    for i in 0..block_starts.len() {
279        let start_addr = block_starts[i];
280        let end_addr = if i + 1 < block_starts.len() {
281            block_starts[i + 1]
282        } else {
283            instructions.last().copied().unwrap_or(start_addr) + 4
284        };
285        
286        let block_instructions: Vec<Address> = instructions.iter()
287            .filter(|&&addr| addr >= start_addr && addr < end_addr)
288            .copied()
289            .collect();
290        
291        if !block_instructions.is_empty() {
292            let block_type = if i == 0 {
293                BlockType::Entry
294            } else if i == block_starts.len() - 1 {
295                BlockType::Exit
296            } else {
297                BlockType::Normal
298            };
299            
300            tracing::debug!("Created basic block {}: 0x{:x}-0x{:x} ({} instructions, type: {:?})", 
301                      i, start_addr, end_addr, block_instructions.len(), block_type);
302            
303            basic_blocks.push(BasicBlock {
304                id: Uuid::new_v4(),
305                start_address: start_addr,
306                end_address: end_addr,
307                instructions: block_instructions,
308                predecessors: Vec::new(), // Will be filled in later
309                successors: Vec::new(),   // Will be filled in later
310                block_type,
311            });
312        }
313    }
314    
315    tracing::debug!("Final result: {} basic blocks created", basic_blocks.len());
316    
317    Ok(basic_blocks)
318}
319
320
321/// Find instruction at specific address
322fn find_instruction_at_address(disasm: &Disassembly, address: Address) -> Option<&Instruction> {
323    disasm.instructions.iter().find(|insn| insn.address == address)
324}
325
326/// Check if instructions form a function prologue
327fn is_function_prologue(instructions: &[Instruction]) -> bool {
328    if instructions.len() < 2 {
329        return false;
330    }
331    
332    let first = &instructions[0];
333    let second = &instructions[1];
334    
335    // x86/x64 function prologue patterns
336    if (first.mnemonic == "push" && first.operands.contains("ebp")) &&
337       (second.mnemonic == "mov" && second.operands.contains("ebp") && second.operands.contains("esp")) {
338        return true; // push ebp; mov ebp, esp (x86)
339    }
340    
341    if (first.mnemonic == "push" && first.operands.contains("rbp")) &&
342       (second.mnemonic == "mov" && second.operands.contains("rbp") && second.operands.contains("rsp")) {
343        return true; // push rbp; mov rbp, rsp (x64)
344    }
345    
346    // Common x86 stack allocation
347    if first.mnemonic == "sub" && first.operands.contains("esp") {
348        return true; // sub esp, imm
349    }
350    
351    if first.mnemonic == "sub" && first.operands.contains("rsp") {
352        return true; // sub rsp, imm
353    }
354    
355    // Single push ebp/rbp (common in optimized code)
356    if first.mnemonic == "push" && (first.operands.contains("ebp") || first.operands.contains("rbp")) {
357        return true;
358    }
359    
360    // Common ARM function prologue patterns
361    // push {r4, r5, r6, r7, lr} or similar
362    (first.mnemonic == "push" && first.operands.contains("lr")) ||
363    // stmfd sp!, {r4, r5, r6, r7, lr}
364    (first.mnemonic == "stmfd" && first.operands.contains("sp!") && first.operands.contains("lr")) ||
365    // mov r7, sp (frame pointer setup)
366    (first.mnemonic == "mov" && first.operands.contains("r7") && first.operands.contains("sp"))
367}
368
369/// Find the closest symbol to a given address (within reasonable range)
370fn find_closest_symbol(symbols: &std::collections::HashMap<Address, String>, target_address: Address) -> Option<String> {
371    // First try exact match
372    if let Some(name) = symbols.get(&target_address) {
373        return Some(name.clone());
374    }
375    
376    // If no exact match, find the closest symbol within a reasonable range
377    // This accounts for function alignment, padding, and address calculations
378    let mut closest_distance = u64::MAX;
379    let mut closest_symbol = None;
380    
381    for (&symbol_addr, symbol_name) in symbols {
382        let distance = if symbol_addr <= target_address {
383            target_address - symbol_addr
384        } else {
385            symbol_addr - target_address
386        };
387        
388        // Consider symbols within 256 bytes of the target (both before and after)
389        // This handles cases where symbol addresses may be calculated differently
390        if distance <= 256 && distance < closest_distance {
391            closest_distance = distance;
392            closest_symbol = Some(symbol_name.clone());
393            
394            // Log the match for debugging
395            tracing::debug!("Found symbol '{}' at 0x{:x} for function at 0x{:x} (distance: {})", 
396                symbol_name, symbol_addr, target_address, distance);
397        }
398    }
399    
400    if closest_symbol.is_none() {
401        tracing::debug!("No symbol found for function at 0x{:x}", target_address);
402    }
403    
404    closest_symbol
405}
406
407/// Classify symbols using YARA rules to identify which ones are actually functions
408fn classify_function_symbols_with_yara(symbols: &std::collections::HashMap<Address, String>) -> Result<Vec<Address>> {
409    let mut function_addresses = Vec::new();
410    
411    // First use basic heuristics to filter obvious function symbols
412    for (&addr, name) in symbols {
413        if is_likely_function_symbol(name) {
414            function_addresses.push(addr);
415        }
416    }
417    
418    tracing::debug!("Basic heuristics identified {} potential functions from {} symbols", 
419        function_addresses.len(), symbols.len());
420    
421    Ok(function_addresses)
422}
423
424/// Check if a symbol name is likely to be a function
425fn is_likely_function_symbol(name: &str) -> bool {
426    // C++ mangled functions
427    if name.starts_with("_Z") {
428        return true;
429    }
430    
431    // Function-like names
432    if name.contains("()") || name.ends_with("()") {
433        return true;
434    }
435    
436    // Common function names
437    let function_keywords = ["main", "init", "start", "setup", "run", "execute", 
438                           "create", "destroy", "handle", "process", "update"];
439    if function_keywords.iter().any(|&keyword| name.to_lowercase().contains(keyword)) {
440        return true;
441    }
442    
443    // Avoid obvious data symbols
444    let data_keywords = ["_data", "_rodata", "_bss", "String", "string", 
445                        "variable", "const", "static", "_var", "__"];
446    if data_keywords.iter().any(|&keyword| name.to_lowercase().contains(keyword)) {
447        return false;
448    }
449    
450    // If it contains common programming patterns, likely a function
451    if name.contains("::") || name.contains("get") || name.contains("set") {
452        return true;
453    }
454    
455    // Default: if it's not obviously data, consider it a potential function
456    // This is conservative but better than including everything
457    !name.chars().any(|c| c.is_ascii_digit()) || name.len() > 8
458}
459