1use crate::{
7 BinaryError, BinaryFile, Result,
8 disasm::{Disassembler, DisassemblyConfig, section_file_data},
9 types::{
10 CallContext, CallGraph, CallGraphConfig, CallGraphEdge, CallGraphNode, CallGraphStatistics,
11 CallSite, CallType, Function, Instruction, NodeType,
12 },
13};
14use petgraph::{algo::kosaraju_scc, graphmap::DiGraphMap};
15use std::collections::{HashMap, HashSet, VecDeque};
16
17type AddressToNode = HashMap<u64, usize>;
18type CallAdjacency = HashMap<u64, Vec<u64>>;
19
20#[derive(Clone)]
22pub struct CallGraphAnalyzer {
23 config: CallGraphConfig,
25}
26
27impl CallGraphAnalyzer {
28 pub fn new(config: CallGraphConfig) -> Self {
30 Self { config }
31 }
32
33 pub fn new_default() -> Self {
35 Self {
36 config: CallGraphConfig::default(),
37 }
38 }
39
40 pub fn analyze_binary(&self, binary: &BinaryFile) -> Result<CallGraph> {
42 let functions = self.extract_functions(binary)?;
44
45 let mut nodes = Vec::new();
47 let mut address_to_node: HashMap<u64, usize> = HashMap::new();
48 nodes
49 .try_reserve(functions.len())
50 .map_err(|error| Self::allocation_error("call-graph nodes", error))?;
51 address_to_node
52 .try_reserve(functions.len())
53 .map_err(|error| Self::allocation_error("call-graph address index", error))?;
54
55 for (i, function) in functions.iter().enumerate() {
56 let node = CallGraphNode {
57 function_address: function.start_address,
58 function_name: function.name.clone(),
59 node_type: self.classify_node_type(function, binary),
60 complexity: 0, in_degree: 0, out_degree: 0, is_recursive: false, call_depth: None, };
66 nodes.push(node);
67 address_to_node.insert(function.start_address, i);
68 }
69
70 let mut edges = self.extract_function_calls(binary, &functions, &address_to_node)?;
72 if !self.config.include_library_calls {
73 let mut library_addresses = HashSet::new();
74 library_addresses
75 .try_reserve(nodes.len())
76 .map_err(|error| Self::allocation_error("library address set", error))?;
77 library_addresses.extend(
78 nodes
79 .iter()
80 .filter(|node| matches!(node.node_type, NodeType::Library))
81 .map(|node| node.function_address),
82 );
83 edges.retain(|edge| {
84 !library_addresses.contains(&edge.caller)
85 && !edge
86 .callee
87 .is_some_and(|callee| library_addresses.contains(&callee))
88 });
89 }
90
91 self.update_node_degrees(&mut nodes, &edges);
93
94 self.detect_recursion(&mut nodes, &edges);
96
97 let entry_points = self.find_entry_points(binary, &functions);
99
100 let mut call_graph = CallGraph {
102 nodes,
103 edges,
104 entry_points: entry_points.clone(),
105 unreachable_functions: Vec::new(),
106 statistics: CallGraphStatistics::default(),
107 };
108
109 self.compute_call_depths(&mut call_graph)?;
110
111 call_graph.unreachable_functions = self.find_unreachable_functions(&call_graph)?;
113
114 call_graph.statistics = self.compute_statistics(&call_graph);
116
117 Ok(call_graph)
118 }
119
120 fn extract_functions(&self, binary: &BinaryFile) -> Result<Vec<Function>> {
122 let mut functions = Vec::new();
123 let mut seen_addresses = HashSet::new();
124 let reservation = binary
125 .symbols()
126 .len()
127 .min(self.config.max_functions.saturating_add(1));
128 functions
129 .try_reserve(reservation)
130 .map_err(|error| Self::allocation_error("discovered functions", error))?;
131 seen_addresses
132 .try_reserve(reservation)
133 .map_err(|error| Self::allocation_error("function address set", error))?;
134
135 for symbol in binary.symbols() {
137 if matches!(symbol.symbol_type, crate::types::SymbolType::Function)
138 && symbol.size > 0
139 && !seen_addresses.contains(&symbol.address)
140 {
141 self.ensure_function_limit(functions.len().saturating_add(1))?;
142 let function = Function {
143 name: symbol.name.clone(),
144 start_address: symbol.address,
145 end_address: symbol.address.checked_add(symbol.size).ok_or_else(|| {
146 BinaryError::invalid_data(format!(
147 "Function '{}' address range overflows",
148 symbol.name
149 ))
150 })?,
151 size: symbol.size,
152 function_type: crate::types::FunctionType::Normal,
153 calling_convention: None,
154 parameters: Vec::new(),
155 return_type: None,
156 };
157 functions.push(function);
158 seen_addresses.insert(symbol.address);
159 }
160 }
161
162 if let Some(entry_point) = binary
164 .entry_point()
165 .filter(|entry_point| !seen_addresses.contains(entry_point))
166 {
167 self.ensure_function_limit(functions.len().saturating_add(1))?;
168 let function = Function {
169 name: "_start".to_string(),
170 start_address: entry_point,
171 end_address: entry_point.checked_add(1000).ok_or_else(|| {
172 BinaryError::invalid_data("Entry-point address range overflows")
173 })?, size: 1000,
175 function_type: crate::types::FunctionType::Entrypoint,
176 calling_convention: None,
177 parameters: Vec::new(),
178 return_type: None,
179 };
180 functions.push(function);
181 }
182
183 functions.sort_by(|left, right| {
184 left.start_address
185 .cmp(&right.start_address)
186 .then_with(|| left.name.cmp(&right.name))
187 });
188 Ok(functions)
189 }
190
191 fn classify_node_type(&self, function: &Function, binary: &BinaryFile) -> NodeType {
193 if matches!(
195 function.function_type,
196 crate::types::FunctionType::Entrypoint
197 ) || matches!(function.function_type, crate::types::FunctionType::Main)
198 || function.name == "_start"
199 || function.name == "main"
200 || function.name == "DllMain"
201 {
202 return NodeType::EntryPoint;
203 }
204
205 for import in binary.imports() {
207 if import.address == Some(function.start_address) {
208 return NodeType::External;
209 }
210 }
211
212 if self.is_library_function(&function.name) {
214 return NodeType::Library;
215 }
216
217 NodeType::Internal
219 }
220
221 fn is_library_function(&self, name: &str) -> bool {
223 const LIBRARY_PREFIXES: &[&str] =
225 &["libc_", "libm_", "__", "_GLOBAL_", "std::", "_ZN", "_Z"];
226
227 const LIBRARY_NAMES: &[&str] = &[
229 "printf", "scanf", "malloc", "free", "strlen", "strcpy", "strcat", "memcpy", "memset",
230 "fopen", "fclose", "fread", "fwrite", "msvcrt", "kernel32", "ntdll",
231 ];
232
233 for prefix in LIBRARY_PREFIXES {
235 if name.starts_with(prefix) {
236 return true;
237 }
238 }
239
240 for lib_name in LIBRARY_NAMES {
243 if name == *lib_name {
244 return true;
245 }
246 }
247
248 false
249 }
250
251 fn extract_function_calls(
253 &self,
254 binary: &BinaryFile,
255 functions: &[Function],
256 address_to_node: &HashMap<u64, usize>,
257 ) -> Result<Vec<CallGraphEdge>> {
258 if functions.is_empty() {
259 return Ok(Vec::new());
260 }
261
262 let mut edges = Vec::new();
263 edges
264 .try_reserve(functions.len().min(self.config.max_total_instructions))
265 .map_err(|error| Self::allocation_error("call-graph edges", error))?;
266 let mut successful_functions = 0_usize;
267 let mut total_instructions = 0_usize;
268 let mut last_error = None;
269
270 for function in functions {
271 let remaining_instructions = self
272 .config
273 .max_total_instructions
274 .saturating_sub(total_instructions);
275 let decode_limit = remaining_instructions.saturating_add(1);
276
277 match self.get_function_instructions(binary, function, decode_limit) {
279 Ok(instructions) => {
280 let new_total = total_instructions
281 .checked_add(instructions.len())
282 .ok_or_else(|| {
283 BinaryError::control_flow(
284 "Call-graph instruction count overflowed usize",
285 )
286 })?;
287 if new_total > self.config.max_total_instructions {
288 return Err(BinaryError::control_flow(format!(
289 "Decoded instruction count {new_total} exceeds configured CallGraphConfig::max_total_instructions {}",
290 self.config.max_total_instructions
291 )));
292 }
293 total_instructions = new_total;
294 successful_functions += 1;
295
296 for instruction in &instructions {
298 if let Some(edge) = self.analyze_call_instruction(
299 instruction,
300 function.start_address,
301 address_to_node,
302 ) {
303 edges.try_reserve(1).map_err(|error| {
304 Self::allocation_error("call-graph edge", error)
305 })?;
306 edges.push(edge);
307 }
308 }
309
310 if self.config.detect_tail_calls
311 && let Some(edge) =
312 self.detect_tail_call(function, address_to_node, &instructions)
313 {
314 edges
315 .try_reserve(1)
316 .map_err(|error| Self::allocation_error("tail-call edge", error))?;
317 edges.push(edge);
318 }
319 }
320 Err(error) => last_error = Some(error),
321 }
322 }
323
324 if successful_functions == 0
325 && let Some(error) = last_error
326 {
327 return Err(error);
328 }
329
330 Ok(Self::merge_edges(edges))
331 }
332
333 fn get_function_instructions(
335 &self,
336 binary: &BinaryFile,
337 function: &Function,
338 max_instructions: usize,
339 ) -> Result<Vec<Instruction>> {
340 for section in binary.sections() {
342 let start = section.address;
343 let Some(end) = start.checked_add(section.size) else {
344 continue;
345 };
346
347 if section.permissions.execute
348 && function.start_address >= start
349 && function.start_address < end
350 {
351 let function_size = usize::try_from(function.size).unwrap_or(usize::MAX);
352 let data = section_file_data(
353 binary,
354 section,
355 function.start_address - start,
356 Some(function_size),
357 )?;
358 let length = data.len();
359 if length == 0 {
360 return Ok(Vec::new());
361 }
362
363 let disassembler = Disassembler::with_config(
364 binary.architecture(),
365 DisassemblyConfig {
366 max_instructions,
367 ..DisassemblyConfig::default()
368 },
369 )?;
370 return disassembler.disassemble_at(data, function.start_address, length);
371 }
372 }
373
374 Err(BinaryError::invalid_data(
375 "Function bytes not found in any executable section",
376 ))
377 }
378
379 fn ensure_function_limit(&self, actual: usize) -> Result<()> {
380 if actual > self.config.max_functions {
381 return Err(BinaryError::control_flow(format!(
382 "Discovered function count {actual} exceeds configured CallGraphConfig::max_functions {}",
383 self.config.max_functions
384 )));
385 }
386 Ok(())
387 }
388
389 fn allocation_error(context: &str, error: std::collections::TryReserveError) -> BinaryError {
390 BinaryError::control_flow(format!("Unable to reserve {context}: {error}"))
391 }
392
393 fn analyze_call_instruction(
395 &self,
396 instruction: &Instruction,
397 caller_address: u64,
398 address_to_node: &HashMap<u64, usize>,
399 ) -> Option<CallGraphEdge> {
400 match &instruction.flow {
401 crate::types::ControlFlow::Call(target_address) => {
402 if address_to_node.contains_key(target_address) {
404 let call_site = CallSite {
405 address: instruction.address,
406 instruction_bytes: instruction.bytes.clone(),
407 context: CallContext::Normal,
408 };
409
410 let call_type = if *target_address == caller_address {
411 CallType::Recursive
412 } else {
413 CallType::Direct
414 };
415
416 return Some(CallGraphEdge {
417 caller: caller_address,
418 callee: Some(*target_address),
419 call_type,
420 call_sites: vec![call_site],
421 });
422 }
423 }
424 _ => {
425 if self.config.analyze_indirect_calls {
427 return self.analyze_indirect_call(instruction, caller_address);
428 }
429 }
430 }
431
432 None
433 }
434
435 fn analyze_indirect_call(
437 &self,
438 instruction: &Instruction,
439 caller_address: u64,
440 ) -> Option<CallGraphEdge> {
441 let mnemonic = instruction.mnemonic.to_ascii_lowercase();
444 if matches!(
445 mnemonic.as_str(),
446 "call" | "callq" | "blr" | "blx" | "jalr" | "bctrl"
447 ) {
448 let call_site = CallSite {
450 address: instruction.address,
451 instruction_bytes: instruction.bytes.clone(),
452 context: CallContext::Normal,
453 };
454
455 return Some(CallGraphEdge {
458 caller: caller_address,
459 callee: None,
460 call_type: CallType::Indirect,
461 call_sites: vec![call_site],
462 });
463 }
464
465 None
466 }
467
468 fn merge_edges(mut edges: Vec<CallGraphEdge>) -> Vec<CallGraphEdge> {
469 edges.sort_by(|left, right| {
470 left.caller
471 .cmp(&right.caller)
472 .then_with(|| left.callee.cmp(&right.callee))
473 .then_with(|| {
474 Self::call_type_rank(&left.call_type)
475 .cmp(&Self::call_type_rank(&right.call_type))
476 })
477 });
478
479 let mut merged: Vec<CallGraphEdge> = Vec::with_capacity(edges.len());
480 for mut edge in edges {
481 if let Some(existing) = merged.last_mut().filter(|existing| {
482 existing.caller == edge.caller
483 && existing.callee == edge.callee
484 && existing.call_type == edge.call_type
485 }) {
486 existing.call_sites.append(&mut edge.call_sites);
487 continue;
488 }
489 merged.push(edge);
490 }
491
492 for edge in &mut merged {
493 edge.call_sites.sort_by(|left, right| {
494 left.address
495 .cmp(&right.address)
496 .then_with(|| left.instruction_bytes.cmp(&right.instruction_bytes))
497 });
498 edge.call_sites.dedup_by(|left, right| {
499 left.address == right.address
500 && left.instruction_bytes == right.instruction_bytes
501 && left.context == right.context
502 });
503 }
504 merged
505 }
506
507 fn call_type_rank(call_type: &CallType) -> u8 {
508 match call_type {
509 CallType::Direct => 0,
510 CallType::Indirect => 1,
511 CallType::TailCall => 2,
512 CallType::Virtual => 3,
513 CallType::Recursive => 4,
514 CallType::Conditional => 5,
515 }
516 }
517
518 fn detect_tail_call(
520 &self,
521 function: &Function,
522 address_to_node: &HashMap<u64, usize>,
523 instructions: &[Instruction],
524 ) -> Option<CallGraphEdge> {
525 instructions
527 .last()
528 .and_then(|last| {
529 if let crate::types::ControlFlow::Jump(target) = &last.flow {
530 (*target != function.start_address && address_to_node.contains_key(target))
531 .then_some((last, *target))
532 } else {
533 None
534 }
535 })
536 .map(|(last_instruction, target)| {
537 let call_site = CallSite {
538 address: last_instruction.address,
539 instruction_bytes: last_instruction.bytes.clone(),
540 context: CallContext::Normal,
541 };
542
543 CallGraphEdge {
544 caller: function.start_address,
545 callee: Some(target),
546 call_type: CallType::TailCall,
547 call_sites: vec![call_site],
548 }
549 })
550 }
551
552 fn update_node_degrees(&self, nodes: &mut [CallGraphNode], edges: &[CallGraphEdge]) {
554 let mut callers: HashMap<u64, HashSet<u64>> = HashMap::new();
555 let mut callees: HashMap<u64, HashSet<Option<u64>>> = HashMap::new();
556
557 for edge in edges {
558 callees.entry(edge.caller).or_default().insert(edge.callee);
559 if let Some(callee) = edge.callee {
560 callers.entry(callee).or_default().insert(edge.caller);
561 }
562 }
563
564 for node in nodes {
565 node.in_degree = callers
566 .get(&node.function_address)
567 .map_or(0, |values| u32::try_from(values.len()).unwrap_or(u32::MAX));
568 node.out_degree = callees
569 .get(&node.function_address)
570 .map_or(0, |values| u32::try_from(values.len()).unwrap_or(u32::MAX));
571 }
572 }
573
574 fn detect_recursion(&self, nodes: &mut [CallGraphNode], edges: &[CallGraphEdge]) {
576 let recursive_functions: HashSet<u64> = cyclic_components(nodes, edges)
577 .into_iter()
578 .flatten()
579 .collect();
580
581 for node in nodes {
583 node.is_recursive = recursive_functions.contains(&node.function_address);
584 }
585 }
586
587 fn find_entry_points(&self, binary: &BinaryFile, functions: &[Function]) -> Vec<u64> {
589 let mut entry_points = Vec::new();
590
591 if let Some(entry) = binary.entry_point() {
593 entry_points.push(entry);
594 }
595
596 for function in functions {
598 match function.function_type {
599 crate::types::FunctionType::Entrypoint | crate::types::FunctionType::Main
600 if !entry_points.contains(&function.start_address) =>
601 {
602 entry_points.push(function.start_address);
603 }
604 _ => {}
605 }
606 }
607
608 entry_points.sort_unstable();
609 entry_points.dedup();
610 entry_points
611 }
612
613 fn compute_call_depths(&self, call_graph: &mut CallGraph) -> Result<()> {
615 let (address_to_node, adjacency) = Self::graph_index(call_graph)?;
616
617 for node in &mut call_graph.nodes {
618 node.call_depth = None;
619 }
620
621 let mut queue = VecDeque::new();
623 queue
624 .try_reserve(call_graph.entry_points.len().min(call_graph.nodes.len()))
625 .map_err(|error| Self::allocation_error("call-depth queue", error))?;
626 for &entry_point in &call_graph.entry_points {
627 if let Some(&node_index) = address_to_node.get(&entry_point) {
628 call_graph.nodes[node_index].call_depth = Some(0);
629 queue.push_back(entry_point);
630 }
631 }
632
633 while let Some(current_addr) = queue.pop_front() {
634 let node_index = address_to_node[¤t_addr];
635 let depth = call_graph.nodes[node_index].call_depth.unwrap_or(0);
636 if self
637 .config
638 .max_labeled_call_depth
639 .is_some_and(|maximum| depth >= maximum)
640 {
641 continue;
642 }
643
644 let next_depth = depth.saturating_add(1);
645 if let Some(neighbors) = adjacency.get(¤t_addr) {
646 for &neighbor in neighbors {
647 let neighbor_index = address_to_node[&neighbor];
648 if call_graph.nodes[neighbor_index].call_depth.is_none() {
649 call_graph.nodes[neighbor_index].call_depth = Some(next_depth);
650 queue.push_back(neighbor);
651 }
652 }
653 }
654 }
655
656 Ok(())
657 }
658
659 fn find_unreachable_functions(&self, call_graph: &CallGraph) -> Result<Vec<u64>> {
661 let (address_to_node, adjacency) = Self::graph_index(call_graph)?;
662 let mut reachable = HashSet::new();
663 reachable
664 .try_reserve(call_graph.nodes.len())
665 .map_err(|error| Self::allocation_error("reachable function set", error))?;
666 let mut queue = VecDeque::new();
667 queue
668 .try_reserve(call_graph.nodes.len())
669 .map_err(|error| Self::allocation_error("reachability queue", error))?;
670
671 for &entry_point in &call_graph.entry_points {
672 if address_to_node.contains_key(&entry_point) && reachable.insert(entry_point) {
673 queue.push_back(entry_point);
674 }
675 }
676
677 while let Some(current) = queue.pop_front() {
678 if let Some(neighbors) = adjacency.get(¤t) {
679 for &neighbor in neighbors {
680 if reachable.insert(neighbor) {
681 queue.push_back(neighbor);
682 }
683 }
684 }
685 }
686
687 let mut unreachable = Vec::new();
688 unreachable
689 .try_reserve(call_graph.nodes.len().saturating_sub(reachable.len()))
690 .map_err(|error| Self::allocation_error("unreachable function list", error))?;
691 unreachable.extend(
692 call_graph
693 .nodes
694 .iter()
695 .filter(|node| !reachable.contains(&node.function_address))
696 .map(|node| node.function_address),
697 );
698 Ok(unreachable)
699 }
700
701 fn graph_index(call_graph: &CallGraph) -> Result<(AddressToNode, CallAdjacency)> {
702 let mut address_to_node = HashMap::new();
703 address_to_node
704 .try_reserve(call_graph.nodes.len())
705 .map_err(|error| Self::allocation_error("call-graph address index", error))?;
706 for (index, node) in call_graph.nodes.iter().enumerate() {
707 address_to_node.insert(node.function_address, index);
708 }
709
710 let mut adjacency: HashMap<u64, Vec<u64>> = HashMap::new();
711 adjacency
712 .try_reserve(call_graph.nodes.len())
713 .map_err(|error| Self::allocation_error("call-graph adjacency", error))?;
714 for edge in &call_graph.edges {
715 if !matches!(edge.call_type, CallType::Indirect)
716 && address_to_node.contains_key(&edge.caller)
717 && let Some(callee) = edge
718 .callee
719 .filter(|callee| address_to_node.contains_key(callee))
720 {
721 let neighbors = adjacency.entry(edge.caller).or_default();
722 neighbors
723 .try_reserve(1)
724 .map_err(|error| Self::allocation_error("call-graph neighbor", error))?;
725 neighbors.push(callee);
726 }
727 }
728 for neighbors in adjacency.values_mut() {
729 neighbors.sort_unstable();
730 neighbors.dedup();
731 }
732
733 Ok((address_to_node, adjacency))
734 }
735
736 fn compute_statistics(&self, call_graph: &CallGraph) -> CallGraphStatistics {
738 let total_functions = call_graph.nodes.len();
739 let total_calls = call_graph.edges.iter().fold(0_usize, |count, edge| {
740 count.saturating_add(edge.call_sites.len())
741 });
742
743 let direct_calls = call_graph
744 .edges
745 .iter()
746 .filter(|edge| matches!(edge.call_type, CallType::Direct | CallType::Recursive))
747 .fold(0_usize, |count, edge| {
748 count.saturating_add(edge.call_sites.len())
749 });
750
751 let indirect_calls = call_graph
752 .edges
753 .iter()
754 .filter(|edge| matches!(edge.call_type, CallType::Indirect))
755 .fold(0_usize, |count, edge| {
756 count.saturating_add(edge.call_sites.len())
757 });
758
759 let recursive_functions = call_graph
760 .nodes
761 .iter()
762 .filter(|node| node.is_recursive)
763 .count();
764
765 let leaf_functions = call_graph
766 .nodes
767 .iter()
768 .filter(|node| node.out_degree == 0)
769 .count();
770
771 let entry_points = call_graph.entry_points.len();
772 let unreachable_functions = call_graph.unreachable_functions.len();
773
774 let max_call_depth = call_graph
775 .nodes
776 .iter()
777 .filter_map(|node| node.call_depth)
778 .max()
779 .unwrap_or(0);
780
781 let depths: Vec<u32> = call_graph
782 .nodes
783 .iter()
784 .filter_map(|node| node.call_depth)
785 .collect();
786
787 let average_call_depth = if !depths.is_empty() {
788 depths.iter().map(|&depth| f64::from(depth)).sum::<f64>() / depths.len() as f64
789 } else {
790 0.0
791 };
792
793 let cyclic_dependencies = self.count_cyclic_dependencies(call_graph);
794
795 CallGraphStatistics {
796 total_functions,
797 total_calls,
798 direct_calls,
799 indirect_calls,
800 recursive_functions,
801 leaf_functions,
802 entry_points,
803 unreachable_functions,
804 max_call_depth,
805 average_call_depth,
806 cyclic_dependencies,
807 }
808 }
809
810 fn count_cyclic_dependencies(&self, call_graph: &CallGraph) -> usize {
812 cyclic_components(&call_graph.nodes, &call_graph.edges).len()
813 }
814}
815
816fn cyclic_components(nodes: &[CallGraphNode], edges: &[CallGraphEdge]) -> Vec<Vec<u64>> {
817 let mut graph = DiGraphMap::<u64, ()>::new();
818 for node in nodes {
819 graph.add_node(node.function_address);
820 }
821 for edge in edges {
822 if !matches!(edge.call_type, CallType::Indirect)
823 && graph.contains_node(edge.caller)
824 && let Some(callee) = edge.callee.filter(|callee| graph.contains_node(*callee))
825 {
826 graph.add_edge(edge.caller, callee, ());
827 }
828 }
829
830 let mut components: Vec<Vec<u64>> = kosaraju_scc(&graph)
831 .into_iter()
832 .filter(|component| {
833 component.len() > 1
834 || component
835 .first()
836 .is_some_and(|&node| graph.contains_edge(node, node))
837 })
838 .collect();
839 for component in &mut components {
840 component.sort_unstable();
841 }
842 components.sort();
843 components
844}
845
846#[derive(Debug, Clone)]
848pub struct DotConfig {
849 pub include_addresses: bool,
851 pub color_by_type: bool,
853 pub show_call_counts: bool,
855 pub cluster_by_module: bool,
857 pub max_nodes: Option<usize>,
859}
860
861impl Default for DotConfig {
862 fn default() -> Self {
863 Self {
864 include_addresses: true,
865 color_by_type: true,
866 show_call_counts: false,
867 cluster_by_module: false,
868 max_nodes: Some(1000),
869 }
870 }
871}
872
873pub trait CallGraphExporter {
875 fn export(&self, graph: &CallGraph) -> Result<String>;
877}
878
879pub struct DotExporter {
881 config: DotConfig,
882}
883
884impl DotExporter {
885 pub fn new(config: DotConfig) -> Self {
887 Self { config }
888 }
889
890 pub fn new_default() -> Self {
892 Self {
893 config: DotConfig::default(),
894 }
895 }
896}
897
898impl CallGraphExporter for DotExporter {
899 fn export(&self, graph: &CallGraph) -> Result<String> {
900 if self.config.cluster_by_module {
901 return Err(BinaryError::feature_not_available(
902 "DOT module clustering is not implemented",
903 ));
904 }
905 let mut dot = String::new();
906
907 dot.push_str("digraph CallGraph {\n");
909 dot.push_str(" rankdir=TB;\n");
910 dot.push_str(" node [shape=box, style=filled];\n");
911 dot.push_str(" edge [arrowhead=normal];\n\n");
912
913 let nodes_to_include = if let Some(max_nodes) = self.config.max_nodes {
915 &graph.nodes[..std::cmp::min(max_nodes, graph.nodes.len())]
916 } else {
917 &graph.nodes
918 };
919
920 for node in nodes_to_include {
922 let escaped_name = escape_dot_label(&node.function_name);
923 let label = if self.config.include_addresses {
924 format!("{}\\n0x{:x}", escaped_name, node.function_address)
925 } else {
926 escaped_name
927 };
928
929 let color = if self.config.color_by_type {
930 match node.node_type {
931 NodeType::EntryPoint => "lightgreen",
932 NodeType::Library => "lightblue",
933 NodeType::External => "lightyellow",
934 NodeType::Internal => "lightgray",
935 NodeType::Indirect => "orange",
936 NodeType::Virtual => "purple",
937 NodeType::Unknown => "pink",
938 }
939 } else {
940 "lightgray"
941 };
942
943 dot.push_str(&format!(
944 " \"0x{:x}\" [label=\"{}\", fillcolor=\"{}\"];\n",
945 node.function_address, label, color
946 ));
947 }
948
949 dot.push('\n');
950
951 let node_addresses: HashSet<u64> = nodes_to_include
953 .iter()
954 .map(|n| n.function_address)
955 .collect();
956 if graph
957 .edges
958 .iter()
959 .any(|edge| edge.callee.is_none() && node_addresses.contains(&edge.caller))
960 {
961 dot.push_str(
962 " \"__unknown_target\" [label=\"unknown target\", shape=ellipse, fillcolor=orange];\n",
963 );
964 }
965
966 for edge in &graph.edges {
967 if !node_addresses.contains(&edge.caller) {
968 continue;
969 }
970
971 let target = match edge.callee {
972 Some(callee) if node_addresses.contains(&callee) => format!("\"0x{callee:x}\""),
973 None => "\"__unknown_target\"".to_string(),
974 Some(_) => continue,
975 };
976 let mut attributes = Vec::new();
977 if self.config.show_call_counts {
978 attributes.push(format!("label=\"{}\"", edge.call_sites.len()));
979 }
980 match edge.call_type {
981 CallType::Direct => {}
982 CallType::Indirect => attributes.push("style=dashed".to_string()),
983 CallType::TailCall => attributes.push("color=red".to_string()),
984 CallType::Virtual => attributes.push("color=purple".to_string()),
985 CallType::Recursive => {
986 attributes.push("color=green".to_string());
987 attributes.push("style=bold".to_string());
988 }
989 CallType::Conditional => attributes.push("color=orange".to_string()),
990 }
991 let attributes = if attributes.is_empty() {
992 String::new()
993 } else {
994 format!(" [{}]", attributes.join(", "))
995 };
996
997 dot.push_str(&format!(
998 " \"0x{:x}\" -> {target}{attributes};\n",
999 edge.caller
1000 ));
1001 }
1002
1003 dot.push_str("}\n");
1004 Ok(dot)
1005 }
1006}
1007
1008fn escape_dot_label(value: &str) -> String {
1009 let mut escaped = String::with_capacity(value.len());
1010 for character in value.chars() {
1011 match character {
1012 '\\' => escaped.push_str("\\\\"),
1013 '"' => escaped.push_str("\\\""),
1014 '\n' => escaped.push_str("\\n"),
1015 '\r' => escaped.push_str("\\r"),
1016 '\t' => escaped.push_str("\\t"),
1017 character if character.is_control() => {
1018 use std::fmt::Write as _;
1019 let _ = write!(escaped, "\\u{:04x}", character as u32);
1020 }
1021 character => escaped.push(character),
1022 }
1023 }
1024 escaped
1025}
1026
1027pub struct JsonExporter;
1029
1030impl CallGraphExporter for JsonExporter {
1031 fn export(&self, graph: &CallGraph) -> Result<String> {
1032 #[cfg(feature = "serde-support")]
1033 {
1034 serde_json::to_string_pretty(graph)
1035 .map_err(|e| BinaryError::invalid_data(format!("JSON serialization failed: {}", e)))
1036 }
1037 #[cfg(not(feature = "serde-support"))]
1038 {
1039 let _ = graph; Err(BinaryError::feature_not_available("serde-support"))
1041 }
1042 }
1043}
1044
1045impl CallGraph {
1046 pub fn to_dot(&self) -> Result<String> {
1048 let exporter = DotExporter::new_default();
1049 exporter.export(self)
1050 }
1051
1052 pub fn to_dot_with_config(&self, config: DotConfig) -> Result<String> {
1054 let exporter = DotExporter::new(config);
1055 exporter.export(self)
1056 }
1057
1058 pub fn to_json(&self) -> Result<String> {
1060 let exporter = JsonExporter;
1061 exporter.export(self)
1062 }
1063
1064 pub fn detect_cycles(&self) -> Vec<Vec<u64>> {
1066 cyclic_components(&self.nodes, &self.edges)
1067 }
1068}
1069
1070pub fn analyze_binary(binary: &BinaryFile) -> Result<CallGraph> {
1072 let analyzer = CallGraphAnalyzer::new_default();
1073 analyzer.analyze_binary(binary)
1074}
1075
1076pub fn analyze_binary_with_config(
1078 binary: &BinaryFile,
1079 config: CallGraphConfig,
1080) -> Result<CallGraph> {
1081 let analyzer = CallGraphAnalyzer::new(config);
1082 analyzer.analyze_binary(binary)
1083}
1084
1085#[cfg(test)]
1086mod tests {
1087 use super::*;
1088 use crate::types::*;
1089
1090 struct TestFormat {
1091 sections: Vec<Section>,
1092 symbols: Vec<Symbol>,
1093 metadata: BinaryMetadata,
1094 }
1095
1096 impl BinaryFormatTrait for TestFormat {
1097 fn format_type(&self) -> BinaryFormat {
1098 BinaryFormat::Raw
1099 }
1100
1101 fn architecture(&self) -> Architecture {
1102 Architecture::X86_64
1103 }
1104
1105 fn entry_point(&self) -> Option<u64> {
1106 None
1107 }
1108
1109 fn sections(&self) -> &[Section] {
1110 &self.sections
1111 }
1112
1113 fn symbols(&self) -> &[Symbol] {
1114 &self.symbols
1115 }
1116
1117 fn imports(&self) -> &[Import] {
1118 &[]
1119 }
1120
1121 fn exports(&self) -> &[Export] {
1122 &[]
1123 }
1124
1125 fn metadata(&self) -> &BinaryMetadata {
1126 &self.metadata
1127 }
1128 }
1129
1130 fn function_symbol(name: &str, address: u64) -> Symbol {
1131 Symbol {
1132 name: name.to_string(),
1133 demangled_name: None,
1134 address,
1135 size: 1,
1136 symbol_type: SymbolType::Function,
1137 binding: SymbolBinding::Global,
1138 visibility: SymbolVisibility::Default,
1139 section_index: None,
1140 }
1141 }
1142
1143 fn test_binary(data: Vec<u8>, sections: Vec<Section>, symbols: Vec<Symbol>) -> BinaryFile {
1144 let metadata = BinaryMetadata {
1145 size: data.len(),
1146 format: BinaryFormat::Raw,
1147 architecture: Architecture::X86_64,
1148 entry_point: None,
1149 base_address: None,
1150 timestamp: None,
1151 compiler_info: None,
1152 endian: Endianness::Little,
1153 security_features: SecurityFeatures::default(),
1154 };
1155
1156 BinaryFile {
1157 data,
1158 parsed: Box::new(TestFormat {
1159 sections,
1160 symbols,
1161 metadata,
1162 }),
1163 }
1164 }
1165
1166 fn function_analysis_binary(include_valid_function: bool) -> BinaryFile {
1167 let data = vec![0xc3];
1168 let mut sections = vec![Section {
1169 name: ".invalid".to_string(),
1170 address: 0x1000,
1171 size: 1,
1172 offset: 2,
1173 file_size: 1,
1174 permissions: SectionPermissions {
1175 read: true,
1176 write: false,
1177 execute: true,
1178 },
1179 section_type: SectionType::Code,
1180 data: None,
1181 }];
1182 let mut symbols = vec![function_symbol("invalid", 0x1000)];
1183
1184 if include_valid_function {
1185 sections.push(Section {
1186 name: ".valid".to_string(),
1187 address: 0x2000,
1188 size: 1,
1189 offset: 0,
1190 file_size: 1,
1191 permissions: SectionPermissions {
1192 read: true,
1193 write: false,
1194 execute: true,
1195 },
1196 section_type: SectionType::Code,
1197 data: Some(data.clone()),
1198 });
1199 symbols.push(function_symbol("valid", 0x2000));
1200 }
1201
1202 test_binary(data, sections, symbols)
1203 }
1204
1205 #[test]
1206 fn test_analyzer_creation() {
1207 let analyzer = CallGraphAnalyzer::new_default();
1208 assert!(analyzer.config.analyze_indirect_calls);
1209 assert!(analyzer.config.detect_tail_calls);
1210 assert_eq!(analyzer.config.max_functions, 10_000);
1211 assert_eq!(analyzer.config.max_total_instructions, 1_000_000);
1212 }
1213
1214 #[test]
1215 fn analyze_binary_returns_error_when_every_function_fails() {
1216 let binary = function_analysis_binary(false);
1217 let analyzer = CallGraphAnalyzer::new_default();
1218
1219 let error = analyzer.analyze_binary(&binary).unwrap_err();
1220
1221 assert!(
1222 matches!(error, BinaryError::InvalidData(message) if message.contains("beyond the end of the file"))
1223 );
1224 }
1225
1226 #[test]
1227 fn analyze_binary_preserves_partial_function_success() {
1228 let binary = function_analysis_binary(true);
1229 let analyzer = CallGraphAnalyzer::new(CallGraphConfig {
1230 max_functions: 2,
1231 max_total_instructions: 1,
1232 ..CallGraphConfig::default()
1233 });
1234
1235 let graph = analyzer.analyze_binary(&binary).unwrap();
1236
1237 assert_eq!(graph.nodes.len(), 2);
1238 assert!(graph.nodes.iter().any(|node| node.function_name == "valid"));
1239 }
1240
1241 #[test]
1242 fn analyze_binary_returns_error_when_function_has_no_executable_section() {
1243 let binary = test_binary(
1244 vec![0xc3],
1245 Vec::new(),
1246 vec![function_symbol("missing", 0x3000)],
1247 );
1248 let analyzer = CallGraphAnalyzer::new_default();
1249
1250 let error = analyzer.analyze_binary(&binary).unwrap_err();
1251
1252 assert!(
1253 matches!(error, BinaryError::InvalidData(message) if message == "Function bytes not found in any executable section")
1254 );
1255 }
1256
1257 #[test]
1258 fn containing_zero_length_file_range_counts_as_success() {
1259 let binary = test_binary(
1260 vec![0xc3],
1261 vec![Section {
1262 name: ".empty".to_string(),
1263 address: 0x3000,
1264 size: 1,
1265 offset: 1,
1266 file_size: 0,
1267 permissions: SectionPermissions {
1268 read: true,
1269 write: false,
1270 execute: true,
1271 },
1272 section_type: SectionType::Code,
1273 data: None,
1274 }],
1275 vec![function_symbol("empty", 0x3000)],
1276 );
1277 let analyzer = CallGraphAnalyzer::new_default();
1278
1279 let graph = analyzer.analyze_binary(&binary).unwrap();
1280
1281 assert_eq!(graph.nodes.len(), 1);
1282 assert!(graph.edges.is_empty());
1283 }
1284
1285 #[test]
1286 fn analyze_binary_rejects_function_count_above_configured_cap() {
1287 let binary = function_analysis_binary(true);
1288 let analyzer = CallGraphAnalyzer::new(CallGraphConfig {
1289 max_functions: 1,
1290 ..CallGraphConfig::default()
1291 });
1292
1293 let error = analyzer.analyze_binary(&binary).unwrap_err();
1294
1295 assert!(
1296 matches!(error, BinaryError::ControlFlowError(message) if message.contains("max_functions 1"))
1297 );
1298 }
1299
1300 #[test]
1301 fn analyze_binary_rejects_total_instructions_above_configured_cap() {
1302 let binary = function_analysis_binary(true);
1303 let analyzer = CallGraphAnalyzer::new(CallGraphConfig {
1304 max_total_instructions: 0,
1305 ..CallGraphConfig::default()
1306 });
1307
1308 let error = analyzer.analyze_binary(&binary).unwrap_err();
1309
1310 assert!(
1311 matches!(error, BinaryError::ControlFlowError(message) if message.contains("max_total_instructions 0"))
1312 );
1313 }
1314
1315 #[test]
1316 fn test_library_function_detection() {
1317 let analyzer = CallGraphAnalyzer::new_default();
1318 assert!(analyzer.is_library_function("printf"));
1319 assert!(analyzer.is_library_function("libc_start_main"));
1320 assert!(analyzer.is_library_function("__stack_chk_fail"));
1321 assert!(!analyzer.is_library_function("user_function"));
1322 assert!(!analyzer.is_library_function("carefree_user"));
1323 assert!(!analyzer.is_library_function("main"));
1324 }
1325
1326 #[test]
1327 fn indirect_call_setting_gates_unknown_target_edges() {
1328 let instruction = Instruction {
1329 address: 0x1000,
1330 bytes: vec![0xff, 0xd0],
1331 mnemonic: "call".to_string(),
1332 operands: "rax".to_string(),
1333 category: InstructionCategory::Control,
1334 flow: crate::types::ControlFlow::Sequential,
1335 size: 2,
1336 };
1337 let enabled = CallGraphAnalyzer::new_default();
1338 let disabled = CallGraphAnalyzer::new(CallGraphConfig {
1339 analyze_indirect_calls: false,
1340 ..CallGraphConfig::default()
1341 });
1342
1343 let detected = enabled
1344 .analyze_call_instruction(&instruction, 0x1000, &HashMap::new())
1345 .unwrap();
1346 assert_eq!(detected.callee, None);
1347 assert!(
1348 disabled
1349 .analyze_call_instruction(&instruction, 0x1000, &HashMap::new())
1350 .is_none()
1351 );
1352 }
1353
1354 #[test]
1355 fn test_node_type_classification() {
1356 let analyzer = CallGraphAnalyzer::new_default();
1357
1358 let entry_function = Function {
1359 name: "_start".to_string(),
1360 start_address: 0x1000,
1361 end_address: 0x1100,
1362 size: 256,
1363 function_type: FunctionType::Entrypoint,
1364 calling_convention: None,
1365 parameters: Vec::new(),
1366 return_type: None,
1367 };
1368
1369 let elf_data = vec![
1371 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00, ];
1380
1381 match crate::BinaryFile::parse(&elf_data) {
1384 Ok(binary) => {
1385 let node_type = analyzer.classify_node_type(&entry_function, &binary);
1386 assert_eq!(node_type, NodeType::EntryPoint);
1387 }
1388 Err(_) => {
1389 assert_eq!(entry_function.function_type, FunctionType::Entrypoint);
1392 }
1393 }
1394 }
1395
1396 fn node(address: u64, name: &str) -> CallGraphNode {
1397 CallGraphNode {
1398 function_address: address,
1399 function_name: name.to_string(),
1400 node_type: NodeType::Internal,
1401 complexity: 0,
1402 in_degree: 0,
1403 out_degree: 0,
1404 is_recursive: false,
1405 call_depth: None,
1406 }
1407 }
1408
1409 fn edge(caller: u64, callee: u64, call_type: CallType, site: u64) -> CallGraphEdge {
1410 CallGraphEdge {
1411 caller,
1412 callee: Some(callee),
1413 call_type,
1414 call_sites: vec![CallSite {
1415 address: site,
1416 instruction_bytes: vec![0xe8],
1417 context: CallContext::Normal,
1418 }],
1419 }
1420 }
1421
1422 fn unknown_edge(caller: u64, site: u64) -> CallGraphEdge {
1423 CallGraphEdge {
1424 caller,
1425 callee: None,
1426 call_type: CallType::Indirect,
1427 call_sites: vec![CallSite {
1428 address: site,
1429 instruction_bytes: vec![0xff, 0xd0],
1430 context: CallContext::Normal,
1431 }],
1432 }
1433 }
1434
1435 fn graph(nodes: Vec<CallGraphNode>, edges: Vec<CallGraphEdge>) -> CallGraph {
1436 CallGraph {
1437 nodes,
1438 edges,
1439 entry_points: vec![1],
1440 unreachable_functions: Vec::new(),
1441 statistics: CallGraphStatistics::default(),
1442 }
1443 }
1444
1445 #[test]
1446 fn dot_export_handles_plain_direct_edges_and_escapes_labels() {
1447 let graph = graph(
1448 vec![node(1, "quoted\"name\nline"), node(2, "callee")],
1449 vec![edge(1, 2, CallType::Direct, 10)],
1450 );
1451
1452 let dot = DotExporter::new_default().export(&graph).unwrap();
1453
1454 assert!(dot.contains("quoted\\\"name\\nline"));
1455 assert!(dot.contains("\"0x1\" -> \"0x2\";"));
1456 }
1457
1458 #[test]
1459 fn repeated_calls_are_merged_with_sorted_call_sites() {
1460 let edges = vec![
1461 edge(1, 2, CallType::Direct, 20),
1462 edge(1, 2, CallType::Direct, 10),
1463 edge(1, 2, CallType::Direct, 10),
1464 ];
1465
1466 let merged = CallGraphAnalyzer::merge_edges(edges);
1467
1468 assert_eq!(merged.len(), 1);
1469 assert_eq!(merged[0].call_sites.len(), 2);
1470 assert_eq!(merged[0].call_sites[0].address, 10);
1471 assert_eq!(merged[0].call_sites[1].address, 20);
1472 }
1473
1474 #[test]
1475 fn unknown_target_does_not_conflate_with_function_at_zero() {
1476 let analyzer = CallGraphAnalyzer::new_default();
1477 let mut nodes = vec![node(0, "zero"), node(1, "caller")];
1478 let edges = vec![unknown_edge(1, 10)];
1479
1480 analyzer.update_node_degrees(&mut nodes, &edges);
1481 let dot = DotExporter::new_default()
1482 .export(&graph(nodes.clone(), edges))
1483 .unwrap();
1484
1485 assert_eq!(nodes[0].in_degree, 0);
1486 assert_eq!(nodes[1].out_degree, 1);
1487 assert!(dot.contains("\"__unknown_target\""));
1488 assert!(dot.contains("\"0x1\" -> \"__unknown_target\""));
1489 assert!(!dot.contains("\"0x1\" -> \"0x0\""));
1490 }
1491
1492 #[test]
1493 fn cycle_detection_returns_deterministic_components() {
1494 let graph = graph(
1495 vec![node(1, "one"), node(2, "two"), node(3, "three")],
1496 vec![
1497 edge(2, 1, CallType::Direct, 20),
1498 edge(1, 2, CallType::Direct, 10),
1499 unknown_edge(3, 30),
1500 ],
1501 );
1502
1503 assert_eq!(graph.detect_cycles(), vec![vec![1, 2]]);
1504 }
1505
1506 #[cfg(not(feature = "serde-support"))]
1507 #[test]
1508 fn json_export_reports_missing_serde_feature() {
1509 let graph = graph(Vec::new(), Vec::new());
1510
1511 assert!(matches!(
1512 graph.to_json(),
1513 Err(BinaryError::FeatureNotAvailable(feature)) if feature == "serde-support"
1514 ));
1515 }
1516
1517 #[cfg(feature = "serde-support")]
1518 #[test]
1519 fn json_export_uses_null_for_unknown_call_target() {
1520 let graph = graph(vec![node(1, "caller")], vec![unknown_edge(1, 10)]);
1521
1522 let json = graph.to_json().unwrap();
1523
1524 assert!(json.contains("\"callee\": null"));
1525 }
1526
1527 #[test]
1528 fn maximum_labeled_call_depth_is_enforced() {
1529 let config = CallGraphConfig {
1530 max_labeled_call_depth: Some(1),
1531 ..CallGraphConfig::default()
1532 };
1533 let analyzer = CallGraphAnalyzer::new(config);
1534 let mut graph = graph(
1535 vec![node(1, "one"), node(2, "two"), node(3, "three")],
1536 vec![
1537 edge(1, 2, CallType::Direct, 10),
1538 edge(2, 3, CallType::Direct, 20),
1539 ],
1540 );
1541
1542 analyzer.compute_call_depths(&mut graph).unwrap();
1543
1544 assert_eq!(graph.nodes[0].call_depth, Some(0));
1545 assert_eq!(graph.nodes[1].call_depth, Some(1));
1546 assert_eq!(graph.nodes[2].call_depth, None);
1547 assert!(
1548 analyzer
1549 .find_unreachable_functions(&graph)
1550 .unwrap()
1551 .is_empty()
1552 );
1553 }
1554}