use qcode::{address_index::AddressIndex, context::Context, value::BlockId};
use qcode_emulator::{EmulatorMemory, StandaloneEmulator};
use qcode_jit::Jit;
use qcode_vm::{FaultKind, PAGE_SIZE, VmMemory, perm};
use wazabin_qcode_sleigh::SleighLifter;
const CODE: u64 = 0x1000;
const DATA: u64 = 0x40000;
const WATCHED: &[&str] = &["RAX", "RBX", "RCX", "RDX"];
fn lifter() -> &'static SleighLifter<'static> {
static LIFTER: std::sync::OnceLock<SleighLifter<'static>> = std::sync::OnceLock::new();
LIFTER.get_or_init(|| SleighLifter::new(sleigh_precompile::x64::spec()))
}
fn lift(code: &[u8]) -> (Context<'static>, BlockId) {
let lifter = lifter();
let mut ctx = lifter.new_context();
let mut index = AddressIndex::analyze(&ctx);
let block = lifter
.decode_and_lift_indexed(&mut ctx, &mut index, CODE, code, None)
.expect("the instruction decodes and lifts");
(ctx, block)
}
fn seed(emu: &mut StandaloneEmulator<VmMemory>, ctx: &Context<'_>, block: BlockId, pointer: u64) {
emu.memory
.mmu
.write_unchecked(DATA, &[0x11; 64], perm::RW_INIT);
for (name, value) in [
("RAX", 0x1234_5678_9abc_def0u64),
("RBX", pointer),
("RCX", 0),
("RDX", 0),
] {
emu.set_varnode_by_name(ctx, name, value)
.expect("register exists");
}
emu.invalidate_block_cache();
emu.block = block;
emu.idx = 0;
}
fn machine(block: BlockId, ctx: &Context<'_>, pointer: u64) -> StandaloneEmulator<VmMemory> {
let mut emu = StandaloneEmulator::<VmMemory>::new_in(block);
emu.memory.configure_spaces(ctx);
emu.memory
.mmu
.map(DATA, 2 * PAGE_SIZE, perm::RW_INIT)
.expect("the data pages map");
seed(&mut emu, ctx, block, pointer);
emu
}
fn state(emu: &mut StandaloneEmulator<VmMemory>, ctx: &Context<'_>) -> (Vec<Option<u64>>, Vec<u8>) {
let registers = WATCHED
.iter()
.map(|name| emu.read_varnode_by_name(ctx, name))
.collect();
let mut page = vec![0u8; 64];
emu.memory
.mmu
.read(DATA, &mut page)
.expect("the data page is readable");
(registers, page)
}
fn agree(code: &[u8], pointer: u64) {
let (ctx, block) = lift(code);
let mut interpreted = machine(block, &ctx, pointer);
interpreted
.run_block(&ctx)
.expect("the interpreter runs the block");
let expected = state(&mut interpreted, &ctx);
let mut jit = Jit::new();
let mut jitted = machine(block, &ctx, pointer);
for pass in 0..2 {
seed(&mut jitted, &ctx, block, pointer);
let ran = jit
.run_block(&ctx, &mut jitted, block, 0, false)
.expect("running compiled code does not fault")
.is_some();
assert!(
ran,
"the block was declined; this test has nothing to check"
);
assert_eq!(
expected,
state(&mut jitted, &ctx),
"compiled code disagreed with the interpreter on pass {pass}"
);
}
assert!(
jit.stats.native_runs >= 2,
"both passes must have run as native code"
);
}
#[test]
fn ram_accesses_agree_with_the_interpreter() {
let programs: [(&str, &[u8]); 6] = [
("mov eax, [rbx]", &[0x8b, 0x03]),
("mov rax, [rbx]", &[0x48, 0x8b, 0x03]),
("mov [rbx], eax", &[0x89, 0x03]),
("mov [rbx], rax", &[0x48, 0x89, 0x03]),
("mov al, [rbx]", &[0x8a, 0x03]),
("mov [rbx], al", &[0x88, 0x03]),
];
for (name, code) in programs {
eprintln!("checking {name}");
agree(code, DATA + 8);
}
}
#[test]
fn an_unaligned_access_within_a_page_stays_inline() {
agree(&[0x48, 0x8b, 0x03], DATA + 3);
agree(&[0x48, 0x89, 0x03], DATA + 5);
}
#[test]
fn an_access_straddling_two_pages_still_agrees() {
agree(&[0x48, 0x8b, 0x03], DATA + PAGE_SIZE - 4);
agree(&[0x48, 0x89, 0x03], DATA + PAGE_SIZE - 4);
}
fn fault_from(code: &[u8], pointer: u64, prepare: impl Fn(&mut VmMemory)) -> Option<FaultKind> {
let (ctx, block) = lift(code);
let mut jit = Jit::new();
let mut emu = machine(block, &ctx, pointer);
prepare(&mut emu.memory);
let mut faulted = None;
for _ in 0..2 {
seed(&mut emu, &ctx, block, pointer);
prepare(&mut emu.memory);
if jit.run_block(&ctx, &mut emu, block, 0, false).is_err() {
faulted = emu.memory.take_fault().map(|fault| fault.kind);
}
}
faulted
}
#[test]
fn an_unmapped_access_faults_where_the_interpreter_would() {
assert_eq!(
fault_from(&[0x48, 0x8b, 0x03], 0x9000_0000, |_| {}),
Some(FaultKind::ReadUnmapped)
);
assert_eq!(
fault_from(&[0x48, 0x89, 0x03], 0x9000_0000, |_| {}),
Some(FaultKind::WriteUnmapped)
);
}
#[test]
fn a_permission_refusal_survives_a_warm_translation() {
let (ctx, block) = lift(&[0x48, 0x89, 0x03]); let mut jit = Jit::new();
let mut emu = machine(block, &ctx, DATA);
emu.memory
.mmu
.protect(DATA + PAGE_SIZE / 2, PAGE_SIZE / 2, perm::READ)
.expect("the second half is mapped");
seed(&mut emu, &ctx, block, DATA);
jit.run_block(&ctx, &mut emu, block, 0, false)
.expect("the writable half accepts the store");
assert!(
emu.memory.mmu.cache_translation(DATA),
"the page must be cacheable for this test to check anything"
);
seed(&mut emu, &ctx, block, DATA + PAGE_SIZE / 2);
let refused = jit.run_block(&ctx, &mut emu, block, 0, false).is_err();
assert!(refused, "a compiled store must not bypass permissions");
let fault = emu.memory.take_fault().expect("the fault was recorded");
assert_eq!(fault.kind, FaultKind::WritePerm);
assert_eq!(fault.addr, DATA + PAGE_SIZE / 2);
let mut out = [0u8; 8];
emu.memory.mmu.read(DATA + PAGE_SIZE / 2, &mut out).unwrap();
assert_eq!(out, [0; 8]);
}
#[test]
fn a_store_through_compiled_code_marks_its_bytes_initialized() {
let (ctx, block) = lift(&[0x48, 0x89, 0x03]); let mut jit = Jit::new();
let mut emu = machine(block, &ctx, DATA + 8);
emu.memory
.mmu
.protect(DATA, PAGE_SIZE, perm::READ | perm::WRITE)
.expect("the page is mapped");
for _ in 0..2 {
emu.invalidate_block_cache();
emu.block = block;
emu.idx = 0;
jit.run_block(&ctx, &mut emu, block, 0, false)
.expect("the store succeeds");
}
emu.memory.mmu.set_check_uninit(true);
let mut out = [0u8; 8];
emu.memory
.mmu
.read(DATA + 8, &mut out)
.expect("the bytes the guest just wrote are defined");
assert_eq!(u64::from_le_bytes(out), 0x1234_5678_9abc_def0);
}