1use crate::{
7 disasm::Disassembler,
8 types::{
9 CallContext, CallGraph, CallGraphConfig, CallGraphEdge, CallGraphNode, CallGraphStatistics,
10 CallSite, CallType, Function, Instruction, NodeType,
11 },
12 BinaryError, BinaryFile, Result,
13};
14use std::collections::{HashMap, HashSet, VecDeque};
15
16#[derive(Clone)]
18pub struct CallGraphAnalyzer {
19 config: CallGraphConfig,
21}
22
23impl CallGraphAnalyzer {
24 pub fn new(config: CallGraphConfig) -> Self {
26 Self { config }
27 }
28
29 pub fn new_default() -> Self {
31 Self {
32 config: CallGraphConfig::default(),
33 }
34 }
35
36 pub fn analyze_binary(&self, binary: &BinaryFile) -> Result<CallGraph> {
38 let functions = self.extract_functions(binary)?;
40
41 let mut nodes = Vec::new();
43 let mut address_to_node: HashMap<u64, usize> = HashMap::new();
44
45 for (i, function) in functions.iter().enumerate() {
46 let node = CallGraphNode {
47 function_address: function.start_address,
48 function_name: function.name.clone(),
49 node_type: self.classify_node_type(function, binary),
50 complexity: 0, in_degree: 0, out_degree: 0, is_recursive: false, call_depth: None, };
56 nodes.push(node);
57 address_to_node.insert(function.start_address, i);
58 }
59
60 let edges = self.extract_function_calls(binary, &functions, &address_to_node)?;
62
63 self.update_node_degrees(&mut nodes, &edges);
65
66 self.detect_recursion(&mut nodes, &edges);
68
69 let entry_points = self.find_entry_points(binary, &functions);
71
72 let mut call_graph = CallGraph {
74 nodes,
75 edges,
76 entry_points: entry_points.clone(),
77 unreachable_functions: Vec::new(),
78 statistics: CallGraphStatistics::default(),
79 };
80
81 self.compute_call_depths(&mut call_graph)?;
82
83 call_graph.unreachable_functions = self.find_unreachable_functions(&call_graph);
85
86 call_graph.statistics = self.compute_statistics(&call_graph);
88
89 Ok(call_graph)
90 }
91
92 fn extract_functions(&self, binary: &BinaryFile) -> Result<Vec<Function>> {
94 let mut functions = Vec::new();
95 let mut seen_addresses = HashSet::new();
96
97 for symbol in binary.symbols() {
99 if matches!(symbol.symbol_type, crate::types::SymbolType::Function)
100 && symbol.size > 0
101 && !seen_addresses.contains(&symbol.address)
102 {
103 let function = Function {
104 name: symbol.name.clone(),
105 start_address: symbol.address,
106 end_address: symbol.address + symbol.size,
107 size: symbol.size,
108 function_type: crate::types::FunctionType::Normal,
109 calling_convention: None,
110 parameters: Vec::new(),
111 return_type: None,
112 };
113 functions.push(function);
114 seen_addresses.insert(symbol.address);
115 }
116 }
117
118 if let Some(entry_point) = binary.entry_point() {
120 if !seen_addresses.contains(&entry_point) {
121 let function = Function {
122 name: "_start".to_string(),
123 start_address: entry_point,
124 end_address: entry_point + 1000, size: 1000,
126 function_type: crate::types::FunctionType::Entrypoint,
127 calling_convention: None,
128 parameters: Vec::new(),
129 return_type: None,
130 };
131 functions.push(function);
132 }
133 }
134
135 Ok(functions)
136 }
137
138 fn classify_node_type(&self, function: &Function, binary: &BinaryFile) -> NodeType {
140 if matches!(
142 function.function_type,
143 crate::types::FunctionType::Entrypoint
144 ) || matches!(function.function_type, crate::types::FunctionType::Main)
145 || function.name == "_start"
146 || function.name == "main"
147 || function.name == "DllMain"
148 {
149 return NodeType::EntryPoint;
150 }
151
152 for import in binary.imports() {
154 if let Some(addr) = import.address {
155 if addr == function.start_address {
156 return NodeType::External;
157 }
158 }
159 }
160
161 if self.is_library_function(&function.name) {
163 return NodeType::Library;
164 }
165
166 NodeType::Internal
168 }
169
170 fn is_library_function(&self, name: &str) -> bool {
172 const LIBRARY_PREFIXES: &[&str] =
174 &["libc_", "libm_", "__", "_GLOBAL_", "std::", "_ZN", "_Z"];
175
176 const LIBRARY_NAMES: &[&str] = &[
178 "printf", "scanf", "malloc", "free", "strlen", "strcpy", "strcat", "memcpy", "memset",
179 "fopen", "fclose", "fread", "fwrite", "msvcrt", "kernel32", "ntdll",
180 ];
181
182 for prefix in LIBRARY_PREFIXES {
184 if name.starts_with(prefix) {
185 return true;
186 }
187 }
188
189 for lib_name in LIBRARY_NAMES {
191 if name == *lib_name || name.contains(lib_name) {
192 return true;
193 }
194 }
195
196 false
197 }
198
199 fn extract_function_calls(
201 &self,
202 binary: &BinaryFile,
203 functions: &[Function],
204 address_to_node: &HashMap<u64, usize>,
205 ) -> Result<Vec<CallGraphEdge>> {
206 let mut edges = Vec::new();
207 let disassembler = Disassembler::new(binary.architecture())?;
208
209 for function in functions {
210 if let Ok(instructions) =
212 self.get_function_instructions(binary, function, &disassembler)
213 {
214 for instruction in &instructions {
216 if let Some(edge) = self.analyze_call_instruction(
217 instruction,
218 function.start_address,
219 address_to_node,
220 ) {
221 edges.push(edge);
222 }
223 }
224 }
225 }
226
227 if self.config.detect_tail_calls {
229 let tail_call_edges = self.detect_tail_calls(binary, functions, address_to_node)?;
230 edges.extend(tail_call_edges);
231 }
232
233 Ok(edges)
234 }
235
236 fn get_function_instructions(
238 &self,
239 binary: &BinaryFile,
240 function: &Function,
241 disassembler: &Disassembler,
242 ) -> Result<Vec<Instruction>> {
243 for section in binary.sections() {
245 let start = section.address;
246 let end = start + section.size;
247
248 if function.start_address >= start && function.start_address < end {
249 let data = section.data.as_ref().ok_or_else(|| {
250 BinaryError::invalid_data("Section data not available for disassembly")
251 })?;
252
253 let offset = (function.start_address - start) as usize;
254 if offset >= data.len() {
255 return Ok(Vec::new());
256 }
257
258 let available = data.len() - offset;
259 let length = std::cmp::min(function.size as usize, available);
260 if length == 0 {
261 return Ok(Vec::new());
262 }
263
264 let slice = &data[offset..offset + length];
265 return disassembler.disassemble_at(slice, function.start_address, length);
266 }
267 }
268
269 Ok(Vec::new())
270 }
271
272 fn analyze_call_instruction(
274 &self,
275 instruction: &Instruction,
276 caller_address: u64,
277 address_to_node: &HashMap<u64, usize>,
278 ) -> Option<CallGraphEdge> {
279 match &instruction.flow {
280 crate::types::ControlFlow::Call(target_address) => {
281 if address_to_node.contains_key(target_address) {
283 let call_site = CallSite {
284 address: instruction.address,
285 instruction_bytes: instruction.bytes.clone(),
286 context: CallContext::Normal,
287 };
288
289 let call_type = if *target_address == caller_address {
290 CallType::Recursive
291 } else {
292 CallType::Direct
293 };
294
295 return Some(CallGraphEdge {
296 caller: caller_address,
297 callee: *target_address,
298 call_type,
299 call_sites: vec![call_site],
300 });
301 }
302 }
303 _ => {
304 if self.config.analyze_indirect_calls {
306 if let Some(edge) = self.analyze_indirect_call(instruction, caller_address) {
307 return Some(edge);
308 }
309 }
310 }
311 }
312
313 None
314 }
315
316 fn analyze_indirect_call(
318 &self,
319 instruction: &Instruction,
320 caller_address: u64,
321 ) -> Option<CallGraphEdge> {
322 if instruction.mnemonic.starts_with("call") && instruction.operands.contains('[') {
324 let call_site = CallSite {
326 address: instruction.address,
327 instruction_bytes: instruction.bytes.clone(),
328 context: CallContext::Normal,
329 };
330
331 return Some(CallGraphEdge {
334 caller: caller_address,
335 callee: 0, call_type: CallType::Indirect,
337 call_sites: vec![call_site],
338 });
339 }
340
341 None
342 }
343
344 fn detect_tail_calls(
346 &self,
347 binary: &BinaryFile,
348 functions: &[Function],
349 address_to_node: &HashMap<u64, usize>,
350 ) -> Result<Vec<CallGraphEdge>> {
351 let mut tail_call_edges = Vec::new();
352 let disassembler = Disassembler::new(binary.architecture())?;
353
354 for function in functions {
355 if let Ok(instructions) =
356 self.get_function_instructions(binary, function, &disassembler)
357 {
358 if let Some(last_instruction) = instructions.last() {
360 if let crate::types::ControlFlow::Jump(target) = &last_instruction.flow {
361 if address_to_node.contains_key(target) && *target != function.start_address
362 {
363 let call_site = CallSite {
364 address: last_instruction.address,
365 instruction_bytes: last_instruction.bytes.clone(),
366 context: CallContext::Normal,
367 };
368
369 tail_call_edges.push(CallGraphEdge {
370 caller: function.start_address,
371 callee: *target,
372 call_type: CallType::TailCall,
373 call_sites: vec![call_site],
374 });
375 }
376 }
377 }
378 }
379 }
380
381 Ok(tail_call_edges)
382 }
383
384 fn update_node_degrees(&self, nodes: &mut [CallGraphNode], edges: &[CallGraphEdge]) {
386 let mut in_degrees: HashMap<u64, u32> = HashMap::new();
388 let mut out_degrees: HashMap<u64, u32> = HashMap::new();
389
390 for edge in edges {
391 *out_degrees.entry(edge.caller).or_insert(0) += 1;
392 *in_degrees.entry(edge.callee).or_insert(0) += 1;
393 }
394
395 for node in nodes {
397 node.in_degree = in_degrees.get(&node.function_address).copied().unwrap_or(0);
398 node.out_degree = out_degrees
399 .get(&node.function_address)
400 .copied()
401 .unwrap_or(0);
402 }
403 }
404
405 fn detect_recursion(&self, nodes: &mut [CallGraphNode], edges: &[CallGraphEdge]) {
407 let mut recursive_functions = HashSet::new();
408
409 for edge in edges {
411 if edge.caller == edge.callee {
412 recursive_functions.insert(edge.caller);
413 }
414 }
415
416 let mut adjacency: HashMap<u64, Vec<u64>> = HashMap::new();
418 for edge in edges {
419 adjacency.entry(edge.caller).or_default().push(edge.callee);
420 }
421
422 for node in nodes.iter() {
423 if self.has_cycle_from_node(node.function_address, &adjacency) {
424 recursive_functions.insert(node.function_address);
425 }
426 }
427
428 for node in nodes {
430 node.is_recursive = recursive_functions.contains(&node.function_address);
431 }
432 }
433
434 fn has_cycle_from_node(&self, start: u64, adjacency: &HashMap<u64, Vec<u64>>) -> bool {
436 let mut visited = HashSet::new();
437 let mut rec_stack = HashSet::new();
438 self.dfs_has_cycle(start, &mut visited, &mut rec_stack, adjacency)
439 }
440
441 #[allow(clippy::only_used_in_recursion)]
443 fn dfs_has_cycle(
444 &self,
445 node: u64,
446 visited: &mut HashSet<u64>,
447 rec_stack: &mut HashSet<u64>,
448 adjacency: &HashMap<u64, Vec<u64>>,
449 ) -> bool {
450 visited.insert(node);
451 rec_stack.insert(node);
452
453 if let Some(neighbors) = adjacency.get(&node) {
454 for &neighbor in neighbors {
455 if !visited.contains(&neighbor) {
456 if self.dfs_has_cycle(neighbor, visited, rec_stack, adjacency) {
457 return true;
458 }
459 } else if rec_stack.contains(&neighbor) {
460 return true;
461 }
462 }
463 }
464
465 rec_stack.remove(&node);
466 false
467 }
468
469 fn find_entry_points(&self, binary: &BinaryFile, functions: &[Function]) -> Vec<u64> {
471 let mut entry_points = Vec::new();
472
473 if let Some(entry) = binary.entry_point() {
475 entry_points.push(entry);
476 }
477
478 for function in functions {
480 match function.function_type {
481 crate::types::FunctionType::Entrypoint | crate::types::FunctionType::Main => {
482 if !entry_points.contains(&function.start_address) {
483 entry_points.push(function.start_address);
484 }
485 }
486 _ => {}
487 }
488 }
489
490 entry_points
491 }
492
493 fn compute_call_depths(&self, call_graph: &mut CallGraph) -> Result<()> {
495 let mut address_to_node: HashMap<u64, usize> = HashMap::new();
496 for (i, node) in call_graph.nodes.iter().enumerate() {
497 address_to_node.insert(node.function_address, i);
498 }
499
500 let mut adjacency: HashMap<u64, Vec<u64>> = HashMap::new();
502 for edge in &call_graph.edges {
503 adjacency.entry(edge.caller).or_default().push(edge.callee);
504 }
505
506 for &entry_point in &call_graph.entry_points {
508 let mut queue = VecDeque::new();
509 let mut visited = HashSet::new();
510
511 queue.push_back((entry_point, 0));
512 visited.insert(entry_point);
513
514 while let Some((current_addr, depth)) = queue.pop_front() {
515 if let Some(&node_index) = address_to_node.get(¤t_addr) {
516 let current_depth = call_graph.nodes[node_index].call_depth;
518 if current_depth.is_none() || current_depth.unwrap() > depth {
519 call_graph.nodes[node_index].call_depth = Some(depth);
520 }
521 }
522
523 if let Some(neighbors) = adjacency.get(¤t_addr) {
525 for &neighbor in neighbors {
526 if !visited.contains(&neighbor) {
527 visited.insert(neighbor);
528 queue.push_back((neighbor, depth + 1));
529 }
530 }
531 }
532 }
533 }
534
535 Ok(())
536 }
537
538 fn find_unreachable_functions(&self, call_graph: &CallGraph) -> Vec<u64> {
540 let reachable: HashSet<u64> = call_graph
541 .nodes
542 .iter()
543 .filter(|node| node.call_depth.is_some())
544 .map(|node| node.function_address)
545 .collect();
546
547 call_graph
548 .nodes
549 .iter()
550 .filter(|node| !reachable.contains(&node.function_address))
551 .map(|node| node.function_address)
552 .collect()
553 }
554
555 fn compute_statistics(&self, call_graph: &CallGraph) -> CallGraphStatistics {
557 let total_functions = call_graph.nodes.len();
558 let total_calls = call_graph.edges.len();
559
560 let direct_calls = call_graph
561 .edges
562 .iter()
563 .filter(|edge| matches!(edge.call_type, CallType::Direct))
564 .count();
565
566 let indirect_calls = call_graph
567 .edges
568 .iter()
569 .filter(|edge| matches!(edge.call_type, CallType::Indirect))
570 .count();
571
572 let recursive_functions = call_graph
573 .nodes
574 .iter()
575 .filter(|node| node.is_recursive)
576 .count();
577
578 let leaf_functions = call_graph
579 .nodes
580 .iter()
581 .filter(|node| node.out_degree == 0)
582 .count();
583
584 let entry_points = call_graph.entry_points.len();
585 let unreachable_functions = call_graph.unreachable_functions.len();
586
587 let max_call_depth = call_graph
588 .nodes
589 .iter()
590 .filter_map(|node| node.call_depth)
591 .max()
592 .unwrap_or(0);
593
594 let depths: Vec<u32> = call_graph
595 .nodes
596 .iter()
597 .filter_map(|node| node.call_depth)
598 .collect();
599
600 let average_call_depth = if !depths.is_empty() {
601 depths.iter().sum::<u32>() as f64 / depths.len() as f64
602 } else {
603 0.0
604 };
605
606 let cyclic_dependencies = self.count_cyclic_dependencies(call_graph);
607
608 CallGraphStatistics {
609 total_functions,
610 total_calls,
611 direct_calls,
612 indirect_calls,
613 recursive_functions,
614 leaf_functions,
615 entry_points,
616 unreachable_functions,
617 max_call_depth,
618 average_call_depth,
619 cyclic_dependencies,
620 }
621 }
622
623 fn count_cyclic_dependencies(&self, call_graph: &CallGraph) -> usize {
625 call_graph
627 .nodes
628 .iter()
629 .filter(|node| node.is_recursive)
630 .count()
631 }
632}
633
634#[derive(Debug, Clone)]
636pub struct DotConfig {
637 pub include_addresses: bool,
639 pub color_by_type: bool,
641 pub show_call_counts: bool,
643 pub cluster_by_module: bool,
645 pub max_nodes: Option<usize>,
647}
648
649impl Default for DotConfig {
650 fn default() -> Self {
651 Self {
652 include_addresses: true,
653 color_by_type: true,
654 show_call_counts: false,
655 cluster_by_module: false,
656 max_nodes: Some(1000),
657 }
658 }
659}
660
661pub trait CallGraphExporter {
663 fn export(&self, graph: &CallGraph) -> Result<String>;
665}
666
667pub struct DotExporter {
669 config: DotConfig,
670}
671
672impl DotExporter {
673 pub fn new(config: DotConfig) -> Self {
675 Self { config }
676 }
677
678 pub fn new_default() -> Self {
680 Self {
681 config: DotConfig::default(),
682 }
683 }
684}
685
686impl CallGraphExporter for DotExporter {
687 fn export(&self, graph: &CallGraph) -> Result<String> {
688 let mut dot = String::new();
689
690 dot.push_str("digraph CallGraph {\n");
692 dot.push_str(" rankdir=TB;\n");
693 dot.push_str(" node [shape=box, style=filled];\n");
694 dot.push_str(" edge [arrowhead=normal];\n\n");
695
696 let nodes_to_include = if let Some(max_nodes) = self.config.max_nodes {
698 &graph.nodes[..std::cmp::min(max_nodes, graph.nodes.len())]
699 } else {
700 &graph.nodes
701 };
702
703 for node in nodes_to_include {
705 let label = if self.config.include_addresses {
706 format!("{}\\n0x{:x}", node.function_name, node.function_address)
707 } else {
708 node.function_name.clone()
709 };
710
711 let color = if self.config.color_by_type {
712 match node.node_type {
713 NodeType::EntryPoint => "lightgreen",
714 NodeType::Library => "lightblue",
715 NodeType::External => "lightyellow",
716 NodeType::Internal => "lightgray",
717 NodeType::Indirect => "orange",
718 NodeType::Virtual => "purple",
719 NodeType::Unknown => "pink",
720 }
721 } else {
722 "lightgray"
723 };
724
725 dot.push_str(&format!(
726 " \"0x{:x}\" [label=\"{}\", fillcolor=\"{}\"];\n",
727 node.function_address, label, color
728 ));
729 }
730
731 dot.push('\n');
732
733 let node_addresses: HashSet<u64> = nodes_to_include
735 .iter()
736 .map(|n| n.function_address)
737 .collect();
738
739 for edge in &graph.edges {
740 if node_addresses.contains(&edge.caller) && node_addresses.contains(&edge.callee) {
742 let style = match edge.call_type {
743 CallType::Direct => "",
744 CallType::Indirect => ", style=dashed",
745 CallType::TailCall => ", color=red",
746 CallType::Virtual => ", color=purple",
747 CallType::Recursive => ", color=green, style=bold",
748 CallType::Conditional => ", color=orange",
749 };
750
751 let label = if self.config.show_call_counts {
752 format!(" [label=\"{}\"{}", edge.call_sites.len(), style)
753 } else {
754 format!(" [{}]", &style[2..]) };
756
757 dot.push_str(&format!(
758 " \"0x{:x}\" -> \"0x{:x}\"{};\n",
759 edge.caller, edge.callee, label
760 ));
761 }
762 }
763
764 dot.push_str("}\n");
765 Ok(dot)
766 }
767}
768
769pub struct JsonExporter;
771
772impl CallGraphExporter for JsonExporter {
773 fn export(&self, graph: &CallGraph) -> Result<String> {
774 #[cfg(feature = "serde-support")]
775 {
776 serde_json::to_string_pretty(graph)
777 .map_err(|e| BinaryError::invalid_data(format!("JSON serialization failed: {}", e)))
778 }
779 #[cfg(not(feature = "serde-support"))]
780 {
781 let _ = graph; Err(BinaryError::invalid_data(
783 "JSON export requires 'serde-support' feature",
784 ))
785 }
786 }
787}
788
789impl CallGraph {
790 pub fn to_dot(&self) -> String {
792 let exporter = DotExporter::new_default();
793 exporter.export(self).unwrap_or_default()
794 }
795
796 pub fn to_dot_with_config(&self, config: DotConfig) -> String {
798 let exporter = DotExporter::new(config);
799 exporter.export(self).unwrap_or_default()
800 }
801
802 pub fn to_json(&self) -> String {
804 let exporter = JsonExporter;
805 exporter.export(self).unwrap_or_default()
806 }
807
808 pub fn detect_cycles(&self) -> Vec<Vec<u64>> {
810 let mut cycles = Vec::new();
811 let mut visited = HashSet::new();
812 let mut rec_stack = HashSet::new();
813 let mut current_path = Vec::new();
814
815 let mut adjacency: HashMap<u64, Vec<u64>> = HashMap::new();
817 for edge in &self.edges {
818 adjacency.entry(edge.caller).or_default().push(edge.callee);
819 }
820
821 for node in &self.nodes {
823 if !visited.contains(&node.function_address) {
824 self.dfs_find_cycles(
825 node.function_address,
826 &mut visited,
827 &mut rec_stack,
828 &mut current_path,
829 &mut cycles,
830 &adjacency,
831 );
832 }
833 }
834
835 cycles
836 }
837
838 #[allow(clippy::only_used_in_recursion)]
840 fn dfs_find_cycles(
841 &self,
842 node: u64,
843 visited: &mut HashSet<u64>,
844 rec_stack: &mut HashSet<u64>,
845 current_path: &mut Vec<u64>,
846 cycles: &mut Vec<Vec<u64>>,
847 adjacency: &HashMap<u64, Vec<u64>>,
848 ) {
849 visited.insert(node);
850 rec_stack.insert(node);
851 current_path.push(node);
852
853 if let Some(neighbors) = adjacency.get(&node) {
854 for &neighbor in neighbors {
855 if !visited.contains(&neighbor) {
856 self.dfs_find_cycles(
857 neighbor,
858 visited,
859 rec_stack,
860 current_path,
861 cycles,
862 adjacency,
863 );
864 } else if rec_stack.contains(&neighbor) {
865 if let Some(cycle_start) = current_path.iter().position(|&x| x == neighbor) {
867 let cycle = current_path[cycle_start..].to_vec();
868 cycles.push(cycle);
869 }
870 }
871 }
872 }
873
874 current_path.pop();
875 rec_stack.remove(&node);
876 }
877}
878
879pub fn analyze_binary(binary: &BinaryFile) -> Result<CallGraph> {
881 let analyzer = CallGraphAnalyzer::new_default();
882 analyzer.analyze_binary(binary)
883}
884
885pub fn analyze_binary_with_config(
887 binary: &BinaryFile,
888 config: CallGraphConfig,
889) -> Result<CallGraph> {
890 let analyzer = CallGraphAnalyzer::new(config);
891 analyzer.analyze_binary(binary)
892}
893
894#[cfg(test)]
895mod tests {
896 use super::*;
897 use crate::types::*;
898
899 #[test]
900 fn test_analyzer_creation() {
901 let analyzer = CallGraphAnalyzer::new_default();
902 assert!(analyzer.config.analyze_indirect_calls);
903 assert!(analyzer.config.detect_tail_calls);
904 }
905
906 #[test]
907 fn test_library_function_detection() {
908 let analyzer = CallGraphAnalyzer::new_default();
909 assert!(analyzer.is_library_function("printf"));
910 assert!(analyzer.is_library_function("libc_start_main"));
911 assert!(analyzer.is_library_function("__stack_chk_fail"));
912 assert!(!analyzer.is_library_function("user_function"));
913 assert!(!analyzer.is_library_function("main"));
914 }
915
916 #[test]
917 fn test_node_type_classification() {
918 let analyzer = CallGraphAnalyzer::new_default();
919
920 let entry_function = Function {
921 name: "_start".to_string(),
922 start_address: 0x1000,
923 end_address: 0x1100,
924 size: 256,
925 function_type: FunctionType::Entrypoint,
926 calling_convention: None,
927 parameters: Vec::new(),
928 return_type: None,
929 };
930
931 let elf_data = vec![
933 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00, ];
942
943 match crate::BinaryFile::parse(&elf_data) {
946 Ok(binary) => {
947 let node_type = analyzer.classify_node_type(&entry_function, &binary);
948 assert_eq!(node_type, NodeType::EntryPoint);
949 }
950 Err(_) => {
951 assert_eq!(entry_function.function_type, FunctionType::Entrypoint);
954 }
955 }
956 }
957}