use std::collections::BTreeMap;
use crate::emulator::{Cpu, Mmu};
pub mod advapi32;
pub mod comctl32;
pub mod gdi32;
pub mod kernel32;
pub mod mfplat;
pub mod msvcrt;
pub mod ole32;
pub mod shell32;
pub mod shlwapi;
pub mod user32;
pub mod version;
pub mod vfw32;
pub mod winmm;
pub const THUNK_BASE: u32 = 0xFFFE_0000;
const THUNK_STRIDE: u32 = 16;
pub type StubFn = fn(&mut Cpu, &mut Mmu, &mut HostState, &Registry) -> Result<u32, Win32Error>;
#[derive(Clone, Debug)]
pub struct StubCall {
pub dll: String,
pub name: String,
pub args: Vec<u32>,
pub ret: u32,
pub call_site_eip: u32,
}
#[derive(Clone)]
pub struct StubEntry {
pub dll: String,
pub name: String,
pub func: StubFn,
pub arg_dwords: u32,
pub thunk_addr: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Win32Error {
UnknownImport { dll: String, name: String },
InvalidArgument { stub: &'static str, reason: String },
InvalidHeapBlock { stub: &'static str, addr: u32 },
BudgetExhausted { executed: u64 },
}
impl core::fmt::Display for Win32Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Win32Error::UnknownImport { dll, name } => {
write!(f, "no Round-1 stub for import {dll}!{name}")
}
Win32Error::InvalidArgument { stub, reason } => {
write!(f, "{stub}: {reason}")
}
Win32Error::InvalidHeapBlock { stub, addr } => {
write!(f, "{stub}: unknown heap allocation {addr:#010x}")
}
Win32Error::BudgetExhausted { executed } => {
write!(
f,
"instruction budget exhausted after {executed} steps without reaching RET_SENTINEL"
)
}
}
}
}
#[derive(Debug, Clone)]
pub struct HicEntry {
pub fcc_type: u32,
pub fcc_handler: u32,
pub mode: u32,
pub driver_proc_va: u32,
pub driver_id: u32,
}
#[derive(Default)]
pub struct HostState {
pub heap: BTreeMap<u32, Vec<u8>>,
pub heap_cursor: u32,
pub heap_arena_end: u32,
pub process_heap_handle: u32,
pub last_error: u32,
pub tick: u32,
pub modules: BTreeMap<String, u32>,
pub primary_module_base: u32,
pub debug_log: Vec<String>,
pub message_box_log: Vec<String>,
pub hics: BTreeMap<u32, HicEntry>,
pub next_hic: u32,
pub default_driver_proc: u32,
pub exit_requested: Option<u32>,
pub instruction_budget: Option<u64>,
pub instructions_executed: u64,
pub context: crate::context::Context,
pub const_arena_cursor: u32,
pub const_arena_end: u32,
pub command_line_ptr: u32,
pub environment_strings_ptr: u32,
pub gdi_hdcs: Option<std::collections::BTreeSet<u32>>,
pub hwnd_registry: std::collections::BTreeSet<u32>,
pub next_hwnd_index: u32,
pub trace_stubs: bool,
pub stub_trace: Vec<String>,
pub stub_calls: Vec<StubCall>,
pub loaded_drivers: std::collections::BTreeSet<u32>,
pub module_resource_dirs: BTreeMap<u32, u32>,
pub com: crate::com::ComObjectTable,
pub rand_state: u32,
}
impl HostState {
pub fn new(heap_start: u32, heap_end: u32) -> Self {
HostState {
heap_cursor: heap_start,
heap_arena_end: heap_end,
process_heap_handle: 0xDEAD_BEEF,
last_error: 0,
tick: 0,
heap: BTreeMap::new(),
modules: BTreeMap::new(),
primary_module_base: 0,
debug_log: Vec::new(),
message_box_log: Vec::new(),
hics: BTreeMap::new(),
next_hic: 1,
default_driver_proc: 0,
exit_requested: None,
instruction_budget: None,
instructions_executed: 0,
context: crate::context::Context::default(),
const_arena_cursor: 0,
const_arena_end: 0,
command_line_ptr: 0,
environment_strings_ptr: 0,
gdi_hdcs: None,
hwnd_registry: std::collections::BTreeSet::new(),
next_hwnd_index: 0,
trace_stubs: false,
stub_trace: Vec::new(),
stub_calls: Vec::new(),
loaded_drivers: std::collections::BTreeSet::new(),
module_resource_dirs: BTreeMap::new(),
com: crate::com::ComObjectTable::new(),
rand_state: 1,
}
}
pub fn with_const_arena(mut self, start: u32, end: u32) -> Self {
self.const_arena_cursor = start;
self.const_arena_end = end;
self
}
pub fn arena_const_alloc(&mut self, n: u32) -> Result<u32, Win32Error> {
let aligned = n
.checked_add(15)
.map(|v| v & !15u32)
.ok_or(Win32Error::InvalidArgument {
stub: "arena_const_alloc",
reason: "size overflow".into(),
})?;
let addr = self.const_arena_cursor;
let next = addr
.checked_add(aligned)
.ok_or(Win32Error::InvalidArgument {
stub: "arena_const_alloc",
reason: "const arena address-space overflow".into(),
})?;
if next > self.const_arena_end {
return Err(Win32Error::InvalidArgument {
stub: "arena_const_alloc",
reason: format!(
"const arena exhausted (need {n}, have {})",
self.const_arena_end - addr
),
});
}
self.const_arena_cursor = next;
Ok(addr)
}
pub fn arena_alloc(&mut self, n: u32) -> Result<u32, Win32Error> {
let aligned = n
.checked_add(15)
.map(|v| v & !15u32)
.ok_or(Win32Error::InvalidArgument {
stub: "arena_alloc",
reason: "size overflow".into(),
})?;
let addr = self.heap_cursor;
let next = addr
.checked_add(aligned)
.ok_or(Win32Error::InvalidArgument {
stub: "arena_alloc",
reason: "heap address-space overflow".into(),
})?;
if next > self.heap_arena_end {
return Err(Win32Error::InvalidArgument {
stub: "arena_alloc",
reason: format!(
"arena exhausted (need {n}, have {})",
self.heap_arena_end - addr
),
});
}
self.heap_cursor = next;
self.heap.insert(addr, vec![0u8; n as usize]);
Ok(addr)
}
}
#[derive(Default)]
pub struct Registry {
by_thunk: BTreeMap<u32, StubEntry>,
by_name: BTreeMap<(String, String), u32>,
next_slot: u32,
data_imports: BTreeMap<(String, String), DataImport>,
next_data_slot: u32,
}
#[derive(Clone, Copy, Debug)]
pub struct DataImport {
pub addr: u32,
pub initial: u32,
}
pub const DATA_IMPORT_BASE: u32 = 0x7010_0000;
const DATA_IMPORT_SIZE: u32 = 0x0000_1000;
const DATA_IMPORT_END: u32 = DATA_IMPORT_BASE + DATA_IMPORT_SIZE;
impl Registry {
pub fn new() -> Self {
Registry {
by_thunk: BTreeMap::new(),
by_name: BTreeMap::new(),
next_slot: 0,
data_imports: BTreeMap::new(),
next_data_slot: DATA_IMPORT_BASE,
}
}
pub fn register_data(&mut self, dll: &str, name: &str, initial: u32) -> u32 {
let key = (dll.to_ascii_lowercase(), name.to_string());
if let Some(d) = self.data_imports.get(&key) {
return d.addr;
}
let addr = self.next_data_slot;
let next = addr.saturating_add(4);
if next > DATA_IMPORT_END {
return 0;
}
self.next_data_slot = next;
self.data_imports.insert(key, DataImport { addr, initial });
self.by_name
.insert((dll.to_ascii_lowercase(), name.to_string()), addr);
addr
}
pub fn data_imports(&self) -> impl Iterator<Item = (&String, &String, &DataImport)> {
self.data_imports
.iter()
.map(|((dll, name), d)| (dll, name, d))
}
pub fn register(&mut self, dll: &str, name: &str, func: StubFn, arg_dwords: u32) -> u32 {
let key = (dll.to_ascii_lowercase(), name.to_string());
if let Some(addr) = self.by_name.get(&key) {
return *addr;
}
let thunk_addr = THUNK_BASE.wrapping_add(self.next_slot.wrapping_mul(THUNK_STRIDE));
self.next_slot += 1;
self.by_name.insert(key.clone(), thunk_addr);
self.by_thunk.insert(
thunk_addr,
StubEntry {
dll: key.0,
name: key.1,
func,
arg_dwords,
thunk_addr,
},
);
thunk_addr
}
pub fn resolve(&self, dll: &str, name: &str) -> Option<u32> {
let key = (dll.to_ascii_lowercase(), name.to_string());
self.by_name.get(&key).copied()
}
pub fn is_thunk(&self, addr: u32) -> bool {
self.by_thunk.contains_key(&addr)
}
pub fn entry(&self, addr: u32) -> Option<&StubEntry> {
self.by_thunk.get(&addr)
}
pub fn register_kernel32(&mut self) -> usize {
let before = self.by_name.len();
kernel32::register(self);
self.by_name.len() - before
}
pub fn register_gdi32(&mut self) -> usize {
let before = self.by_name.len();
gdi32::register(self);
self.by_name.len() - before
}
pub fn register_user32(&mut self) -> usize {
let before = self.by_name.len();
user32::register(self);
self.by_name.len() - before
}
pub fn register_winmm(&mut self) -> usize {
let before = self.by_name.len();
winmm::register(self);
self.by_name.len() - before
}
pub fn register_advapi32(&mut self) -> usize {
let before = self.by_name.len();
advapi32::register(self);
self.by_name.len() - before
}
pub fn register_ole32(&mut self) -> usize {
let before = self.by_name.len();
ole32::register(self);
self.by_name.len() - before
}
pub fn register_msvcrt(&mut self) -> usize {
let before = self.by_name.len();
msvcrt::register(self);
self.by_name.len() - before
}
pub fn register_msvcr71(&mut self) -> usize {
let before = self.by_name.len();
msvcrt::register_alias(self, "msvcr71.dll");
self.by_name.len() - before
}
pub fn register_pncrt(&mut self) -> usize {
let before = self.by_name.len();
msvcrt::register_alias(self, "pncrt.dll");
self.by_name.len() - before
}
pub fn register_mfplat(&mut self) -> usize {
let before = self.by_name.len();
mfplat::register(self);
self.by_name.len() - before
}
pub fn register_shell_support(&mut self) -> usize {
let before = self.by_name.len();
version::register(self);
comctl32::register(self);
shell32::register(self);
shlwapi::register(self);
self.by_name.len() - before
}
pub fn register_all(&mut self) -> usize {
let host_before = self.by_name.len();
crate::com::host_iface::register(self);
crate::com::host_iface_r31::register(self);
let host_count = self.by_name.len() - host_before;
self.register_kernel32()
+ self.register_gdi32()
+ self.register_user32()
+ self.register_winmm()
+ self.register_advapi32()
+ self.register_ole32()
+ self.register_msvcrt()
+ self.register_msvcr71()
+ self.register_pncrt()
+ self.register_mfplat()
+ self.register_shell_support()
+ host_count
}
}
pub fn arg_dword(cpu: &Cpu, mmu: &Mmu, n: u32) -> Result<u32, crate::emulator::Trap> {
let addr = cpu.regs.esp().wrapping_add(4u32 * (n + 1));
mmu.load32(addr)
}
pub fn cdecl_trace_arg_count(dll: &str, name: &str) -> Option<u32> {
match (dll, name) {
("msvcrt.dll", "malloc")
| ("msvcrt.dll", "free")
| ("msvcrt.dll", "??2@YAPAXI@Z")
| ("msvcrt.dll", "??3@YAXPAX@Z") => Some(1),
("msvcrt.dll", "calloc") | ("msvcrt.dll", "realloc") => Some(2),
_ => None,
}
}
pub fn trap_to_win32_local(stub: &'static str, t: crate::emulator::Trap) -> Win32Error {
Win32Error::InvalidArgument {
stub,
reason: format!("{t}"),
}
}
pub fn read_cstr_local(mmu: &Mmu, mut addr: u32, max: u32) -> Result<String, Win32Error> {
let mut bytes = Vec::new();
for _ in 0..max {
let b = mmu
.load8(addr)
.map_err(|t| trap_to_win32_local("read_cstr", t))?;
if b == 0 {
break;
}
bytes.push(b);
addr = addr.wrapping_add(1);
}
Ok(String::from_utf8_lossy(&bytes).into_owned())
}
pub fn dispatch_stub(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
) -> Result<(), crate::Error> {
let addr = cpu.regs.eip;
let entry = registry
.entry(addr)
.ok_or_else(|| Win32Error::UnknownImport {
dll: "<thunk>".into(),
name: format!("@{:#010x}", addr),
})?
.clone();
let capture_args = state.trace_stubs;
#[cfg(feature = "trace")]
let capture_args = capture_args || mmu.trace.has_sink();
let snapshot: Option<(u32, Vec<u32>)> = if capture_args {
let call_site_eip = mmu.load32(cpu.regs.esp()).unwrap_or(0);
let n_args = cdecl_trace_arg_count(&entry.dll, &entry.name).unwrap_or(entry.arg_dwords);
let mut args = Vec::with_capacity(n_args as usize);
for i in 0..n_args {
let a = arg_dword(cpu, mmu, i).unwrap_or(0);
args.push(a);
}
Some((call_site_eip, args))
} else {
None
};
let ret = (entry.func)(cpu, mmu, state, registry)?;
if state.trace_stubs {
let (call_site_eip, args) = snapshot.clone().unwrap_or((0, Vec::new()));
let args_str = args
.iter()
.map(|a| format!("{a:#010x}"))
.collect::<Vec<_>>()
.join(", ");
state.stub_trace.push(format!(
"{}!{}({args_str}) → {:#010x}",
entry.dll, entry.name, ret
));
state.stub_calls.push(StubCall {
dll: entry.dll.clone(),
name: entry.name.clone(),
args,
ret,
call_site_eip,
});
}
#[cfg(feature = "trace")]
if let Some((call_site_eip, args)) = snapshot {
mmu.trace
.ev_win32_call(&entry.dll, &entry.name, &args, ret, call_site_eip);
}
let ret_addr = cpu.pop32(mmu)?;
cpu.regs.set32(crate::emulator::regs::Reg32::Eax, ret);
let new_esp = cpu
.regs
.esp()
.wrapping_add(entry.arg_dwords.wrapping_mul(4));
cpu.regs.set_esp(new_esp);
cpu.regs.eip = ret_addr;
Ok(())
}
pub fn run_until_sentinel(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
) -> Result<(), crate::Error> {
use crate::emulator::isa_int::{StepOk, RET_SENTINEL};
state.instructions_executed = 0;
loop {
if state.exit_requested.is_some() {
cpu.regs.eip = RET_SENTINEL;
return Ok(());
}
if cpu.regs.eip == RET_SENTINEL {
return Ok(());
}
if let Some(remaining) = state.instruction_budget.as_mut() {
if *remaining == 0 {
return Err(crate::Error::Win32(Win32Error::BudgetExhausted {
executed: state.instructions_executed,
}));
}
*remaining -= 1;
}
state.instructions_executed = state.instructions_executed.saturating_add(1);
if registry.is_thunk(cpu.regs.eip) {
match dispatch_stub(cpu, mmu, registry, state) {
Ok(()) => continue,
Err(e) => {
#[cfg(feature = "trace")]
emit_trap_event(cpu, mmu, &e);
return Err(e);
}
}
}
match cpu.step(mmu) {
Ok(StepOk::Continued) => continue,
Ok(StepOk::Halted) => return Ok(()),
Err(t) => {
let e: crate::Error = t.into();
#[cfg(feature = "trace")]
emit_trap_event(cpu, mmu, &e);
return Err(e);
}
}
}
}
#[cfg(feature = "trace")]
fn emit_trap_event(cpu: &Cpu, mmu: &Mmu, err: &crate::Error) {
use crate::emulator::regs::Reg32;
let (label, eip, opcode) = match err {
crate::Error::Trap(t) => match t {
crate::emulator::Trap::MemoryFault { addr } => ("MemoryFault", *addr, None::<u32>),
crate::emulator::Trap::ReadProtectFault { addr } => ("ReadProtectFault", *addr, None),
crate::emulator::Trap::WriteProtectFault { addr } => ("WriteProtectFault", *addr, None),
crate::emulator::Trap::ExecuteProtectFault { addr } => {
("ExecuteProtectFault", *addr, None)
}
crate::emulator::Trap::UndefinedOpcode { eip, opcode } => {
("UndefinedOpcode", *eip, Some(*opcode))
}
crate::emulator::Trap::PrivilegedOpcode { eip, .. } => ("PrivilegedOpcode", *eip, None),
crate::emulator::Trap::DivideByZero { eip } => ("DivideByZero", *eip, None),
crate::emulator::Trap::UnresolvedImport { .. } => {
("UnresolvedImport", cpu.regs.eip, None)
}
crate::emulator::Trap::InstructionLimitExceeded { eip, .. } => {
("InstructionLimitExceeded", *eip, None)
}
crate::emulator::Trap::UnimplementedMmx { eip, opcode, .. } => {
("UnimplementedMmx", *eip, Some(*opcode))
}
},
crate::Error::PeLoader(_) => ("PeLoader", cpu.regs.eip, None),
crate::Error::Win32(_) => ("Win32", cpu.regs.eip, None),
crate::Error::NotImplemented => ("NotImplemented", cpu.regs.eip, None),
};
let regs = [
("eax", cpu.regs.get32(Reg32::Eax)),
("ecx", cpu.regs.get32(Reg32::Ecx)),
("edx", cpu.regs.get32(Reg32::Edx)),
("ebx", cpu.regs.get32(Reg32::Ebx)),
("esp", cpu.regs.esp()),
("ebp", cpu.regs.get32(Reg32::Ebp)),
("esi", cpu.regs.get32(Reg32::Esi)),
("edi", cpu.regs.get32(Reg32::Edi)),
];
mmu.trace.ev_trap(label, eip, opcode, ®s);
}
pub fn call_guest(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
target_va: u32,
args: &[u32],
) -> Result<u32, crate::Error> {
use crate::emulator::isa_int::RET_SENTINEL;
use crate::emulator::regs::Reg32;
for a in args.iter().rev() {
cpu.push32(mmu, *a)?;
}
cpu.push32(mmu, RET_SENTINEL)?;
cpu.regs.eip = target_va;
run_until_sentinel(cpu, mmu, registry, state)?;
Ok(cpu.regs.get32(Reg32::Eax))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::emulator::{mmu::Perm, Mmu};
fn dummy_stub(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_h: &mut HostState,
_r: &Registry,
) -> Result<u32, Win32Error> {
Ok(0xCAFE)
}
#[test]
fn registry_assigns_stable_thunk_addresses() {
let mut r = Registry::new();
let a = r.register("kernel32.dll", "Foo", dummy_stub, 1);
let b = r.register("kernel32.dll", "Bar", dummy_stub, 0);
let a2 = r.register("kernel32.dll", "Foo", dummy_stub, 1);
assert_eq!(a, a2);
assert_ne!(a, b);
assert!(r.is_thunk(a));
}
#[test]
fn registry_resolve_is_case_insensitive_on_dll_name() {
let mut r = Registry::new();
let addr = r.register("KERNEL32.DLL", "GetProcessHeap", dummy_stub, 0);
assert_eq!(r.resolve("kernel32.dll", "GetProcessHeap"), Some(addr));
assert_eq!(r.resolve("Kernel32.Dll", "GetProcessHeap"), Some(addr));
}
#[test]
fn cdecl_trace_arg_count_covers_msvcrt_heap_surface() {
assert_eq!(cdecl_trace_arg_count("msvcrt.dll", "malloc"), Some(1));
assert_eq!(cdecl_trace_arg_count("msvcrt.dll", "free"), Some(1));
assert_eq!(
cdecl_trace_arg_count("msvcrt.dll", "??2@YAPAXI@Z"),
Some(1),
"operator new",
);
assert_eq!(
cdecl_trace_arg_count("msvcrt.dll", "??3@YAXPAX@Z"),
Some(1),
"operator delete",
);
assert_eq!(cdecl_trace_arg_count("msvcrt.dll", "calloc"), Some(2));
assert_eq!(cdecl_trace_arg_count("msvcrt.dll", "realloc"), Some(2));
}
#[test]
fn cdecl_trace_arg_count_returns_none_for_unknown_calls() {
assert_eq!(
cdecl_trace_arg_count("kernel32.dll", "GetProcessHeap"),
None
);
assert_eq!(cdecl_trace_arg_count("msvcrt.dll", "memcpy"), None);
assert_eq!(
cdecl_trace_arg_count("MSVCRT.DLL", "malloc"),
None,
"match is exact-case on dll string per registry contract"
);
}
#[cfg(feature = "trace")]
#[test]
fn dispatch_emits_size_arg_for_msvcrt_malloc() {
use std::sync::{Arc, Mutex};
struct CapSink(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for CapSink {
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(b);
Ok(b.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let buf = Arc::new(Mutex::new(Vec::new()));
let mut mmu = Mmu::new();
mmu.map(0x4000, 0x4000, Perm::R | Perm::W);
mmu.trace.set_sink(Box::new(CapSink(Arc::clone(&buf))));
let mut cpu = Cpu::new();
cpu.regs.set_esp(0x7000);
let mut registry = Registry::new();
fn dummy_malloc_stub(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_h: &mut HostState,
_r: &Registry,
) -> Result<u32, Win32Error> {
Ok(0x6000_0000)
}
let addr = registry.register("msvcrt.dll", "malloc", dummy_malloc_stub, 0);
cpu.push32(&mut mmu, 2928).unwrap(); cpu.push32(&mut mmu, 0x1c218058).unwrap();
cpu.regs.eip = addr;
let mut state = HostState::new(0, 0);
dispatch_stub(&mut cpu, &mut mmu, ®istry, &mut state).unwrap();
let s = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
assert!(s.contains(r#""kind":"win32_call""#), "line: {s}");
assert!(s.contains(r#""dll":"msvcrt.dll""#), "line: {s}");
assert!(s.contains(r#""name":"malloc""#), "line: {s}");
assert!(
s.contains(r#""args":[2928]"#),
"expected args:[2928] (== 0xb70), got: {s}",
);
assert!(s.contains(r#""ret":"0x60000000""#), "line: {s}");
assert!(s.contains(r#""eip":"0x1c218058""#), "line: {s}");
}
#[cfg(feature = "trace")]
#[test]
fn dispatch_emits_pointer_arg_for_msvcrt_operator_delete() {
use std::sync::{Arc, Mutex};
struct CapSink(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for CapSink {
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(b);
Ok(b.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let buf = Arc::new(Mutex::new(Vec::new()));
let mut mmu = Mmu::new();
mmu.map(0x4000, 0x4000, Perm::R | Perm::W);
mmu.trace.set_sink(Box::new(CapSink(Arc::clone(&buf))));
let mut cpu = Cpu::new();
cpu.regs.set_esp(0x7000);
let mut registry = Registry::new();
fn dummy_delete_stub(
_cpu: &mut Cpu,
_mmu: &mut Mmu,
_h: &mut HostState,
_r: &Registry,
) -> Result<u32, Win32Error> {
Ok(0)
}
let addr = registry.register("msvcrt.dll", "??3@YAXPAX@Z", dummy_delete_stub, 0);
cpu.push32(&mut mmu, 0x6000_02c0).unwrap(); cpu.push32(&mut mmu, 0x1c237e58).unwrap(); cpu.regs.eip = addr;
let mut state = HostState::new(0, 0);
dispatch_stub(&mut cpu, &mut mmu, ®istry, &mut state).unwrap();
let s = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
assert!(
s.contains(r#""args":[1610613440]"#),
"expected args:[1610613440] (== 0x600002c0), got: {s}",
);
assert!(s.contains(r#""name":"??3@YAXPAX@Z""#), "line: {s}");
}
#[test]
fn dispatch_pops_return_addr_and_args() {
let mut mmu = Mmu::new();
mmu.map(0x4000, 0x4000, Perm::R | Perm::W);
let mut cpu = Cpu::new();
cpu.regs.set_esp(0x7000);
let mut registry = Registry::new();
let addr = registry.register("kernel32.dll", "Sample", dummy_stub, 2);
cpu.push32(&mut mmu, 0x4444).unwrap(); cpu.push32(&mut mmu, 0x3333).unwrap(); cpu.push32(&mut mmu, 0x2222).unwrap(); let esp_before = cpu.regs.esp();
cpu.regs.eip = addr;
let mut state = HostState::new(0, 0);
dispatch_stub(&mut cpu, &mut mmu, ®istry, &mut state).unwrap();
assert_eq!(cpu.regs.get32(crate::emulator::regs::Reg32::Eax), 0xCAFE);
assert_eq!(cpu.regs.eip, 0x2222);
assert_eq!(cpu.regs.esp(), esp_before + 12);
}
}