在浏览器中运行经典 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。
兼容层清单:
- DLL 加载:内置 stub DLL 字节嵌入二进制,动态加载时 memcpy 到 guest mem。
- Vtable 暴露:COM 接口如 DirectDraw,静态数据在 DLL 中:
.globl _IDirectDrawClipper后列函数指针。 - 数据导出:msvcrt.dll 变量如 errno,用 emulated mem 位置。
- 异常处理:SEH 通过 syscall 模拟 vectored handlers。
- 线程:单线程优先,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 路径:
- IAT -> user32.MessageBox stub。
- stub call retrowin32_syscall (sysenter)。
- hook -> Rust MessageBox: parse LPCSTR -> JS alert () 或 custom modal。
- 返回 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/