v86 深度调查:浏览器里跑 Windows 的原理
目录
调查对象:https://copy.sh/v86/ —— 在浏览器里运行完整 Windows 的开源项目。 本文回答四个问题:它是模拟器还是别的?原理是什么?Windows 是不是精简版?每次都要下载完整镜像吗?
一、技术本质:全系统 x86 模拟器 + 运行时 x86→WASM JIT
官方定义(GitHub README,2026-10):
“v86 emulates an x86-compatible CPU and hardware. Machine code is translated to WebAssembly modules at runtime in order to achieve decent performance.”
三层理解:
- 全系统模拟(full-system emulation):不是 Wine 那种“API 兼容层”,也不是容器;它模拟一整台 PC,从 BIOS、CPU、内存到磁盘控制器。
- CPU 是“解释 + JIT”混合:先解释执行,统计热页;热到阈值后,把整页 x86 指令翻译成 WebAssembly 模块,交给浏览器编译成本地机器码。
- 设备是 JavaScript/Rust 模拟:VGA、PS/2、IDE、网卡、声卡等由 JS 设备模型驱动,CPU 核在 WASM 里。
所以准确说法是:“真实 OS 镜像 + 模拟硬件 + x86→WASM JIT”,不是“没有模拟器”。
二、底层架构:x86 怎么被翻译,设备怎么模拟
1. CPU 与 JIT
- 指令集:32 位 x86,约 Pentium 4 水平,含 SSE3;不支持 64 位、多核、部分保护模式特性、任务门、单步调试等。
- 执行流程:
- 解释器先跑,记录函数调用/间接跳转入口,统计每个页的热度;
- 页热度超过 200,000 条指令(
JIT_THRESHOLD)后,编译整页,以及最多 3 个直接可达页(MAX_PAGES=3); - 生成一个大函数 +
br_table分发的 WASM 模块,用 stackifier 算法把任意控制流结构化; - 模块跑完退出,主循环检测下一个模块再进入(目前不做 WASM 模块链接)。
- 为什么这么设计:WASM 没有任意跳转、不能跨函数保留硬件寄存器、没有 mmap、模块生成慢且有内存开销。所以只能“整页编译 + 大 switch + 模块边界”。
- 优化:
- TLB 快路径:4MB 缓存,存物理地址、只读位、是否代码页、是否 MMIO;绝大多数访问走快路径,缺页/MMIO 走慢路径;
- lazy flags:算术指令延迟更新标志位;
- 条件跳转直译:
cmp+setcc合并成一次比较; - FPU 用 Berkeley SoftFloat:80 位浮点精确但很慢。
2. 内存与分页
- 客户机物理内存放在 WASM 线性内存/ArrayBuffer 里;
- 分页(页表、缺页、只读页、代码页失效)完全软件模拟,没有硬件 mmap;
- 代码页被写入时要失效对应 JIT 代码,保证自修改代码正确。
3. 设备模型
| 设备 | 模拟方式 | 说明 |
|---|---|---|
| VGA | Bochs VBE / SVGA,帧缓冲 + Canvas | 2D framebuffer,无 3D GPU 加速;默认 640×480×4bit(16 色) |
| 键盘/鼠标 | 8042 PS/2 控制器 | 扫描码注入;鼠标绝对/相对定位 |
| 硬盘 | IDE(PATA) | 不是 AHCI/SATA;支持 raw .img,异步按块加载 |
| CD-ROM | IDE + 内置 ISO 9660 生成器 | 支持 Joliet;.iso |
| 软盘 | 8272A 控制器 | 1.44MB 等 |
| 网卡 | NE2000 (RTL8390) 或 virtio-net | 需要代理后端:inbrowser / wsproxy / wisp / fetch;不是裸 socket |
| 声卡 | SoundBlaster 16 | Web Audio 输出,有延迟 |
| 其他 | PIT、PIC、RTC、PCI、部分 APIC、Hayes Modem | ACPI 仅部分/实验性 |
4. 为什么能“接近原生”?
准确说:不是接近原生,而是“热代码接近原生”。
- x86 热页被翻译成 WASM,浏览器再用 Liftoff/TurboFan 编译成本地机器码,所以纯计算循环可以跑到原生的一个可观比例;
- 但 CPU 状态在内存里而不是寄存器、模块边界、页表模拟、设备 I/O、Canvas 渲染、SoftFloat 都会拖慢;
- 第三方实测/估算:计算密集型约为原生的 20–40%,整体比原生虚拟机慢 3–10 倍(Pi Stack 2026-06 对比文;非官方数字)。
- Windows 98 之所以“感觉能接受”,是因为 1998 年的软件本身很轻、默认 16 色低分辨率,再加上状态快照直接跳过开机,不是因为它真的有原生速度。
- 3D 游戏、视频解码、现代浏览器基本不要想。
三、里面的 Windows 到底是什么
1. 版本与镜像大小(关键:页面上的“13+MB”不是磁盘大小)
| 系统 | 磁盘镜像 | 内存 | 状态快照 | 页面显示“Size” |
|---|---|---|---|---|
| Windows 98 | 300 MB raw .img | 128 MB | 13.4 MB .bin.zst | 13+ MB(是快照,不是磁盘) |
| Windows 2000 | 2 GB raw .img | 512 MB | 29.6 MB .bin.zst | 28+ MB(快照) |
| Windows 95 | 450 MB | 64 MB | 无(或另配) | 19+ MB |
| Windows ME | 1 GB | 256 MB | 有 | 28+ MB |
依据:src/browser/main.js 的 profile 配置;i.copy.sh 的 Content-Length 实测(2026-10-30)。
所以“Windows 98 只有 13MB”是误解:13MB 是预启动 RAM/CPU 快照,真正的磁盘是 300MB。
2. 是 9x 还是 XP/2000?
- 默认演示同时有 Windows 98(9x 内核) 和 Windows 2000(NT 5.0),不是二选一;
- 还有 95、ME、NT 3.1/3.51/4.0;
- Windows XP 不在默认列表,README 说“在特定条件下可运行”,属于实验性支持。
3. 是不是“精简定制版”?
- 是预制好的完整安装镜像,但做了“为 v86 适配”的精简,不是微软官方的“Lite 发行版”。
- 具体手段(来自官方
docs/windows-9x.md/windows-nt.md):- 用 QEMU 安装,
-M pc,acpi=off,改成 Standard PC(2000 以上必须关 ACPI); - 最小化安装,删掉不必要组件/文件夹(如 ADMIN、DEMOS、HELP 等);
- 装 VBE9x / VMDisp9x 驱动,才能上真彩色;默认只有 640×480×4bit;
- 预装 IE、小游戏、Winamp、Delphi 等,纯粹为了演示。
- 用 QEMU 安装,
- Windows 98 是 9x 内核,Windows 2000 是 NT 5.0;两者不是“精简版 XP”。
4. 镜像怎么“塞进去”的?
不是运行时安装,是预制的 raw 磁盘镜像。
流程:
- QEMU 里正常安装 Windows;
- 导出 raw
.img; - 用
tools/split-image.py按 256KB 切成0-262144.img、262144-524288.img…; - 静态服务器托管,v86 按需取块。
你也可以自己构建:qemu-img create -f raw hdd.img 300M → 安装 → 导出 → 切块 → 在 V86 配置里指向你的 hda。官方文档给了 9x/NT/Linux 的完整步骤。
版权提醒:v86 代码是开源 BSD-2-Clause,但 Windows 镜像版权归微软;自建/自托管镜像要自己解决授权。
四、加载机制:每次打开都要下完整镜像吗?
不需要。
1. 默认路径(?profile=windows98)
- 下载 JS/WASM(
v86_all.js、v86.wasm)和 BIOS; - 下载 13.4MB 的 zstd 状态快照(
windows98_state-v2.bin.zst),浏览器解压后restore_state(),直接进入桌面,不经历 BIOS/开机; - 磁盘镜像 300MB 不整包下载:
async: true+use_parts: true+fixed_chunk_size: 256KB,v86 只取客户机实际读到的 256KB 块(例如打开 IE 才取 IE 相关块); - 块有内存缓存(
block_cache),同一块不重复请求;服务器返回的 state 带Cache-Control: public, max-age=43200(12 小时),浏览器 HTTP 缓存可复用。
2. 有没有 IndexedDB?
没有。 我在源码里 grep 过 indexedDB,v86 默认实现只有:
- HTTP 缓存(服务器头控制);
- 内存块缓存(
Map,页面关闭即失); - 手动“Save State”是下载文件,不是自动 IndexedDB。
所以:刷新页面、缓存未过期 → 可能命中 HTTP 缓存;换浏览器/清缓存 → 重新下 13MB + 工作集。不会每次下 300MB,但也不是“永久本地缓存”。
3. 首次加载多久?
- 状态快照 13.4MB:10–50 Mbps 下约 2–10 秒;加上 JS/WASM 和浏览器解压,桌面通常 数秒内出现;
- 冷启动 profile(
windows98-boot,无快照):要读大量磁盘块,30 秒到几分钟,取决于网络和磁盘; - Windows 2000 状态 29.6MB,首次更久。
以上是量级估算,不是官方 benchmark。
五、项目本身
| 项目 | 事实 |
|---|---|
| 作者 | Fabian Hemmer(GitHub copy,copy.sh,copy@copy.sh) |
| 起始 | 2013-11-06 创建仓库 |
| 协议 | BSD-2-Clause(内置 SeaBIOS 等第三方组件各自许可) |
| Star | 23,644(2026-10-30) |
| Fork | 1,938 |
| 活跃度 | 最后 push 2026-10-18;demo 构建 1e4f43c95(2026-07-03) |
| 语言 | JavaScript + Rust(CPU/JIT 编译到 WASM) |
| 支持系统 | Linux(32 位)、Windows 1.x/3.x/95/98/ME/NT/2000、FreeDOS/MS-DOS、ReactOS、Haiku、FreeBSD/OpenBSD、Android-x86 1.6/4.4、KolibriOS、QNX、SerenityOS 等 50+ |
| 嵌入 | libv86.js / npm v86;给 screen_container + BIOS + 磁盘即可 |
| 自建镜像 | 可以;官方有 9x/NT/Linux 构建文档 |
| 主要限制 | 无 64 位、无多核、无 3D GPU、无 USB、IDE 非 AHCI、ACPI 部分、FPU 慢、网络需代理、音频延迟、XP 实验性 |
| 镜像版权 | 代码开源 ≠ Windows 镜像可自由分发 |
六、同类项目对比
| 项目 | 技术路线 | 能跑什么 | 性能/特点 |
|---|---|---|---|
| v86 | 全系统 x86 模拟 + 运行时 x86→WASM JIT | Windows 9x/2000、Linux、DOS、ReactOS | 计算 20–40% 原生;完整 PC,兼容性好 |
| JSLinux(Fabrice Bellard) | 纯 JavaScript x86 模拟(无 WASM JIT) | Linux(2.6/4.x 等) | 更慢;只做 Linux,不支持 Windows |
| WebVM / CheerpX | 专有 x86→WASM JIT,用户态 Linux | Debian/Linux 应用 | 接近原生;闭源、偏应用层 |
| PCjs | 周期精确的 8086/286/386 模拟 | 早期 IBM PC、DOS | 历史还原度高,速度慢,不适合 Win9x |
| QEMU.wasm / container2wasm | QEMU 全系统模拟编译到 WASM | 多种 OS | 功能全但体积大、速度慢 |
| Figma | C++ 编译到 WebAssembly(不是 x86 模拟) | 设计工具渲染 | 说明 WASM 对计算密集代码可接近原生,但不是同一类 |
| 98.js | 用 JS 重画 Windows 98 桌面 | 网页版“像 Win98” | 不是真 OS,不能跑任意 exe |
| wine-assembly | x86 解释器 + Win32 API 重实现 | 直接跑 Win98 exe | 不启动完整 OS,应用级方案 |
一句话区分:v86 是“把整台 PC 搬进浏览器”;JSLinux 是“只把 Linux 搬进去”;CheerpX 是“把 Linux 用户态搬进去但闭源”;98.js 是“画了一个 Windows 98”;Figma 是“用 WASM 加速自己的 C++”,不是模拟器。
七、事实 / 推测对照
| 说法 | 定性 |
|---|---|
| v86 是全系统 x86 模拟器,不是 OS 重实现 | 事实(README/源码) |
| 运行时把 x86 热页翻译成 WASM | 事实(docs/how-it-works.md、jit.rs) |
| Windows 98 磁盘 300MB、2000 磁盘 2GB | 事实(profile 配置) |
| 页面“13+MB/28+MB”是状态快照,不是磁盘 | 事实(HEAD 实测 13.4MB/29.6MB) |
| 每次打开要下完整镜像 | 错误;只下状态 + 按需 256KB 块 |
| 用 IndexedDB 缓存镜像 | 没有;默认 HTTP 缓存 + 内存块缓存(源码 grep 无 IndexedDB) |
| Windows 98 是 9x、2000 是 NT 5.0 | 事实;XP 非默认,实验性 |
| 镜像是“精简定制版” | 部分事实:完整安装后做兼容性精简(Standard PC、关 ACPI、VBE 驱动、删组件),非官方 Lite 发行版 |
| “接近原生速度” | 夸大;第三方估算计算 20–40%,整体慢 3–10 倍 |
| 作者 Fabian Hemmer、BSD-2-Clause、23.6k stars | 事实(GitHub API 2026-10-30) |
| 自建镜像可塞进去 | 事实(官方 9x/NT 文档、split-image.py) |
| Windows 镜像可自由分发 | 错误;代码开源不等于镜像授权 |
八、结论
- 它是真模拟器,跑的是真 OS:真实 Windows 镜像 + 模拟的 x86 PC + x86→WASM JIT。
- “接近原生”要打引号:热代码可以到原生的 20–40%,整体远不到原生;Windows 98 的流畅感来自“轻负载 + 快照跳过开机”。
- 13MB 不是 Windows 的大小:是预启动状态快照;磁盘是 300MB(98)/2GB(2000)。
- 不用每次下完整镜像:状态快照 + 256KB 按需分块 + HTTP 缓存;没有 IndexedDB。
- 项目健康:23.6k stars、BSD-2-Clause、2026 年仍活跃;适合教学、怀旧、嵌入式 Web VM,不适合跑现代 3D/视频/生产负载。
如果陛下想,我可以下一步做两件事之一:① 在本机用 v86 嵌一个最小 Linux 镜像,实测 MIPS 和首屏时间;② 把 Windows 98 的磁盘块请求抓一遍,画出“打开 IE 到底下载了哪些块”的图。