neo_decompiler/decompiler/analysis/
call_graph.rs1#![allow(
6 clippy::cast_possible_truncation,
7 clippy::cast_possible_wrap,
8 clippy::cast_sign_loss
9)]
10
11use std::collections::BTreeMap;
12
13use serde::Serialize;
14
15use crate::instruction::{Instruction, OpCode, Operand, OperandEncoding};
16use crate::manifest::ContractManifest;
17use crate::nef::NefFile;
18use crate::{syscalls, util};
19
20use super::{MethodRef, MethodTable};
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
24#[non_exhaustive]
25pub enum CallTarget {
26 Internal {
28 method: MethodRef,
30 },
31 MethodToken {
33 index: u16,
35 hash_le: String,
37 hash_be: String,
39 method: String,
41 parameters_count: u16,
43 has_return_value: bool,
45 call_flags: u8,
47 },
48 Syscall {
50 hash: u32,
52 name: Option<String>,
54 returns_value: bool,
56 },
57 Indirect {
59 opcode: String,
61 operand: Option<u16>,
63 },
64 UnresolvedInternal {
66 target: isize,
68 },
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
73pub struct CallEdge {
74 pub caller: MethodRef,
76 pub call_offset: usize,
78 pub opcode: String,
80 pub target: CallTarget,
82}
83
84#[derive(Debug, Clone, Default, Serialize)]
86pub struct CallGraph {
87 pub methods: Vec<MethodRef>,
89 pub edges: Vec<CallEdge>,
91}
92
93#[must_use]
95pub fn build_call_graph(
96 nef: &NefFile,
97 instructions: &[Instruction],
98 manifest: Option<&ContractManifest>,
99) -> CallGraph {
100 let table = MethodTable::new(instructions, manifest);
101 let mut methods: BTreeMap<usize, MethodRef> = table
102 .spans()
103 .iter()
104 .map(|span| (span.method.offset, span.method.clone()))
105 .collect();
106
107 let mut edges = Vec::new();
108 for (index, instr) in instructions.iter().enumerate() {
109 match instr.opcode {
110 OpCode::Syscall => {
111 let Some(Operand::Syscall(hash)) = instr.operand else {
112 continue;
113 };
114 let info = syscalls::lookup(hash);
115 edges.push(CallEdge {
116 caller: table.method_for_offset(instr.offset),
117 call_offset: instr.offset,
118 opcode: instr.opcode.to_string(),
119 target: CallTarget::Syscall {
120 hash,
121 name: info.map(|i| i.name.to_string()),
122 returns_value: info.map(|i| i.returns_value).unwrap_or(true),
123 },
124 });
125 }
126 OpCode::Call | OpCode::Call_L => {
127 let caller = table.method_for_offset(instr.offset);
128 match relative_target_isize(instructions, index, instr) {
129 Some(target) if target >= 0 => {
130 let target = target as usize;
131 let callee = table.resolve_internal_target(target);
132 methods.insert(callee.offset, callee.clone());
133 edges.push(CallEdge {
134 caller,
135 call_offset: instr.offset,
136 opcode: instr.opcode.to_string(),
137 target: CallTarget::Internal { method: callee },
138 });
139 }
140 Some(target) => edges.push(CallEdge {
141 caller,
142 call_offset: instr.offset,
143 opcode: instr.opcode.to_string(),
144 target: CallTarget::UnresolvedInternal { target },
145 }),
146 None => edges.push(CallEdge {
147 caller,
148 call_offset: instr.offset,
149 opcode: instr.opcode.to_string(),
150 target: CallTarget::UnresolvedInternal { target: -1 },
151 }),
152 }
153 }
154 OpCode::CallT => {
155 let Some(Operand::U16(index)) = instr.operand else {
156 continue;
157 };
158 let token = nef.method_tokens.get(index as usize);
159 if let Some(token) = token {
160 edges.push(CallEdge {
161 caller: table.method_for_offset(instr.offset),
162 call_offset: instr.offset,
163 opcode: instr.opcode.to_string(),
164 target: CallTarget::MethodToken {
165 index,
166 hash_le: util::format_hash(&token.hash),
167 hash_be: util::format_hash_be(&token.hash),
168 method: token.method.clone(),
169 parameters_count: token.parameters_count,
170 has_return_value: token.has_return_value,
171 call_flags: token.call_flags,
172 },
173 });
174 } else {
175 edges.push(CallEdge {
176 caller: table.method_for_offset(instr.offset),
177 call_offset: instr.offset,
178 opcode: instr.opcode.to_string(),
179 target: CallTarget::Indirect {
180 opcode: instr.opcode.to_string(),
181 operand: Some(index),
182 },
183 });
184 }
185 }
186 OpCode::CallA => {
187 edges.push(CallEdge {
189 caller: table.method_for_offset(instr.offset),
190 call_offset: instr.offset,
191 opcode: instr.opcode.to_string(),
192 target: CallTarget::Indirect {
193 opcode: instr.opcode.to_string(),
194 operand: None,
195 },
196 });
197 }
198 _ => {}
199 }
200 }
201
202 CallGraph {
203 methods: methods.into_values().collect(),
204 edges,
205 }
206}
207
208fn relative_target_isize(
209 instructions: &[Instruction],
210 index: usize,
211 instr: &Instruction,
212) -> Option<isize> {
213 let delta = match &instr.operand {
214 Some(Operand::Jump(v)) => *v as isize,
215 Some(Operand::Jump32(v)) => *v as isize,
216 _ => return None,
217 };
218 let base = instructions
219 .get(index + 1)
220 .map(|ins| ins.offset)
221 .unwrap_or_else(|| instr.offset + instr_len_fallback(instr));
222 Some(base as isize + delta)
223}
224
225fn instr_len_fallback(instr: &Instruction) -> usize {
226 match instr.opcode.operand_encoding() {
227 OperandEncoding::None => 1,
228 OperandEncoding::I8 | OperandEncoding::U8 | OperandEncoding::Jump8 => 2,
229 OperandEncoding::I16 | OperandEncoding::U16 => 3,
230 OperandEncoding::I32
231 | OperandEncoding::U32
232 | OperandEncoding::Jump32
233 | OperandEncoding::Syscall => 5,
234 OperandEncoding::I64 => 9,
235 OperandEncoding::Bytes(n) => 1 + n,
236 OperandEncoding::Data1 => 1 + 1 + bytes_len(instr),
237 OperandEncoding::Data2 => 1 + 2 + bytes_len(instr),
238 OperandEncoding::Data4 => 1 + 4 + bytes_len(instr),
239 }
240}
241
242fn bytes_len(instr: &Instruction) -> usize {
243 match &instr.operand {
244 Some(Operand::Bytes(bytes)) => bytes.len(),
245 _ => 0,
246 }
247}