use callgraph_verify::{analyze, cfg_fingerprint, diff_calls, has_direct_branch_target, Pe};
#[test]
fn real_binary_sweeps_agree_and_diff_clean() {
let exe = match std::env::var("CALLGRAPH_REAL_EXE") {
Ok(p) if !p.trim().is_empty() => p,
_ => {
eprintln!(
"skipping: set CALLGRAPH_REAL_EXE=<path-to-32bit-exe> to sweep a real binary"
);
return;
}
};
let pe = Pe::load(&exe).unwrap_or_else(|e| panic!("load {exe}: {e}"));
let refs = pe.referenced_addrs();
let (lo, hi) = pe.image_range();
let mut starts: Vec<(u64, u64)> = pe
.sections()
.iter()
.filter(|s| s.is_executable())
.map(|s| {
let start = pe.image_base + s.virtual_address as u64;
(start, start + s.virtual_size.max(s.raw_size) as u64)
})
.collect();
starts.sort();
assert!(!starts.is_empty(), "{exe} has no executable section");
let mut fns = 0u32;
let mut checked_callers = 0u32;
let mut calls = 0u32;
let mut disagreements = Vec::new();
let mut unclean = Vec::new();
for (start, end) in starts {
let mut va = start;
while va < end {
if let Ok(cg) = analyze(&pe, va) {
fns += 1;
let real: Vec<u64> = cg.call_targets().into_iter().collect();
match diff_calls(&pe, va, &real, true) {
Ok(d) if d.hallucinated.is_empty() && d.missing.is_empty() => {}
other => unclean.push((va, format!("{other:?}"))),
}
if refs.contains(&(va as u32)) {
checked_callers += 1;
for &t in cg.direct_calls.keys() {
if !(lo..hi).contains(&t) {
continue; }
calls += 1;
if !has_direct_branch_target(&pe, t) {
disagreements.push((va, t));
}
}
}
va = va.saturating_add(cg.max_end.saturating_sub(va).max(1));
} else {
va += 1;
}
}
}
eprintln!(
"{exe}: swept {fns} entries ({checked_callers} referenced), {calls} direct calls"
);
assert!(fns > 0, "no analyzable functions in {exe}");
assert!(
disagreements.is_empty(),
"{} of {calls} CALL targets disagree with the byte scan: {disagreements:?}",
disagreements.len()
);
assert!(
unclean.is_empty(),
"{} of {fns} functions not diff-clean: {unclean:?}",
unclean.len()
);
}
#[test]
fn real_le_fixup_post_images_are_referenced() {
let exe = match std::env::var("CALLGRAPH_REAL_EXE") {
Ok(p) if !p.trim().is_empty() => p,
_ => {
eprintln!(
"skipping: set CALLGRAPH_REAL_EXE=<path-to-DOS4GW-LE> to pin fixup post images"
);
return;
}
};
let pe = Pe::load(&exe).unwrap_or_else(|e| panic!("load {exe}: {e}"));
if pe.fixup_count() == 0 {
eprintln!("{exe}: no 07 fixup pairs; nothing to pin");
return;
}
let refs = pe.referenced_addrs();
let mut checked = 0u32;
let mut sample: Vec<(u64, u64)> = Vec::new();
for &(pre, post) in pe.fixup_pairs() {
if (pre as u32) == (post as u32) || !refs.contains(&(pre as u32)) {
continue;
}
assert!(
refs.contains(&(post as u32)),
"post-fixup image {post:#x} of referenced link-time {pre:#x} \
missing from referenced_addrs on {exe}"
);
if sample.len() < 8 {
sample.push((pre, post));
}
checked += 1;
if checked >= 512 {
break;
}
}
eprintln!("{exe}: verified {checked} pre->post references; sample {sample:x?}");
assert!(
checked > 0,
"{exe}: no in-image 07 pre value appeared in referenced_addrs"
);
}
#[test]
fn real_le_jump_table_entries_translate_to_post_images() {
let exe = match std::env::var("CALLGRAPH_REAL_EXE") {
Ok(p) if !p.trim().is_empty() => p,
_ => {
eprintln!(
"skipping: set CALLGRAPH_REAL_EXE=<path-to-DOS4GW-LE> to pin jump-table translation"
);
return;
}
};
let pe = Pe::load(&exe).unwrap_or_else(|e| panic!("load {exe}: {e}"));
if pe.fixup_count() == 0 {
eprintln!("{exe}: no 07 fixup pairs (not LE); jump-table translation is a no-op");
return;
}
let refs = pe.referenced_addrs();
let mut starts: Vec<(u64, u64)> = pe
.sections()
.iter()
.filter(|s| s.is_executable())
.map(|s| {
let start = pe.image_base + s.virtual_address as u64;
(start, start + s.virtual_size.max(s.raw_size) as u64)
})
.collect();
starts.sort();
let mut indirect_fns = 0u32;
let mut resolved_fns = 0u32;
let mut resolved_cases = 0u32;
let mut sample: Vec<(u64, usize, usize)> = Vec::new();
for (start, end) in starts {
let mut va = start;
while va < end {
if refs.contains(&(va as u32)) {
if let Ok(cg) = analyze(&pe, va) {
if cg.indirect_jumps > 0 {
indirect_fns += 1;
if let Ok(fp) = cfg_fingerprint(&pe, va) {
if fp.switch_cases >= 1 {
resolved_fns += 1;
resolved_cases += fp.switch_cases as u32;
if sample.len() < 8 {
sample.push((va, fp.switch_cases, cg.direct_calls.len()));
}
}
}
}
va = va.saturating_add(cg.max_end.saturating_sub(va).max(1));
continue;
}
}
va += 1;
}
}
eprintln!(
"{exe}: {indirect_fns} indirect-jump entries, {resolved_fns} resolved \
({resolved_cases} cases); sample {sample:x?}"
);
if indirect_fns == 0 {
eprintln!("{exe}: no indirect-jump entries; nothing to pin");
return;
}
assert!(
resolved_fns > 0,
"{exe}: {indirect_fns} indirect-jump entries but none resolved a switch table \
- pre-fixup table translation regressed"
);
}