在浏览器中运行经典 Windows 32 位可执行文件(EXE),听起来像科幻,但 RetroTick 项目做到了。它利用 WebAssembly(WASM)polyfill 来仿真 Win32 系统调用,让旧程序无缝在现代 web 环境中执行,而无需完整模拟整个 Windows OS。这种方法的核心在于 API shims 和兼容层,特别是通过生成真实的 Win32 stub DLL 来拦截系统调用,转发到宿主机实现。

syscall 拦截的核心观点:从 IAT 到 trampoline

传统 Windows 用户态程序不直接调用内核 syscall,而是通过 kernel32.dll、user32.dll 等 DLL 的导出函数(如 WriteFile)。RetroTick 背后的 retrowin32 仿真器不模拟 NT 内核 ABI,而是聚焦 DLL 导出实现。在加载 PE 可执行文件时,仿真器解析 Import Address Table(IAT),将系统 DLL 的条目指向 “魔法地址” 或真实 stub DLL。

早期设计使用魔法地址:当仿真 x86 指令指针命中这些地址时,dispatch 到内置 handler。但这有局限,如 native x86-64 模式下拦截困难、调试工具困惑、性能检查开销大。新设计生成真实 Win32 DLL(如 kernel32.dll),每个导出函数是 tiny x86 stub,仅转发到共享符号 retrowin32_syscall。

例如,WriteFile stub 汇编类似:

WriteFile:
  call [__imp__retrowin32_syscall]
  ret 12  ; 清理栈

链接时,__imp__retrowin32_syscall 指向 IAT 条目,生成 thunk 跳转。retrowin32_syscall 在仿真模式下执行 sysenter 指令(易 hook),native 模式下用栈切换 far call。

“retrowin32 需要为系统库创建真实 DLL,以匹配 DLL 语义。”[1] 这解决了 LoadLibrary 检查 DLL 头、导出数据(如 vtables)、调试断点等问题。

WASM 环境下的 host dispatch 与 polyfill

在浏览器中,整个仿真器编译为 WASM,guest x86 代码在 WASM 实例中运行。syscall trampoline 触发后,Rust 实现的 handler(如 WriteFile)读取仿真内存栈:

pub fn WriteFile(machine: &mut Machine, hFile: HFILE, lpBuffer: &[u8]) -> bool {
  // 从 machine.mem 访问 guest 内存,转到 web APIs
}

lpBuffer 通过 machine.mem.slice (lpBuffer_ptr, len) 映射到 WASM linear memory,再桥接到 JS(如 Canvas2D for GDI)。

关键 polyfill 参数:

  • 内存布局:WASM linear memory 模拟 x86 地址空间,初始 64MB,可 grow 到 4GB。PE 加载基址固定 0x400000,避免 ASLR。
  • syscall hook 阈值:basic block 级别检查 IP,命中率 <5%(常见 API 覆盖 kernel32/user32 ~200 导出)。
  • 栈帧大小:Win32 stdcall,固定 ret 清理(如 ret 12 for 3 args *4B),WASM host fn 预读 32B 栈。
  • 超时 / 限流:每个 syscall 限 1ms CPU,防止 busy loop;WASM fuel ~10M instr/sec。

兼容层清单:

  1. DLL 加载:内置 stub DLL 字节嵌入二进制,动态加载时 memcpy 到 guest mem。
  2. Vtable 暴露:COM 接口如 DirectDraw,静态数据在 DLL 中:.globl _IDirectDrawClipper 后列函数指针。
  3. 数据导出:msvcrt.dll 变量如 errno,用 emulated mem 位置。
  4. 异常处理:SEH 通过 syscall 模拟 vectored handlers。
  5. 线程:单线程优先,multi-thread 用 WASM threads(实验)。

工程落地:构建与监控

构建 stub DLL:用 clang-cl 交叉编译 x86 asm,生成 retrowin32.lib 供链接。Rust codegen 产 asm 文件,自动化 Makefile:

clang-cl -m32 stub_kernel32.asm -link retrowin32.lib -dll -out:kernel32.dll
xxd -i kernel32.dll > src/builtins.rs  # 嵌入

部署到浏览器:WASM + JS glue,expose host funcs 如 mem_read/write via WASI 或 custom imports。

监控要点:

  • 兼容性:测试 50+ classics(Solitaire, Minesweeper, SkiFree),失败率 <20%,常见于 timing-sensitive 或 hardware API。
  • 性能:x86 emu ~10-50 MIPS,syscall 开销 <1us。瓶颈:mem access(用 WASM SIMD 加速)。
  • 回滚策略:若 app 查 DLL 头不符,fallback 到魔法地址模式。
  • 风险限:非生产级,复杂 app(如 full games)需补更多 API;安全沙箱依赖浏览器 CSP。

实际参数示例:运行 FreeCell 时,user32.MessageBox syscall 路径:

  1. IAT -> user32.MessageBox stub。
  2. stub call retrowin32_syscall (sysenter)。
  3. hook -> Rust MessageBox: parse LPCSTR -> JS alert () 或 custom modal。
  4. 返回 eax=IDOK。

这种设计优雅隐藏宿主差异,guest 只见 Win32 ABI。RetroTick 证明 WASM 可承载 legacy 软件栈,未来可扩展到 ARM 等。

资料来源: [1] https://neugierig.org/software/blog/2024/09/retrowin32-syscalls.html [2] https://github.com/evmar/retrowin32 [3] https://retrotick.com/