use std::path::PathBuf;
use std::process::{Command, Stdio};
use assert2::assert;
use idakit::decompiler::ctree::Ctree;
use idakit::decompiler::ctree::query::{base_var, global_target};
use idakit::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq)]
struct VtableInstall {
this_offset: i64,
vtable: Address,
vtable_name: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ThisCall {
callee: Address,
callee_name: Option<String>,
this_offset: i64,
}
fn vtable_installs(tree: &Ctree) -> Vec<VtableInstall> {
let Some(this) = tree.this_lvar() else {
return Vec::new();
};
tree.assigns()
.filter_map(|(_, op, x, y)| {
if op != AssignmentOp::Assign {
return None;
}
let (v, off) = base_var(tree, x)?;
if v != this {
return None;
}
let g = global_target(tree, y)?;
Some(VtableInstall {
this_offset: off,
vtable: g.address,
vtable_name: g.name,
})
})
.collect()
}
fn this_arg_calls(tree: &Ctree) -> Vec<ThisCall> {
let Some(this) = tree.this_lvar() else {
return Vec::new();
};
tree.calls()
.filter_map(|(_, callee, args)| {
let g = global_target(tree, callee)?;
let (v, off) = base_var(tree, *args.first()?)?;
if v != this {
return None;
}
Some(ThisCall {
callee: g.address,
callee_name: g.name,
this_offset: off,
})
})
.collect()
}
fn gxx_available() -> bool {
Command::new("g++")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
#[test]
fn ctor() {
if !gxx_available() {
eprintln!("skipping: g++ not available to build the fixture");
return;
}
let src = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/vtbl.cpp");
let bin: PathBuf = std::env::temp_dir().join(format!("idakit_vtbl_{}", std::process::id()));
let status = Command::new("g++")
.args(["-O0", "-w", "-o"])
.arg(&bin)
.arg(src)
.status()
.expect("failed to spawn g++");
assert!(status.success(), "g++ failed to compile the fixture");
let bin_str = bin.to_string_lossy().into_owned();
Ida::run(move |ida| {
ida.call(move |idb| {
idb.open(&bin_str)
.run_auto(true)
.call()
.expect("open + auto-analysis failed");
let eas: Vec<_> = idb.functions().map(|f| (f.address(), f.name())).collect();
let mut analyzed = Vec::new();
for (address, name) in eas {
let Ok(tree) = idb.ctree(address) else {
continue;
};
let installs = vtable_installs(&tree);
if installs.is_empty() {
continue;
}
let calls = this_arg_calls(&tree);
analyzed.push((String::from(name), installs, calls));
}
let mi_ctor = analyzed
.iter()
.find(|(_, installs, _)| {
installs.len() >= 2
&& installs.iter().any(|i| i.this_offset == 0)
&& installs.iter().any(|i| i.this_offset > 0)
})
.unwrap_or_else(|| {
panic!("no multiple-inheritance constructor found; analyzed: {analyzed:#?}")
});
let (name, installs, calls) = mi_ctor;
let sub_off = installs
.iter()
.map(|i| i.this_offset)
.find(|&o| o > 0)
.expect("a nonzero subobject install offset");
assert!(
calls.iter().any(|c| c.this_offset == 0),
"expected a base ctor call at this+0; calls: {calls:#?}"
);
assert!(
calls.iter().any(|c| c.this_offset == sub_off),
"expected a subobject ctor call at this+{sub_off}; calls: {calls:#?}"
);
idb.close(false);
println!(
"ctor fixture OK: `{name}` installs 2 vtables (offsets 0, {sub_off}) and \
calls both base ctors with matching this-relative args"
);
})
.unwrap_or_else(|e| e.resume());
})
.expect("kernel init failed");
let _ = std::fs::remove_file(&bin);
let _ = std::fs::remove_file(bin.with_extension("i64"));
}