callgraph_verify/disasm.rs
1//! Recursive-descent function disassembly over real PE bytes (iced-x86).
2//!
3//! Given a function entry VA, we decode instructions and follow the function's
4//! *intra-function* control flow (conditional branches + their fallthrough,
5//! unconditional `JMP rel`) until every reachable path terminates at a `RET` /
6//! interrupt / undecodable byte. Along the way we harvest, purely from the
7//! decoded operands, every fact that lets us catch a rebuild hallucination:
8//!
9//! * `direct_calls` — targets of `CALL rel16/rel32` (the call graph proper)
10//! * `tail_calls` — `JMP rel` leaving the body (a tail call into another fn)
11//! * `indirect_calls`/`indirect_jumps` — counts of register/memory dispatch
12//! * `vcall_slots` — displacements of `CALL/JMP [reg+0xNN]` (vtable slots)
13//! * `iat_calls` — absolute targets of `CALL [0x00a28xxx]` (IAT imports)
14//! * `field_offsets` — displacements of `[reg+0xNN]` accesses (struct fields),
15//! excluding ESP/EBP (those are stack locals/args)
16//! * `globals` — absolute data addresses referenced as `[0x00bXXXXX]`
17//! * `imm_addrs` — in-image immediates (`MOV [esi],0x00a7e38c` vtable VAs,
18//! function pointers, string-literal addresses)
19//!
20//! This is *structural* ground truth: no decompiler in the loop.
21
22use crate::pe::Pe;
23use iced_x86::{Decoder, DecoderOptions, FlowControl, Instruction, Mnemonic, OpKind, Register};
24use std::collections::{BTreeMap, BTreeSet};
25
26/// Upper bound on how far past the entry we follow control flow. Oblivion's
27/// largest functions are a few KB; 64 KB stops a mis-decoded `JMP` from walking
28/// the whole section.
29const MAX_FN_SPAN: u64 = 0x10000;
30/// `[reg+disp]` with disp below this is treated as a struct-field access; larger
31/// displacements are almost certainly absolute addresses or noise.
32const MAX_FIELD_OFFSET: u64 = 0x8000;
33
34/// The recovered "kind" of a single call argument, harvested from the instruction that set it
35/// up (a register load or a `PUSH`). Coarse on purpose — only the distinctions a faithful port
36/// must preserve, and only those a short local scan can recover without false positives.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ArgKind {
39 /// An immediate constant (`PUSH 0x10`, `MOV ECX,0`, `XOR ECX,ECX`).
40 Imm(u64),
41 /// A struct-field load `[reg+disp]` off a non-stack base (`PUSH [ESI+0xc]`).
42 Field(u64),
43 /// An absolute in-image data reference `[0x00bXXXXX]` (a global).
44 Global(u64),
45 /// Address of a stack local (`LEA ECX,[EBP-8]` / `LEA EAX,[ESP+4]`) — an out-param / temp.
46 LocalAddr,
47 /// Anything not confidently classified (a register value, computed expression, prior result).
48 Other,
49}
50
51/// One direct call site with the argument set-up recovered by a short backward scan of its basic
52/// block. `ecx`/`edx` are the fastcall/thiscall register args (the receiver lands in `ecx`); `stack`
53/// is the pushed args in source (left-to-right) order. `None`/empty means "not recovered in-block".
54#[derive(Debug, Clone, Default)]
55pub struct CallSite {
56 pub target: u64,
57 pub ecx: Option<ArgKind>,
58 pub edx: Option<ArgKind>,
59 pub stack: Vec<ArgKind>,
60}
61
62#[derive(Debug, Default)]
63pub struct CallGraph {
64 pub entry: u64,
65 /// Direct `CALL` targets → number of call sites hitting that target.
66 pub direct_calls: BTreeMap<u64, u32>,
67 /// Per-call-site recovered argument shapes (one entry per direct `CALL` instruction).
68 pub call_sites: Vec<CallSite>,
69 /// Direct `JMP rel` targets that leave the walked body (tail calls).
70 pub tail_calls: BTreeSet<u64>,
71 pub indirect_calls: u32,
72 pub indirect_jumps: u32,
73 /// vtable slot displacements seen in `CALL/JMP [reg+0xNN]`.
74 pub vcall_slots: BTreeSet<u64>,
75 /// IAT import slot addresses seen in `CALL [0x00a28xxx]`.
76 pub iat_calls: BTreeSet<u64>,
77 /// Struct-field displacements off a non-stack base register.
78 pub field_offsets: BTreeSet<u64>,
79 /// Absolute data addresses referenced as memory operands (globals).
80 pub globals: BTreeSet<u64>,
81 /// In-image immediate values (vtable VAs, fn pointers, string addresses).
82 pub imm_addrs: BTreeSet<u64>,
83 /// Highest instruction-end VA reached — the function's apparent extent.
84 pub max_end: u64,
85 pub decode_errors: u32,
86 /// First few decode failures as `(ip, raw_bytes)`. Decoders hitting
87 /// invalid opcodes is normal at padding/alignment/data boundaries, but a
88 /// long run inside what is supposed to be a function usually means the
89 /// entry VA is wrong, or the function crosses into embedded data. The
90 /// hex bytes here are enough to tell which.
91 pub decode_error_samples: Vec<(u64, Vec<u8>)>,
92}
93
94impl CallGraph {
95 pub fn call_targets(&self) -> BTreeSet<u64> {
96 self.direct_calls.keys().copied().collect()
97 }
98
99 /// Every in-image address this function references — call targets, tail-call
100 /// JMPs, IAT slots, global data refs and baked immediates (vtable VAs / fn
101 /// pointers / string addresses). Used to detect inlining: if callee C's whole
102 /// footprint also appears in caller F's footprint, the binary pasted C's body
103 /// into F, so a Rust call F→C is faithful (un-inlined), not hallucinated.
104 pub fn footprint(&self) -> BTreeSet<u64> {
105 let mut s: BTreeSet<u64> = self.direct_calls.keys().copied().collect();
106 s.extend(&self.tail_calls);
107 s.extend(&self.iat_calls);
108 s.extend(&self.globals);
109 s.extend(&self.imm_addrs);
110 s
111 }
112}
113
114/// Harvest every memory/immediate operand fact from one decoded instruction.
115fn scan_operands(pe: &Pe, instr: &Instruction, cg: &mut CallGraph) {
116 for i in 0..instr.op_count() {
117 match instr.op_kind(i) {
118 OpKind::Memory => {
119 let base = instr.memory_base();
120 let index = instr.memory_index();
121 let disp = instr.memory_displacement64();
122 if base == Register::None && index == Register::None {
123 // absolute `[0x...]` — a global/static data reference.
124 // Skip LEA: it computes an address without dereferencing, so
125 // the operand isn't a memory reference. (BUG #14)
126 if pe.section_of(disp).is_some() && instr.mnemonic() != Mnemonic::Lea {
127 cg.globals.insert(disp);
128 }
129 } else if pe.section_of(disp).is_some()
130 && instr.mnemonic() != Mnemonic::Lea
131 {
132 // Indexed `[base + index*scale + disp]` (or `[reg+disp]`)
133 // off a large in-image displacement — a table base, not a
134 // struct field (e.g. FALL.EXE `[eax*4+0x18F01]`). Record it
135 // as a global so bodies may bake the true address without
136 // tripping HALLUCINATED-ADDR. Small offsets miss
137 // `section_of` and fall through to the field branch below;
138 // LEA never dereferences (BUG #14), so it stays excluded.
139 cg.globals.insert(disp);
140 } else if base != Register::None
141 && base != Register::ESP
142 && base != Register::EBP
143 && index == Register::None
144 && disp > 0
145 && disp < MAX_FIELD_OFFSET
146 // LEA computes an address (pointer math / &field), it doesn't
147 // dereference a struct field — skip it to avoid e.g. the
148 // `LEA EDX,[EAX+1]` of a strlen loop looking like field 0x1.
149 && instr.mnemonic() != Mnemonic::Lea
150 {
151 // `[reg+0xNN]` off a non-stack base — a struct field access.
152 cg.field_offsets.insert(disp);
153 }
154 }
155 OpKind::Immediate8
156 | OpKind::Immediate8to16
157 | OpKind::Immediate8to32
158 | OpKind::Immediate16
159 | OpKind::Immediate32 => {
160 let v = instr.immediate(i);
161 // In-image immediate = a baked address (vtable VA, fn ptr, string).
162 if pe.section_of(v).is_some() {
163 cg.imm_addrs.insert(v);
164 }
165 }
166 _ => {}
167 }
168 }
169}
170
171/// Classify operand `i` of `instr` as an argument kind (immediate / field / global / other).
172fn classify_operand(pe: &Pe, instr: &Instruction, i: u32) -> ArgKind {
173 match instr.op_kind(i) {
174 OpKind::Immediate8
175 | OpKind::Immediate8to16
176 | OpKind::Immediate8to32
177 | OpKind::Immediate16
178 | OpKind::Immediate32 => ArgKind::Imm(instr.immediate(i)),
179 OpKind::Memory => {
180 let base = instr.memory_base();
181 let index = instr.memory_index();
182 let disp = instr.memory_displacement64();
183 if base == Register::None && index == Register::None {
184 if pe.section_of(disp).is_some() {
185 ArgKind::Global(disp)
186 } else {
187 ArgKind::Other
188 }
189 } else if base != Register::ESP
190 && base != Register::EBP
191 && base != Register::None
192 && index == Register::None
193 && disp > 0
194 && disp < MAX_FIELD_OFFSET
195 {
196 ArgKind::Field(disp)
197 } else {
198 ArgKind::Other
199 }
200 }
201 _ => ArgKind::Other,
202 }
203}
204
205/// Classify the *source* of a register-setup instruction (`MOV reg,src` / `LEA reg,src` /
206/// `XOR reg,reg`). Returns the arg kind the register holds going into the following `CALL`.
207fn classify_reg_src(pe: &Pe, instr: &Instruction) -> ArgKind {
208 match instr.mnemonic() {
209 Mnemonic::Lea => {
210 let base = instr.memory_base();
211 if base == Register::EBP || base == Register::ESP {
212 ArgKind::LocalAddr
213 } else {
214 ArgKind::Other // lea of a field address — not distinguished in v1
215 }
216 }
217 Mnemonic::Xor => ArgKind::Imm(0), // only called when op0==op1 (zero idiom)
218 Mnemonic::Mov | Mnemonic::Movzx | Mnemonic::Movsx => classify_operand(pe, instr, 1),
219 _ => ArgKind::Other,
220 }
221}
222
223/// True if `instr` writes register `reg` as its destination (op0) via a recognized setup form.
224fn writes_reg(instr: &Instruction, reg: Register) -> bool {
225 if instr.op0_kind() != OpKind::Register || instr.op0_register() != reg {
226 return false;
227 }
228 match instr.mnemonic() {
229 Mnemonic::Mov | Mnemonic::Lea | Mnemonic::Movzx | Mnemonic::Movsx => true,
230 // XOR reg,reg is the zero idiom only when both operands are the same register.
231 Mnemonic::Xor => instr.op1_kind() == OpKind::Register && instr.op1_register() == reg,
232 _ => false,
233 }
234}
235
236/// Recover the argument shape of a `CALL` from the instructions preceding it in its basic block
237/// (`buf` excludes the CALL itself). Scans backward: the first `PUSH`es encountered are the
238/// left-to-right stack args (cdecl/stdcall push right-to-left, so backward order = source order),
239/// and the first writes to ECX/EDX are the fastcall/thiscall register args.
240pub(crate) fn recover_call_site(pe: &Pe, buf: &[Instruction], target: u64) -> CallSite {
241 let mut cs = CallSite {
242 target,
243 ..Default::default()
244 };
245 for instr in buf.iter().rev().take(64) {
246 // Stop at a prologue / frame boundary: an explicit ESP adjust (`sub esp,N`) or any
247 // FS-relative access (SEH frame set-up). Argument pushes never precede these, so this
248 // keeps callee-saved / SEH-cookie pushes out of the recovered argument list.
249 if is_frame_boundary(instr) {
250 break;
251 }
252 if instr.mnemonic() == Mnemonic::Push {
253 cs.stack.push(classify_operand(pe, instr, 0));
254 } else if cs.ecx.is_none() && writes_reg(instr, Register::ECX) {
255 cs.ecx = Some(classify_reg_src(pe, instr));
256 } else if cs.edx.is_none() && writes_reg(instr, Register::EDX) {
257 cs.edx = Some(classify_reg_src(pe, instr));
258 }
259 }
260 cs
261}
262
263/// A prologue / stack-frame boundary that argument set-up never precedes: an `ADD/SUB ESP,imm`
264/// or any instruction touching FS-relative memory (the SEH frame).
265fn is_frame_boundary(instr: &Instruction) -> bool {
266 if matches!(instr.mnemonic(), Mnemonic::Add | Mnemonic::Sub)
267 && instr.op0_kind() == OpKind::Register
268 && instr.op0_register() == Register::ESP
269 {
270 return true;
271 }
272 (0..instr.op_count())
273 .any(|i| instr.op_kind(i) == OpKind::Memory && instr.memory_segment() == Register::FS)
274}
275
276/// If `addr` is a one-instruction thunk (`JMP target`), return `target`. Callers often invoke a
277/// function through such a thunk (a tail-jump stub, or `/OPT:ICF`-folded forwarder), so the byte
278/// call edge points at the thunk, not the real callee — following it one level lets the call-graph
279/// check see the true target.
280pub fn resolve_thunk(pe: &Pe, addr: u64) -> Option<u64> {
281 let bytes = pe.read(addr, 16)?;
282 let mut dec = Decoder::with_ip(32, bytes, addr, DecoderOptions::NONE);
283 if !dec.can_decode() {
284 return None;
285 }
286 let instr = dec.decode();
287 if instr.flow_control() == FlowControl::UnconditionalBranch {
288 let t = instr.near_branch_target();
289 if t != 0 {
290 return Some(t);
291 }
292 }
293 None
294}
295
296/// Does `instr` *read* `reg` (as a source register or a memory base/index)? Excludes the
297/// destination register operand (op0) so a pure write like `MOV ECX,EAX` doesn't count as a read.
298fn reads_reg(instr: &Instruction, reg: Register) -> bool {
299 for i in 0..instr.op_count() {
300 match instr.op_kind(i) {
301 OpKind::Memory => {
302 if instr.memory_base() == reg || instr.memory_index() == reg {
303 return true;
304 }
305 }
306 OpKind::Register if i > 0 && instr.op_register(i) == reg => {
307 return true;
308 }
309 _ => {}
310 }
311 }
312 false
313}
314
315/// Heuristically determine whether the function at `addr` takes register parameters in ECX / EDX
316/// (i.e. is thiscall/fastcall). Scans the entry block for a READ of the register before any write:
317/// thiscall reads ECX (the `this`), fastcall also reads EDX, cdecl/stdcall read neither. Used to
318/// align a caller's recovered register args to the callee's actual parameters (so a stray EDX in a
319/// caller is not mistaken for an argument to a thiscall callee). Conservative: under-detection just
320/// means a position is skipped, never a false mismatch.
321pub fn reg_params(pe: &Pe, addr: u64) -> (bool, bool) {
322 let Some(bytes) = pe.read(addr, 32 * 32) else {
323 return (false, false);
324 };
325 let mut dec = Decoder::with_ip(32, bytes, addr, DecoderOptions::NONE);
326 let mut instr = Instruction::default();
327 let (mut ecx_written, mut edx_written) = (false, false);
328 let (mut ecx_param, mut edx_param) = (false, false);
329 let mut n = 0;
330 while dec.can_decode() && n < 24 {
331 dec.decode_out(&mut instr);
332 if instr.is_invalid() {
333 break;
334 }
335 if !ecx_written && reads_reg(&instr, Register::ECX) {
336 ecx_param = true;
337 }
338 if !edx_written && reads_reg(&instr, Register::EDX) {
339 edx_param = true;
340 }
341 if instr.op0_kind() == OpKind::Register {
342 match instr.op0_register() {
343 Register::ECX => ecx_written = true,
344 Register::EDX => edx_written = true,
345 _ => {}
346 }
347 }
348 // A leaf that immediately forwards its incoming registers to another call is still
349 // thiscall/fastcall even though iced-x86 cannot represent the implicit register use on
350 // the CALL itself. Preserve untouched ECX/EDX on the first instruction so constructors
351 // such as BSScissorTriShape (which forwards both registers to its base ctor) align stack
352 // arguments correctly. Later calls retain the caller-saved rule below.
353 if n == 0
354 && matches!(instr.flow_control(), FlowControl::Call)
355 && instr.near_branch_target() != instr.next_ip()
356 {
357 if !ecx_written {
358 ecx_param = true;
359 }
360 if !edx_written {
361 edx_param = true;
362 }
363 }
364 // Per the x86 cdecl/fastcall/thiscall conventions, ECX and EDX are caller-saved:
365 // any CALL in the entry block clobbers them, so a later READ of ECX/EDX is not
366 // evidence that the callee takes that register as a parameter. Mark them written
367 // across the CALL — without this, a stray `MOV EAX, [ECX]` after a prior CALL
368 // would falsely set `ecx_param = true` and pollute ARG-SHAPE diffs.
369 if matches!(instr.flow_control(), FlowControl::Call) {
370 ecx_written = true;
371 edx_written = true;
372 }
373 if !matches!(instr.flow_control(), FlowControl::Next | FlowControl::Call) {
374 break;
375 }
376 n += 1;
377 }
378 (ecx_param, edx_param)
379}
380
381/// Walk the function at `entry` and return its facts, sourced purely from bytes.
382pub fn analyze(pe: &Pe, entry: u64) -> Result<CallGraph, String> {
383 if !pe.is_executable_va(entry) {
384 return Err(format!(
385 "{entry:#010x} is not inside any PE section of this image\n{}",
386 pe.describe_sections(entry)
387 ));
388 }
389 let mut cg = CallGraph {
390 entry,
391 ..Default::default()
392 };
393 let lo = entry;
394 let hi = entry.saturating_add(MAX_FN_SPAN);
395
396 let mut work = vec![entry];
397 let mut seen: BTreeSet<u64> = BTreeSet::new();
398 // Per-branch-target seed: when we encounter `Jcc target` / `JMP target` /
399 // indirect-jump case, the args set up before the branch (PUSHes, MOV-to-ECX,
400 // etc.) also apply to the target block's first CALL — the predecessor and
401 // target share the same register/stack state at the branch instruction. The
402 // outer loop resets `block_buf` when it pops a new `start`, so without this
403 // seed map the target's calls would be processed with an empty `block_buf`
404 // and `recover_call_site` would undercount arguments. (BUG #8 fix.)
405 //
406 // Multiple predecessors may push the same target — `entry().or_insert_with`
407 // keeps the FIRST push, since the args are equivalent along either path up
408 // to the branch instruction itself (the differing branches only affect the
409 // value of flags, which `recover_call_site` doesn't read).
410 let mut branch_seed: std::collections::HashMap<u64, Vec<Instruction>> =
411 std::collections::HashMap::new();
412
413 while let Some(start) = work.pop() {
414 if !seen.insert(start) {
415 continue;
416 }
417 let Some(bytes) = pe.read(start, 32 * 64) else {
418 continue;
419 };
420 let mut dec = Decoder::with_ip(32, bytes, start, DecoderOptions::NONE);
421 let mut instr = Instruction::default();
422 // Straight-line instruction buffer for the current block, used to recover the argument
423 // set-up preceding each CALL. Reset at block boundaries (branches) and after each call.
424 // For non-entry `start`s, seed from the predecessor branch (see `branch_seed`).
425 let mut block_buf: Vec<Instruction> = branch_seed.remove(&start).unwrap_or_default();
426
427 loop {
428 if !dec.can_decode() {
429 break;
430 }
431 let ip = dec.ip();
432 if ip != start && seen.contains(&ip) {
433 break; // block already covered by another path
434 }
435 if ip > start {
436 seen.insert(ip);
437 }
438 dec.decode_out(&mut instr);
439 if instr.is_invalid() {
440 cg.decode_errors += 1;
441 // Keep up to 5 samples so the user can tell at a glance
442 // whether the failures are padding (single 0x00/0xCC/0x90),
443 // an aligned jump table (4 distinct bytes), or something
444 // structural (random bytes that decode to nothing — entry
445 // VA is probably wrong).
446 if cg.decode_error_samples.len() < 5 {
447 let bytes = pe.read(ip, 8).unwrap_or(&[]).to_vec();
448 cg.decode_error_samples.push((ip, bytes));
449 }
450 break;
451 }
452 cg.max_end = cg.max_end.max(instr.next_ip());
453 scan_operands(pe, &instr, &mut cg);
454 block_buf.push(instr);
455
456 match instr.flow_control() {
457 FlowControl::Call => {
458 let t = instr.near_branch_target();
459 if t != 0 {
460 *cg.direct_calls.entry(t).or_insert(0) += 1;
461 let prior = &block_buf[..block_buf.len() - 1]; // exclude the CALL itself
462 let cs = recover_call_site(pe, prior, t);
463 cg.call_sites.push(cs);
464 }
465 block_buf.clear(); // args after a call start fresh
466 }
467 FlowControl::IndirectCall => {
468 cg.indirect_calls += 1;
469 record_indirect_target(&instr, pe, &mut cg);
470 block_buf.clear(); // an indirect call's args must not leak into the next call
471 }
472 FlowControl::ConditionalBranch => {
473 let t = instr.near_branch_target();
474 if (lo..hi).contains(&t) {
475 branch_seed.entry(t).or_insert_with(|| block_buf.clone());
476 work.push(t);
477 } else {
478 cg.tail_calls.insert(t);
479 }
480 // Don't clear `block_buf` here: the args set up before the
481 // conditional apply to BOTH the taken and the fallthrough
482 // paths (they share register/stack state at the branch).
483 // Clearing would make the fallthrough's first CALL recover
484 // an empty argument set. The buffer is reset by the next
485 // CALL we encounter (Call/IndirectCall arms above clear
486 // post-call) or by an explicit boundary.
487 }
488 FlowControl::UnconditionalBranch => {
489 let t = instr.near_branch_target();
490 if (lo..hi).contains(&t) {
491 branch_seed.entry(t).or_insert_with(|| block_buf.clone());
492 work.push(t);
493 } else {
494 cg.tail_calls.insert(t);
495 }
496 break;
497 }
498 FlowControl::IndirectBranch => {
499 cg.indirect_jumps += 1;
500 record_indirect_target(&instr, pe, &mut cg);
501 let mut cases: Vec<u64> = Vec::new();
502 follow_jump_table(&instr, pe, lo, hi, &mut cases);
503 for c in &cases {
504 branch_seed.entry(*c).or_insert_with(|| block_buf.clone());
505 work.push(*c);
506 }
507 break;
508 }
509 FlowControl::Return | FlowControl::Exception => break,
510 // NOTE: `Interrupt` (`int n`) is deliberately NOT terminal:
511 // DOS `int 21h` / DPMI `int 31h` are API calls that return
512 // normally, so decoding must continue at `next_ip()`.
513 // Treating them like `ret` truncated listings and hid
514 // calls reachable only via post-`int` fallthrough.
515 FlowControl::Next | FlowControl::Interrupt | FlowControl::XbeginXabortXend => {}
516 }
517 }
518 }
519 Ok(cg)
520}
521
522/// Addresses a tail-call target references (its globals, baked immediates,
523/// and IAT slots), unioned over every `JMP`-out edge of `cg`.
524///
525/// A tail-call `JMP G` transfers control to G, so G's data references execute
526/// as part of THIS function. The call-graph diff (`diff_body_calls`) already
527/// counts a tail target's *callees* as acceptable spellings; this is the
528/// address-side mirror for the HALLUCINATED-ADDR check: a port that
529/// duplicates a shared tail body (e.g. a predicate tail-jumped into from two
530/// origins) bakes the tail's addresses faithfully. Only ever ADDS acceptance
531/// — a target that fails to analyze is skipped, never an error.
532pub fn tail_referenced(pe: &Pe, cg: &CallGraph) -> BTreeSet<u64> {
533 let mut out = BTreeSet::new();
534 for t in &cg.tail_calls {
535 if let Ok(tcg) = analyze(pe, *t) {
536 out.extend(&tcg.globals);
537 out.extend(&tcg.imm_addrs);
538 out.extend(&tcg.iat_calls);
539 }
540 }
541 out
542}
543
544/// Follow an MSVC-style `switch` jump table: `JMP dword ptr [idx*4 + TABLE_VA]`.
545/// `TABLE_VA` (the operand displacement, an in-image .rdata address) holds an
546/// array of u32 case addresses. We read entries until one falls outside the
547/// function's `[lo, hi)` span — switch cases always point back into the function,
548/// so that's a reliable terminator. Recovers the case bodies (and their `CALL`s)
549/// that a plain recursive descent, unable to follow an indirect jump, would miss.
550pub(crate) fn follow_jump_table(
551 instr: &Instruction,
552 pe: &Pe,
553 lo: u64,
554 hi: u64,
555 work: &mut Vec<u64>,
556) {
557 for i in 0..instr.op_count() {
558 if instr.op_kind(i) != OpKind::Memory {
559 continue;
560 }
561 let disp = instr.memory_displacement64();
562 // Need a scaled index (the case selector) and an in-image table base.
563 if instr.memory_index() == Register::None || pe.section_of(disp).is_none() {
564 return;
565 }
566 let mut k = 0u64;
567 while k < 1024 {
568 let Some(b) = pe.read(disp + k * 4, 4) else {
569 break;
570 };
571 if b.len() < 4 {
572 break;
573 }
574 let target = u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as u64;
575 if !(lo..hi).contains(&target) {
576 break; // first entry outside the function ends the table
577 }
578 work.push(target);
579 k += 1;
580 }
581 return;
582 }
583}
584
585/// Classify an indirect `CALL`/`JMP [...]` target: `[reg+disp]` is a vtable slot
586/// (record the displacement); `[abs]` in the IAT range is an import (record addr).
587fn record_indirect_target(instr: &Instruction, pe: &Pe, cg: &mut CallGraph) {
588 // The branch target operand is the (single) memory operand, if any.
589 for i in 0..instr.op_count() {
590 if instr.op_kind(i) == OpKind::Memory {
591 let base = instr.memory_base();
592 let disp = instr.memory_displacement64();
593 if base == Register::None && instr.memory_index() == Register::None {
594 if pe.section_of(disp).is_some() {
595 cg.iat_calls.insert(disp); // CALL [0x00a28xxx]
596 }
597 } else if base != Register::None && disp < MAX_FIELD_OFFSET {
598 cg.vcall_slots.insert(disp); // CALL [reg+slot]
599 }
600 return;
601 }
602 }
603}