use std::cell::RefCell;
use std::rc::Rc;
use qcode::{context::Context, value::BlockId};
use qcode_emulator::{EmulatorErrorKind, StandaloneEmulator};
use qcode_jit::Jit;
use qcode_vm::{
AddressHook, BlockEntryHook, CompareHook, InterruptKind, Vm, VmExit, VmMemory, WriteWatch, perm,
};
use qcode_vm::{BlockExecutor, Executed};
use wazabin_qcode_sleigh::vm_source::SleighCodeSource;
const ENTRY: u64 = 0x1000;
struct SharedJit(Rc<RefCell<Jit>>);
impl BlockExecutor for SharedJit {
fn run_block(
&mut self,
ctx: &Context<'_>,
emu: &mut StandaloneEmulator<VmMemory>,
block: BlockId,
start: usize,
chain: bool,
) -> Result<Option<Executed>, EmulatorErrorKind> {
self.0.borrow_mut().run_block(ctx, emu, block, start, chain)
}
}
fn machine(code: &[u8], jit: bool) -> Vm<SleighCodeSource<'static>> {
let source = SleighCodeSource::new(sleigh_precompile::x64::spec());
let ctx = source.new_context();
let mut memory = VmMemory::new();
memory
.mmu
.write_unchecked(ENTRY, code, perm::READ | perm::EXEC);
memory.mmu.map(0x20000, 0x2000, perm::RW_INIT).unwrap();
let mut vm = Vm::at_address(ctx, ENTRY, source, memory).expect("the entry decodes");
if jit {
vm.set_block_executor(Box::new(Jit::new()));
}
vm
}
const LOOP: &[u8] = &[
0xb9, 0x05, 0x00, 0x00, 0x00, 0xff, 0xc9, 0x75, 0xfc, 0xb8, 0x2a, 0x00, 0x00, 0x00, ];
const ARITH: &[u8] = &[
0xb8, 0x39, 0x05, 0x00, 0x00, 0xbb, 0x07, 0x00, 0x00, 0x00, 0x01, 0xd8, 0x29, 0xd8, 0x31, 0xd8, ];
fn drive(
vm: &mut Vm<SleighCodeSource<'static>>,
regs: &[&str],
mut on_hook: impl FnMut(&mut Vm<SleighCodeSource<'static>>, u64, u64),
) -> Vec<Option<u64>> {
drive_with(vm, regs, |vm, code, args| {
let addr = args[0].expect("the hook passes the address");
assert_eq!(
vm.pending_interrupt().and_then(|i| i.pc),
Some(addr),
"the stop is at the hooked address"
);
on_hook(vm, code, addr);
})
}
fn drive_with(
vm: &mut Vm<SleighCodeSource<'static>>,
regs: &[&str],
mut on_hook: impl FnMut(&mut Vm<SleighCodeSource<'static>>, u64, &[Option<u64>]),
) -> Vec<Option<u64>> {
for _ in 0..10_000 {
match vm.run(1_000_000) {
VmExit::Interrupt(interrupt) => {
let InterruptKind::Explicit { code } = interrupt.kind else {
panic!("unexpected intrinsic {:?}", interrupt.kind);
};
on_hook(vm, code, &interrupt.args);
vm.resume(None).unwrap();
}
_ => break,
}
}
let ctx = vm.context().clone();
regs.iter()
.map(|name| vm.emulator().read_varnode_by_name(&ctx, name))
.collect()
}
#[test]
fn a_block_entry_hook_fires_the_same_way_on_both_strategies() {
let mut traces = Vec::new();
for jit in [false, true] {
let mut vm = machine(LOOP, jit);
vm.add_hook(BlockEntryHook {
begin: 1,
end: 0,
code: 7,
});
let mut trace = Vec::new();
let regs = drive(&mut vm, &["ECX", "EAX"], |vm, code, addr| {
assert_eq!(code, 7);
assert_eq!(vm.pending_interrupt().and_then(|i| i.pc), Some(addr));
trace.push(addr);
});
assert_eq!(regs, vec![Some(0), Some(42)], "jit={jit}");
assert_eq!(trace.first(), Some(&0x1000), "jit={jit}");
assert_eq!(trace.last(), Some(&0x1009), "jit={jit}");
assert!(
trace.iter().filter(|&&addr| addr == 0x1005).count() >= 4,
"jit={jit}: the loop body is entered once per iteration: {trace:x?}"
);
if jit {
assert!(
vm.stats.native_bodies > 0,
"hooked blocks still run natively"
);
}
traces.push(trace);
}
assert_eq!(traces[0], traces[1]);
}
#[test]
fn an_address_hook_in_the_middle_of_a_block_keeps_both_halves_native() {
let mut results = Vec::new();
for jit in [false, true] {
let mut vm = machine(ARITH, jit);
vm.add_hook(AddressHook::new([0x100a], 3));
let mut fired = 0;
let mut native_at_hook = 0;
let mut idx_at_hook = 0;
let regs = drive(&mut vm, &["EAX", "EBX"], |vm, code, addr| {
assert_eq!((code, addr), (3, 0x100a));
fired += 1;
native_at_hook = vm.stats.native_bodies;
idx_at_hook = vm.emulator().idx;
});
assert_eq!(fired, 1, "jit={jit}");
assert_eq!(regs, vec![Some(1337 ^ 7), Some(7)], "jit={jit}");
if jit {
assert!(
idx_at_hook > 0,
"the two `mov`s before the hook are in the same block"
);
assert!(native_at_hook > 0, "the prefix ran natively");
assert!(
vm.stats.native_bodies > native_at_hook,
"the rest of the block ran natively after the hook"
);
}
results.push(regs);
}
assert_eq!(results[0], results[1]);
}
#[test]
fn a_hook_registered_later_reaches_code_already_lifted() {
for jit in [false, true] {
let mut vm = machine(LOOP, jit);
vm.add_breakpoint(0x1005);
let exit = vm.run(1_000_000);
assert!(
matches!(exit, VmExit::Breakpoint(0x1005)),
"jit={jit}: {exit:?}"
);
vm.remove_breakpoint(0x1005);
vm.add_hook(AddressHook::new([0x1005], 9));
let mut fired = 0;
let regs = drive(&mut vm, &["ECX"], |_, code, addr| {
assert_eq!((code, addr), (9, 0x1005));
fired += 1;
});
assert_eq!(regs, vec![Some(0)], "jit={jit}");
assert!(fired >= 1, "jit={jit}: the hook was reached");
}
}
#[test]
fn state_written_at_a_hook_is_what_the_code_after_it_sees() {
for jit in [false, true] {
let mut vm = machine(LOOP, jit);
vm.add_hook(AddressHook::new([0x1005], 1));
let mut fired = 0;
let regs = drive(&mut vm, &["ECX", "EAX"], |vm, _, _| {
fired += 1;
if fired == 1 {
let ctx = vm.context().clone();
vm.emulator().set_varnode_by_name(&ctx, "ECX", 2).unwrap();
}
});
assert_eq!(fired, 2, "jit={jit}: dec runs twice after the write");
assert_eq!(regs, vec![Some(0), Some(42)], "jit={jit}");
}
}
const WRITES: &[u8] = &[
0xb8, 0x44, 0x33, 0x22, 0x11, 0x89, 0x04, 0x25, 0x00, 0x00, 0x02, 0x00, 0x89, 0x04, 0x25, 0x00, 0x10, 0x02, 0x00, 0xc6, 0x04, 0x25, 0x10, 0x00, 0x02, 0x00,
0x7f, 0xb9, 0x32, 0x00, 0x00, 0x00, 0x89, 0x0c, 0x25, 0x08, 0x10, 0x02, 0x00, 0xff, 0xc9, 0x75, 0xf5, ];
#[test]
fn a_write_watch_only_leaves_the_vm_for_writes_in_its_range() {
for jit in [false, true] {
let mut vm = machine(WRITES, false);
let shared = Rc::new(RefCell::new(Jit::new()));
if jit {
vm.set_block_executor(Box::new(SharedJit(shared.clone())));
}
vm.add_hook(WriteWatch {
begin: 0x20000,
end: 0x20fff,
code: 5,
});
let mut writes = Vec::new();
let regs = drive_with(&mut vm, &["ECX"], |vm, code, args| {
assert_eq!(code, 5);
let pc = vm.pending_interrupt().and_then(|i| i.pc);
writes.push((pc, args.to_vec()));
});
assert_eq!(regs, vec![Some(0)], "jit={jit}: the loop ran to completion");
assert_eq!(
writes,
vec![
(
Some(0x1005),
vec![Some(0x20000), Some(4), Some(0x1122_3344)]
),
(Some(0x1013), vec![Some(0x20010), Some(1), Some(0x7f)]),
],
"jit={jit}"
);
let mut word = [0u8; 4];
vm.memory().mmu.read(0x20000, &mut word).unwrap();
assert_eq!(u32::from_le_bytes(word), 0x1122_3344);
let mut byte = [0u8; 1];
vm.memory().mmu.read(0x20010, &mut byte).unwrap();
assert_eq!(byte[0], 0x7f);
if jit {
let stats = shared.borrow().stats.clone();
assert!(
stats.native_runs >= 100,
"jit: expected the hooked loop to run natively, got {stats:?}"
);
let ctx = vm.context().clone();
let mut jit = shared.borrow_mut();
for func in ctx.function_ids() {
for block in qcode::value::FunctionBody::from_id(&ctx, func).block_ids() {
if let Err(reason) = jit.try_compile(&ctx, block) {
assert!(
ctx.block(block).instruction_ids().is_empty(),
"jit: block {block:?} declined: {reason}"
);
}
}
}
}
}
}
const COMPARE: &[u8] = &[
0xb8, 0x39, 0x05, 0x00, 0x00, 0xbb, 0x07, 0x00, 0x00, 0x00, 0x39, 0xd8, ];
#[test]
fn a_compare_hook_receives_the_operands_the_guest_compared() {
let mut results = Vec::new();
for jit in [false, true] {
let mut vm = machine(COMPARE, jit);
vm.add_hook(CompareHook { code: 11 });
let mut seen = Vec::new();
drive_with(&mut vm, &[], |_, code, args| {
assert_eq!(code, 11);
seen.push(args.to_vec());
});
assert!(
seen.contains(&vec![Some(1337), Some(7)]),
"jit={jit}: no comparison of eax against ebx was reported: {seen:?}"
);
results.push(seen);
}
assert_eq!(results[0], results[1]);
}