M5+M7: indirect-dispatch reachability + .rdata string detection
Two MEDIUM milestones bundled (both opportunistic per plan; both small).
## M5 — indirect-dispatch reachability
- `xenia_analysis::indirect`: per-basic-block register tracker over each
detected function. Recognises the canonical static-vtable pattern
`lis+addi → lwz off(rA) → mtctr → bcctrl` where rA holds a known M3
vtable address. Emits one `Xref { kind: IndirectCall }` per resolvable
bcctrl site.
- PowerPC ABI awareness: `bl`-style calls clobber volatile r0..r12 + ctr
but preserve non-volatile r13..r31, so a vtable pointer parked in r30/r31
before a call survives.
- Label-based basic-block boundaries kill register state — bounds
false-positive risk for jump-IN paths.
- New `XrefKind::IndirectCall` variant (DB tag `'ind_call'`).
- New SQL view `v_indirect_reachability_from_entry` — strict superset of
`v_reachability_from_entry`, taking `ind_call` edges in the BFS.
Sylpheed yield: 0 edges detected. The binary's 1,001 static lis+addi
references into vtables are nearly all constructor-side vptr writes, not
dispatches; real method dispatch goes through `this->vptr` which requires
alias analysis we explicitly don't do. Documented in SCHEMA.md as the
expected limitation. Three unit tests cover the synthetic-correctness path.
## M7 — string / constant-pool detection
- `xenia_analysis::strings`: scans `.rdata` for runs of ≥ 6 printable
ASCII bytes (NUL-terminated) and ≥ 6 UTF-16LE code units (basic-plane
printable ASCII, NUL u16 terminator).
- New `strings(address PK, encoding, length, content)` table + encoding index.
- Implicit cross-ref via existing `xrefs.kind='ref'` rows whose target
matches a strings.address.
Sylpheed yield: 6,311 ASCII strings (including embedded HLSL shader source
and AS_CB_SURFACE_SWIZZLE_* assertion strings). 9,132 lis+addi sites
cross-reference detected strings — names source PCs near each string in
one query. Four unit tests cover encoding detection, NUL termination, and
short-run rejection.
Tests 626→633 (+3 indirect, +4 strings).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
468
crates/xenia-analysis/src/indirect.rs
Normal file
468
crates/xenia-analysis/src/indirect.rs
Normal file
@@ -0,0 +1,468 @@
|
||||
//! Indirect-dispatch reachability for vtable-bound `bcctrl`/`bctrl` sites.
|
||||
//!
|
||||
//! Walks each detected function with a tiny per-basic-block register tracker,
|
||||
//! recognising the canonical MSVC PowerPC pattern that loads a slot from a
|
||||
//! statically-addressed vtable into CTR and indirectly calls it:
|
||||
//!
|
||||
//! ```text
|
||||
//! lis rA, hi
|
||||
//! addi rA, rA, lo ; rA = vtable_address
|
||||
//! lwz rB, slot*4(rA) ; rB = vtable[slot]
|
||||
//! mtctr rB ; CTR = vtable[slot]
|
||||
//! bcctrl ; indirect call → vtable[slot]
|
||||
//! ```
|
||||
//!
|
||||
//! Pattern hits are emitted as `(source_pc, target_pc)` pairs that callers
|
||||
//! insert into the `xrefs` table with `kind='ind_call'`.
|
||||
//!
|
||||
//! ### What this does NOT cover
|
||||
//!
|
||||
//! - Vtable pointer loaded from a `this`-pointer field (`lwz rA, off(this)`)
|
||||
//! is the dominant pattern in real C++ code; resolving it requires
|
||||
//! alias / points-to analysis that's far beyond this layer's scope.
|
||||
//! - Indirect calls via function-pointer fields (callbacks) are similarly
|
||||
//! unresolvable without object-flow analysis.
|
||||
//! - Register state is intentionally killed at every label (basic-block
|
||||
//! boundary) — we don't try to do flow-sensitive merging across joins.
|
||||
//!
|
||||
//! Reference: IBM PowerPC ABI on register-save convention, plus the
|
||||
//! `xenia_analysis::xref` `lis+addi`/`lis+ori` tracker which we mirror
|
||||
//! conceptually.
|
||||
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
use crate::func::FuncAnalysis;
|
||||
use crate::vtables::Vtable;
|
||||
|
||||
/// One detected indirect-call edge: `bcctrl` at `source` jumps to `target`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct IndirectEdge {
|
||||
pub source: u32,
|
||||
pub target: u32,
|
||||
/// Vtable the source resolved through.
|
||||
pub via_vtable: u32,
|
||||
/// Method slot index within the vtable.
|
||||
pub slot: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum RegVal {
|
||||
/// Register holds a known constant (e.g. after `lis+addi`).
|
||||
Const(u32),
|
||||
/// Register holds a method pointer loaded from a known vtable slot.
|
||||
MethodPtr {
|
||||
vtable_addr: u32,
|
||||
slot: u32,
|
||||
method_pc: u32,
|
||||
},
|
||||
}
|
||||
|
||||
const OP_ADDI: u32 = 14;
|
||||
const OP_ADDIS: u32 = 15;
|
||||
const OP_BCCTR: u32 = 19; // also covers blr — distinguish via XO
|
||||
const OP_LWZ: u32 = 32;
|
||||
const OP_ORI: u32 = 24;
|
||||
const OP_X_FORM: u32 = 31; // mtspr / mr / etc.
|
||||
|
||||
/// Run the static indirect-dispatch scan. Returns one edge per resolvable
|
||||
/// `bcctrl` site.
|
||||
#[tracing::instrument(skip_all, fields(image_base = format_args!("{:#010x}", image_base)))]
|
||||
pub fn analyze(
|
||||
pe: &[u8],
|
||||
image_base: u32,
|
||||
func_analysis: &FuncAnalysis,
|
||||
vtables: &[Vtable],
|
||||
labels: &HashMap<u32, String>,
|
||||
) -> Vec<IndirectEdge> {
|
||||
let started = std::time::Instant::now();
|
||||
// Index vtables by their start VA so the lwz handler can decide
|
||||
// whether a given Const(addr) is "really" a vtable.
|
||||
let vtable_by_addr: BTreeMap<u32, &Vtable> =
|
||||
vtables.iter().map(|v| (v.address, v)).collect();
|
||||
|
||||
// Set of all "label"-bearing PCs in the analyzed binary. We treat each
|
||||
// label as a basic-block boundary (anything `loc_*` is a jump target,
|
||||
// so register state arriving at it is unreliable).
|
||||
let mut block_boundaries: HashSet<u32> = HashSet::with_capacity(labels.len());
|
||||
for &addr in labels.keys() {
|
||||
block_boundaries.insert(addr);
|
||||
}
|
||||
|
||||
let mut edges: Vec<IndirectEdge> = Vec::new();
|
||||
|
||||
for (&fn_start, fi) in &func_analysis.functions {
|
||||
if fi.is_saverestore { continue; }
|
||||
let mut reg: [Option<RegVal>; 32] = [None; 32];
|
||||
let mut ctr: Option<RegVal> = None;
|
||||
let mut pc = fn_start;
|
||||
while pc < fi.end {
|
||||
// Reset register state on basic-block entry. We don't reset on
|
||||
// the function entry itself (PC == fn_start) because labels and
|
||||
// function-starts coincide; the initial state is already None.
|
||||
if pc != fn_start && block_boundaries.contains(&pc) {
|
||||
reg = [None; 32];
|
||||
ctr = None;
|
||||
}
|
||||
|
||||
let instr = match read_instr(pe, image_base, pc) {
|
||||
Some(i) => i,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let op = instr >> 26;
|
||||
let rd = ((instr >> 21) & 0x1F) as usize;
|
||||
let ra = ((instr >> 16) & 0x1F) as usize;
|
||||
let simm = ((instr & 0xFFFF) as i16) as i32;
|
||||
let uimm = instr & 0xFFFF;
|
||||
|
||||
match op {
|
||||
// lis rD, IMM (== addis rD, r0, IMM)
|
||||
OP_ADDIS if ra == 0 => {
|
||||
reg[rd] = Some(RegVal::Const(uimm << 16));
|
||||
}
|
||||
// addis rD, rA, IMM
|
||||
OP_ADDIS => {
|
||||
if let Some(RegVal::Const(b)) = reg[ra] {
|
||||
reg[rd] = Some(RegVal::Const(b.wrapping_add(uimm << 16)));
|
||||
} else {
|
||||
reg[rd] = None;
|
||||
}
|
||||
}
|
||||
// addi rD, rA, IMM
|
||||
OP_ADDI if ra != 0 => {
|
||||
if let Some(RegVal::Const(b)) = reg[ra] {
|
||||
reg[rd] = Some(RegVal::Const(b.wrapping_add(simm as u32)));
|
||||
} else {
|
||||
reg[rd] = None;
|
||||
}
|
||||
}
|
||||
// li rD, IMM (== addi rD, 0, IMM)
|
||||
OP_ADDI => {
|
||||
reg[rd] = Some(RegVal::Const(simm as u32));
|
||||
}
|
||||
// ori rA, rS, IMM — note operand order: bits 21..25 = rS, 16..20 = rA
|
||||
OP_ORI => {
|
||||
let rs = rd; // bits 21..25 = source
|
||||
if let Some(RegVal::Const(b)) = reg[rs] {
|
||||
reg[ra] = Some(RegVal::Const(b | uimm));
|
||||
} else {
|
||||
reg[ra] = None;
|
||||
}
|
||||
}
|
||||
// lwz rD, off(rA) — try to resolve as vtable slot load.
|
||||
OP_LWZ => {
|
||||
if ra != 0
|
||||
&& let Some(RegVal::Const(base)) = reg[ra]
|
||||
{
|
||||
let target = base.wrapping_add(simm as u32);
|
||||
// Two-step lookup so we accept both:
|
||||
// (a) base = exact vtable head, simm/4 = slot
|
||||
// (b) base + simm = exact vtable head (rare;
|
||||
// compiler hoists the slot offset into addi)
|
||||
let resolved = resolve_vtable_slot(target, &vtable_by_addr)
|
||||
.or_else(|| resolve_vtable_slot_via_off(base, simm, &vtable_by_addr));
|
||||
reg[rd] = resolved.map(|(vt, slot, pc)| RegVal::MethodPtr {
|
||||
vtable_addr: vt, slot, method_pc: pc,
|
||||
});
|
||||
} else {
|
||||
reg[rd] = None;
|
||||
}
|
||||
}
|
||||
// X-form: mtspr/mtctr, bcctrl, mr, etc.
|
||||
OP_X_FORM => {
|
||||
let xo = (instr >> 1) & 0x3FF;
|
||||
match xo {
|
||||
467 => {
|
||||
// mtspr SPR, rS — PPC SPR field is split: high 5 bits
|
||||
// in PPC bits 16:20 (= Rust bits 11..15), low 5 bits
|
||||
// in PPC bits 11:15 (= Rust bits 16..20). Mirrors
|
||||
// the convention in `func.rs::is_mfspr_lr`.
|
||||
let spr = (((instr >> 11) & 0x1F) << 5) | ((instr >> 16) & 0x1F);
|
||||
if spr == 9 {
|
||||
ctr = reg[rd];
|
||||
}
|
||||
// Otherwise no observable effect on tracked state.
|
||||
}
|
||||
// Anything that writes rD (most arithmetic, loads, etc.) clobbers it.
|
||||
// Conservative: invalidate rD on any X-form that has rD in bits 21..25
|
||||
// and is NOT a comparison or branch.
|
||||
_ => {
|
||||
// Heuristic: most X-form ops with non-zero RC encode rD; we
|
||||
// invalidate to avoid stale Const propagation past arithmetic.
|
||||
// This is over-eager but safe (false negatives on edges, never
|
||||
// false positives).
|
||||
reg[rd] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// bcctr/bcctrl — opcode 19, XO=528. LK in low bit.
|
||||
OP_BCCTR => {
|
||||
let xo = (instr >> 1) & 0x3FF;
|
||||
if xo == 528 {
|
||||
let lk = (instr & 1) != 0;
|
||||
if lk
|
||||
&& let Some(RegVal::MethodPtr { vtable_addr, slot, method_pc }) = ctr
|
||||
{
|
||||
edges.push(IndirectEdge {
|
||||
source: pc,
|
||||
target: method_pc,
|
||||
via_vtable: vtable_addr,
|
||||
slot,
|
||||
});
|
||||
}
|
||||
// After the call, CTR is preserved but rD register
|
||||
// values across the call boundary are not trustworthy.
|
||||
// Don't touch reg state — most ABIs preserve only
|
||||
// some regs anyway.
|
||||
}
|
||||
}
|
||||
// op 18: b / bl / ba / bla. LK=1 is a call; LK=0 is an
|
||||
// unconditional branch with no fall-through (next PC is
|
||||
// reached only via a different basic block, which the
|
||||
// label-based reset already handles). On a call, the
|
||||
// PowerPC ABI marks r0..r12 + ctr as volatile and
|
||||
// r13..r31 as non-volatile (callee-saved); preserve the
|
||||
// non-volatile half so vtable pointers loaded into r30/r31
|
||||
// before a `bl` survive the call.
|
||||
18 => {
|
||||
let lk = (instr & 1) != 0;
|
||||
if lk {
|
||||
for r in 0..=12 { reg[r] = None; }
|
||||
ctr = None;
|
||||
}
|
||||
// LK=0 (`b`) makes fall-through unreachable; nothing to do —
|
||||
// any next reachable PC will hit a label boundary.
|
||||
}
|
||||
// Conditional branches (op 16) fall through; preserve all reg
|
||||
// state for the fall-through path. The label-based join-point
|
||||
// invalidation bounds false-positive risk for jump-IN paths.
|
||||
16 => {
|
||||
let lk = (instr & 1) != 0;
|
||||
if lk {
|
||||
for r in 0..=12 { reg[r] = None; }
|
||||
ctr = None;
|
||||
}
|
||||
}
|
||||
// Stores and loads we don't track explicitly clobber rD only
|
||||
// when rD is on the destination side; the conservative rule
|
||||
// is "any non-recognised opcode that may write rD invalidates it".
|
||||
36..=55 => {
|
||||
// Loads write rD; stores don't. The safe pessimisation is
|
||||
// to invalidate rD for the load family (32..=35, 40..=43, etc.)
|
||||
// and leave it alone for stores. We've already handled lwz
|
||||
// above; for the rest, invalidate rD.
|
||||
if matches!(op, 32..=35 | 40..=43 | 48..=51) {
|
||||
reg[rd] = None;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
pc = pc.wrapping_add(4);
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed_ms = started.elapsed().as_millis() as f64;
|
||||
metrics::histogram!("analysis.phase_ms", "phase" => "indirect").record(elapsed_ms);
|
||||
tracing::info!(
|
||||
edges = edges.len(),
|
||||
elapsed_ms,
|
||||
"indirect-dispatch scan complete"
|
||||
);
|
||||
edges
|
||||
}
|
||||
|
||||
fn read_instr(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> {
|
||||
let off = addr.wrapping_sub(image_base) as usize;
|
||||
if off + 4 > pe.len() { return None; }
|
||||
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
|
||||
}
|
||||
|
||||
/// `target = base + simm` where `target` is an exact vtable head (rare,
|
||||
/// compiler hoists the slot offset into the addi).
|
||||
fn resolve_vtable_slot_via_off(
|
||||
base: u32,
|
||||
simm: i32,
|
||||
vtable_by_addr: &BTreeMap<u32, &Vtable>,
|
||||
) -> Option<(u32, u32, u32)> {
|
||||
let target = base.wrapping_add(simm as u32);
|
||||
if let Some(v) = vtable_by_addr.get(&target)
|
||||
&& !v.methods.is_empty()
|
||||
{
|
||||
return Some((v.address, 0, v.methods[0]));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// `target` is an absolute address. If it falls inside a known vtable's
|
||||
/// `[address, address + length*4)` range AND is 4-aligned to a slot,
|
||||
/// return `(vtable_addr, slot, method_pc)`.
|
||||
fn resolve_vtable_slot(
|
||||
target: u32,
|
||||
vtable_by_addr: &BTreeMap<u32, &Vtable>,
|
||||
) -> Option<(u32, u32, u32)> {
|
||||
// BTreeMap range search for the largest key ≤ target.
|
||||
let (&vt_addr, vt) = vtable_by_addr.range(..=target).next_back()?;
|
||||
if target < vt_addr { return None; }
|
||||
let off = target - vt_addr;
|
||||
if !off.is_multiple_of(4) { return None; }
|
||||
let slot = off / 4;
|
||||
if slot >= vt.length { return None; }
|
||||
let method_pc = *vt.methods.get(slot as usize)?;
|
||||
Some((vt_addr, slot, method_pc))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::func::FuncInfo;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn mk_vtable(addr: u32, methods: Vec<u32>) -> Vtable {
|
||||
Vtable {
|
||||
address: addr,
|
||||
length: methods.len() as u32,
|
||||
col_address: None,
|
||||
class_name: "ANON_test".into(),
|
||||
rtti_present: false,
|
||||
base_classes_json: None,
|
||||
methods,
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode the canonical pattern at PC `start`:
|
||||
/// lis r3, hi
|
||||
/// addi r3, r3, lo ; r3 = vtable_addr
|
||||
/// lwz r4, slot*4(r3) ; r4 = vtable[slot]
|
||||
/// mtctr r4
|
||||
/// bcctrl
|
||||
fn encode_pattern(buf: &mut [u8], offset: usize, vtable_addr: u32, slot_off: i32) {
|
||||
let hi = (vtable_addr >> 16) as u16;
|
||||
let lo = (vtable_addr & 0xFFFF) as i16;
|
||||
let lis = (15u32 << 26) | (3 << 21) | (0 << 16) | (hi as u32);
|
||||
// addi r3, r3, lo (signed) — note: addi is treated as signed
|
||||
let addi = (14u32 << 26) | (3 << 21) | (3 << 16) | ((lo as u16) as u32);
|
||||
let lwz = (32u32 << 26) | (4 << 21) | (3 << 16) | ((slot_off as u16) as u32);
|
||||
// mtctr r4 = mtspr CTR(=9), r4. SPR_low (=9) → Rust bits 16-20;
|
||||
// SPR_high (=0) → Rust bits 11-15. Rc bit 0.
|
||||
let mtctr = (31u32 << 26) | (4 << 21) | (9 << 16) | (0 << 11) | (467 << 1);
|
||||
let bcctrl = (19u32 << 26) | (20 << 21) | (528 << 1) | 1; // bcctrl 20, 0
|
||||
let words = [lis, addi, lwz, mtctr, bcctrl];
|
||||
for (i, w) in words.iter().enumerate() {
|
||||
buf[offset + i * 4..offset + i * 4 + 4].copy_from_slice(&w.to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_canonical_lis_addi_lwz_mtctr_bcctrl() {
|
||||
let image_base = 0x82000000u32;
|
||||
let text_va = 0x1000u32;
|
||||
let pc_start = image_base + text_va;
|
||||
let vtable_addr = 0x82010000u32;
|
||||
|
||||
// PE: just the .text we'll write the pattern into.
|
||||
let mut pe = vec![0u8; 0x1100];
|
||||
encode_pattern(&mut pe, text_va as usize, vtable_addr, 8); // slot 2
|
||||
|
||||
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
|
||||
functions.insert(pc_start, FuncInfo {
|
||||
start: pc_start,
|
||||
end: pc_start + 5 * 4,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
});
|
||||
let func_analysis = FuncAnalysis {
|
||||
functions,
|
||||
save_gpr_base: None,
|
||||
restore_gpr_base: None,
|
||||
pdata_entries: Vec::new(),
|
||||
};
|
||||
|
||||
let vtables = vec![mk_vtable(vtable_addr, vec![0xAA, 0xBB, 0xCC, 0xDD])];
|
||||
let labels: HashMap<u32, String> = HashMap::new();
|
||||
let edges = analyze(&pe, image_base, &func_analysis, &vtables, &labels);
|
||||
|
||||
assert_eq!(edges.len(), 1);
|
||||
assert_eq!(edges[0].source, pc_start + 4 * 4); // bcctrl at 5th instruction
|
||||
assert_eq!(edges[0].target, 0xCC); // slot 2
|
||||
assert_eq!(edges[0].via_vtable, vtable_addr);
|
||||
assert_eq!(edges[0].slot, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_slot_yields_no_edge() {
|
||||
let image_base = 0x82000000u32;
|
||||
let text_va = 0x1000u32;
|
||||
let pc_start = image_base + text_va;
|
||||
let vtable_addr = 0x82010000u32;
|
||||
|
||||
let mut pe = vec![0u8; 0x1100];
|
||||
// Encode slot 12, but vtable only has 4 methods.
|
||||
encode_pattern(&mut pe, text_va as usize, vtable_addr, 48);
|
||||
|
||||
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
|
||||
functions.insert(pc_start, FuncInfo {
|
||||
start: pc_start,
|
||||
end: pc_start + 5 * 4,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
});
|
||||
let func_analysis = FuncAnalysis {
|
||||
functions,
|
||||
save_gpr_base: None,
|
||||
restore_gpr_base: None,
|
||||
pdata_entries: Vec::new(),
|
||||
};
|
||||
|
||||
let vtables = vec![mk_vtable(vtable_addr, vec![0xAA, 0xBB, 0xCC, 0xDD])];
|
||||
let labels: HashMap<u32, String> = HashMap::new();
|
||||
let edges = analyze(&pe, image_base, &func_analysis, &vtables, &labels);
|
||||
assert_eq!(edges.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn label_in_middle_kills_state() {
|
||||
let image_base = 0x82000000u32;
|
||||
let text_va = 0x1000u32;
|
||||
let pc_start = image_base + text_va;
|
||||
let vtable_addr = 0x82010000u32;
|
||||
|
||||
let mut pe = vec![0u8; 0x1100];
|
||||
encode_pattern(&mut pe, text_va as usize, vtable_addr, 0);
|
||||
|
||||
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
|
||||
functions.insert(pc_start, FuncInfo {
|
||||
start: pc_start,
|
||||
end: pc_start + 5 * 4,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
});
|
||||
let func_analysis = FuncAnalysis {
|
||||
functions,
|
||||
save_gpr_base: None,
|
||||
restore_gpr_base: None,
|
||||
pdata_entries: Vec::new(),
|
||||
};
|
||||
|
||||
let vtables = vec![mk_vtable(vtable_addr, vec![0xAA, 0xBB])];
|
||||
|
||||
// Label between addi and lwz — must kill the Const tracking.
|
||||
let mut labels: HashMap<u32, String> = HashMap::new();
|
||||
labels.insert(pc_start + 8, "loc_mid".to_string());
|
||||
|
||||
let edges = analyze(&pe, image_base, &func_analysis, &vtables, &labels);
|
||||
assert_eq!(edges.len(), 0, "label in middle of pattern must kill register state");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user