浏览器标签中的Windows内核,第一部分:冷启动、快速启动与四兆字节
摘要
本文介绍了 nanokrnl(一个用 Rust 编写的 64 位 Windows 内核)和 nanox(一个 65KB 的 WebAssembly 模拟器),后者可直接在浏览器中以长模式启动该内核,仅需约四兆字节即可运行一个操作系统。
<p><a href="https://lobste.rs/s/oaqknh/windows_kernel_browser_tab_part_i_cold">评论</a></p>
查看缓存全文
缓存时间: 2026/07/03 22:28
# 浏览器标签页中的 Windows 内核,第一部分:冷启动、快速启动与四兆字节
来源:https://www.msuiche.com/posts/nanokrnl-cold-boot-fast-boot/
2026 年 7 月 3 日 · 2207 字 · 11 分钟阅读
*作者:Twinkle.*
前两篇文章([第一部分](https://www.msuiche.com/posts/fable-5-windows-kernel/) 和 [第二部分](https://www.msuiche.com/posts/fable-5-windows-kernel-part-2/))讲述了这件事是如何写成的:一个用 Rust 编写的 NT 风格内核,Fable 5 在三十八分钟内从一个空目录将其启动到系统,随后几天里发展成能够加载真实 Windows 驱动并运行真实微软控制台二进制文件的系统。那些文章是起源故事。这是一个不同的系列。它关注的是工件本身:**nanokrnl**,内核,以及**nanox**,我们为在浏览器中运行它而编写的模拟器。这里没有 AI 流程叙述,只有系统本身。第一篇回答一个小问题,而这个问题事实证明是一个好问题。当你打开 nanokrnl.ai,机器到达 `C:\` 提示符时,究竟发生了什么,它占用了多少内存?简而言之:运行中的操作系统在提示符下占用大约四兆字节。运行它的模拟器是六十五千字节的 WebAssembly。这两个数字都值得细品,所以我们先逐步展开。
## nanox:一个 64 位模拟器,因为现成的那些不合适
🔗 (https://www.msuiche.com/posts/nanokrnl-cold-boot-fast-boot/#nanox-a-64-bit-emulator-because-the-off-the-shelf-ones-do-not-fit)
不谈运行 nanokrnl 的模拟器,就无法谈论在浏览器中启动它,因为这是第一个难题,它决定了其他一切。nanokrnl 是一个 64 位内核。它在 x86-64 长模式下运行:四级分页、`syscall`/`sysret`、`swapgs`、本地 APIC、通过真实 IDT 传递的中断。它不是一个在 32 位保护模式下运行的玩具。这一事实排除了显而易见的浏览器选项。
**v86** 是大多数人首先想到的模拟器。它体积小,纯 JavaScript,能够在浏览器中启动 Linux 和 Windows 2000。但它在这里行不通,而且我是字面意思。在 Node 中下运行我们的内核镜像,它会在任何串行输出出现之前因 `Unimplemented: #GP handler` (cpu.rs:846) 而崩溃。v86 是一个 32 位时代的模拟器。它的长模式支持不完整到无法通过 IDT 传递通用保护错误,而一个 64 位内核在第一次故障时就崩溃了,甚至无法打印它已启动。对于 64 位内核,v86 根本不可行,任何补丁代码都无法改变这一点:差距在于 CPU 核心。
**qemu-wasm** 则是另一个极端。它是通过 emscripten 编译为 WebAssembly 的 QEMU,因此它忠实地模拟完整的 PC,并且可以原样启动我们的镜像。但它大约是一个 46 MB 的工件,并且需要线程、`SharedArrayBuffer` 以及 `COOP`/`COEP` 跨源隔离头才能运行。对于“在标签页中看到内核启动”来说,这是一个沉重的依赖足迹,仅头要求就使其难以托管在静态站点上。
因此我们编写了 **nanox**。它是一个用 Rust 编写的定制 x86-64 模拟器,编译为一个单独的 **67,074 字节** 的 WebAssembly 模块,大约六十五千字节。没有线程。没有 `SharedArrayBuffer`。没有跨源头。它可以放在任何静态主机上。使其保持小巧的设计选择是,nanox 并非从重置向量开始模拟 PC。没有 BIOS,没有实模式启动,没有 A20 门,没有保护模式过渡。nanox **直接在长模式下启动内核**:它构建页表、GDT 与 TSS、IDT,以及 64 位内核期望已经存在的控制寄存器,应用 `bootloader_api` 交接结构,然后跳转到 `_start`。它实现了刚好足够的架构来运行这个内核及其之上的真实二进制文件:
- 四级分页,带有 2 MiB 和 1 GiB 大页快捷方式(见下文),
- `syscall`/`sysret` 和 `swapgs`,
- 本地 APIC(定时器和处理器间中断),
- 通过客户 IDT 传递中断和故障,
- 16550 UART 和 PS/2 控制器,
- 以及一个小的 9P 传输设备,浏览器页面通过它向内核提供文件(本系列后续文章)。
“刚好足够”对于模拟器来说是一个危险的短语,因为缺少或错误的指令的成本是静默发散,而不是编译错误。因此 nanox 通过与真实 oracle 进行差异测试来验证:使用 [iced-x86](https://github.com/icedland/iced) 作为解码长度 oracle,使用 [Unicorn](https://www.unicorn-engine.org/)(QEMU 的 CPU 核心)作为随机状态上的语义 oracle,以及一个锁步测试工具,该工具逐条指令重放真实程序的真实执行,并将后状态与 Unicorn 进行差异比较。这个测试工具物有所值。在为此项目连接文件服务功能时,它发现 nanox 缺少 `BSWAP`,这是一个完全标准的指令,优化器只在少数习语中才发出它,而在此之前它从未出现在指令流中。这本身就是一个好故事,也是后续一篇文章的主题,关于如何找到你忘记实现的指令。
## 到达提示符的两种方式
🔗 (https://www.msuiche.com/posts/nanokrnl-cold-boot-fast-boot/#two-ways-to-arrive-at-the-prompt)
打开页面,你会看到两个按钮,它们都以不同路径最终到达 `C:\`。
```
flowchart TD
A[打开页面] --> B{哪个按钮?}
B -->|启动 / 重启| C[冷启动]
B -->|快速启动| D[快速启动]
C --> C1[将内核 ELF 加载到 RAM 中]
C1 --> C2[进入长模式,跳转到 _start]
C2 --> C3["~100,000,000 条解释指令;自测试滚动而过"]
C3 --> E["交互式 C 提示符"]
D --> D1["获取 snapshot.bin.gz,901 KB"]
D1 --> D2[使用 DecompressionStream 解压缩]
D2 --> D3["恢复寄存器 + 1,052 个 RAM 页面"]
D3 --> E
```
**冷启动**是真实情况。nanox 将内核镜像加载到模拟 RAM 中,进入长模式,并开始解释。内核初始化其子系统,运行其完整的自测试套件(六十七个通过的检查滚动而过),加载一个 PE 驱动并测试其定时器、DPC 和 IOCTL 路径,针对 kernel32、msvcrt 和 ulib 填充程序启动一个用户模式进程,最后启动 cmd.exe。通过这种方式到达提示符大约需要 **一亿条解释指令**。在现代笔记本电脑上,这相当于几秒钟观察机器实际通电、自测试等过程。这就是引导和重启按钮所做的,这是一个真实的演示:一个 64 位内核从镜像启动,在标签页中。
**快速启动**则是有意作弊的,值得精确解释这个作弊是什么,因为它更有工程趣味。
## 快照:冻结整个机器,传输增量
🔗 (https://www.msuiche.com/posts/nanokrnl-cold-boot-fast-boot/#the-snapshot-freeze-the-whole-machine-ship-the-delta)
一亿条指令一次是便宜的,但如果每次页面加载都重新解释整个启动过程,演示就会感觉像加载屏幕。因此我们在构建时一次性将内核启动到提示符,并捕获模拟机器的整个状态到一个文件中。浏览器随后可以恢复该文件,并在零解释启动的情况下到达 `C:\`。x86-64 机器的状态具体来说是两部分:CPU 和 RAM。nanox 将两者序列化为一个小的自描述 blob(魔数 `NXS1`):
- **CPU 和设备**:十六个通用寄存器、`RIP` 和 `RFLAGS`、段基址、所有十六个 XMM 寄存器、控制寄存器(`CR0`/`CR3`/`CR4`/`EFER`,加上 `CR2`/`CR8`)、描述符表寄存器、`syscall` MSR(`STAR`/`LSTAR`/`SFMASK`、`KERNEL_GS_BASE`)、TSS 指针以及 UART、APIC 和 PS/2 控制器的设备状态。所有这些加起来只有 **624 字节**。整个机器架构 CPU 状态比这一段文字还小。
- **RAM**:这里变得很巧妙。机器有 128 MiB 的 RAM,即 32,768 个 4 KiB 页面。但其中大部分页面从未被触及。快照逐页遍历内存,只输出包含非零字节的页面,每个页面以其索引为前缀。零页面被直接省略,恢复时先将 RAM 清零,然后将保存的页面放回原位。结果如下:在 32,768 个 RAM 页面中,提示符下正好有 **1,052** 个非零页面。其余 96.8% 的地址空间从未被写入,存储成本为零。
所以快照是:
- RAM 已分配:128 MiB(32,768 页)
- 实际触发的页面:1,052(3.2%)
- 触发的 RAM:4,308,992 字节(4.11 MiB)
- CPU + 设备状态:624 字节
- 原始快照:4,313,828 字节(4.11 MiB)
- 发送到浏览器的 gzip 压缩快照:922,305 字节(约 901 KB)
浏览器获取这个 901 KB 的文件,使用 `DecompressionStream` API 就地解压缩(无需库,这是平台内置的),将字节交给 nanox,然后调用恢复。压缩比约为 4.7 倍,因为即使是被触及的页面也是稀疏的:页表、栈和刚刚清零的堆区域内部大多是一串零。快速启动需要在 UI 中有一个横幅,以及引导和重启现在总是执行真正的冷启动的原因,正是因为这个方法工作得太好了。恢复快照速度极快,以至于访问者可能会错过这里实际上启动了一个真实操作系统。因此页面会告诉你正在看的是哪一种,而重启总是向你展示真实情况。
## 四兆字节就是整个操作系统
🔗 (https://www.msuiche.com/posts/nanokrnl-cold-boot-fast-boot/#four-megabytes-is-the-whole-operating-system)
现在回到开头那个数字。快照是 4.11 MiB,不是因为我们选择了预算,而是因为运行中的系统在交互提示符下实际占用的内存就是这么多。这是一个测量结果,而不是一个目标。这 4.11 MiB 用在哪里?大约 **1.60 MiB** 是内核本身:其代码、初始化数据和零初始化的 BSS,这是从镜像的可加载段测量的(磁盘上 2.64 MB 的内核文件因调试信息而膨胀,这些信息从未映射到 RAM 中)。剩下的约 2.5 兆字节是运行系统在其之上建立的一切:页表、内核池和栈,以及提示符下的活动用户模式进程,这意味着 cmd.exe 以及它运行时依赖的 kernel32、msvcrt 和 ulib 填充镜像。这是一个完整的、可交互的 64 位操作系统,拥有驱动模型、对象管理器、调度器、系统调用层和运行中的命令 shell,仅占四兆字节。
## 对比:臃肿程度
🔗 (https://www.msuiche.com/posts/nanokrnl-cold-boot-fast-boot/#the-bloat-for-contrast)
值得记住的是底线已经抬升了多少。以下是微软为其发布的 Windows 版本公布的最低 RAM 要求:
| 版本 | 发布年份 | 最低 RAM |
|------|----------|----------|
| Windows 95 | 1995 | 4 MB |
| Windows 98 | 1998 | 16 MB |
| Windows XP | 2001 | 64 MB |
| Windows Vista | 2007 | 512 MB(Aero 体验需要 1 GB) |
| Windows 7 | 2009 | 1 GB(32 位),2 GB(64 位) |
| Windows 11 | 2021 | 4 GB,外加 64 GB 存储、TPM 2.0 和安全启动 |
Vista 是每个人记忆中的转折点。“Vista Capable”时代是底线在单个版本中跃升了八倍,一代原本性能良好的机器突然变得配置不足。从那以后要求只升不降,Windows 11 在内存底线之上又加了一道硬件门槛(TPM 2.0、安全启动),结果就是把能用的笔记本电脑扔掉。将 nanokrnl 放在那个表格旁边,意思不言自明。运行中的系统只占用大约 **4 兆字节**,大致相当于 Windows 95 的 *1995 年最低要求*,而且它是在浏览器标签页内,由 65 KB 的模拟器驱动,无需安装、无需头文件、无需丢弃任何东西。这显然不是与 Windows 功能对等,也不是试图成为那样。这是一个对底线的展示:一旦你停止堆积,一个真正的、结构化的操作系统实际上只需要很少的资源就能到达交互式提示符。
## 关于页面大小的旁白(既然提到了)
🔗 (https://www.msuiche.com/posts/nanokrnl-cold-boot-fast-boot/#an-aside-on-page-sizes-since-it-came-up)
四兆字节这个数值是以 4 KiB 粒度测量的,因为快照就是这样扫描内存的:一页要么全为零(跳过),要么不全为零(保存)。但内核不仅仅以 4 KiB 页为单位思考。nanox 的 MMU 和内核的页表遍历器都支持大页。当页大小位被设置时,遍历器在 PDPT 中的 1 GiB 叶子或 PD 中的 2 MiB 叶子处短路,两边都是如此。这并非装饰:引导加载程序将物理内存的直接映射以大量页面构建起来交给内核,因此内核*必须*理解它们才能转换自己的地址。当需要更精细的控制时,例如将加载的 PE 镜像的页面标记为可执行而不翻转整个 2 MiB 区域的不可执行位,它会将大页拆分为 4 KiB 条目,并仅调整它打算修改的页面上的标志。内核自己的新分配目前仍然是 4 KiB;为内核分配主动创建大页映射是我们尚未投入的已知优化。所以对“只有 4K 还是也有大页?”这个问题的诚实回答是:机器正确消耗和操作 2 MiB 和 1 GiB 页面,而为其创建内核自身映射是未来的工作。
## 下一篇
🔗 (https://www.msuiche.com/posts/nanokrnl-cold-boot-fast-boot/#next)
以上就是启动路径和内存占用。这个系列的后续文章将进一步深入本文只提及的部分:nanox 如何被验证以及缺失的 `BSWAP` 是如何被追查的,9P 传输如何让浏览器页面通过 `H:` 驱动器向内核提供真实的主机文件,以及内核如何在其自己的 NT 系统调用上运行未经修改的微软控制台二进制文件。如果你想先试试看,[nanokrnl.ai](https://nanokrnl.ai/) 上有两个按钮,源代码在 [GitHub](https://github.com/msuiche/nanokrnl) 上。
相似文章
浏览器标签中的类Linux内核 - 深入解析BrowserPod架构
深入解析BrowserPod架构,这是一个基于WebAssembly内核的浏览器内沙箱,完全在客户端运行兼容Linux的应用程序。本文涵盖内核设计、磁盘和网络子系统,以及其在浏览器中运行诸如Claude Code等工具的能力。
用 x86_64 汇编写成的 Linux 桌面
一位开发者借助 Claude Code,用纯 x86_64 汇编重建了完整的 Linux 桌面栈——从 shell、终端、窗口管理器到各种工具,实现微秒级启动,并延长数小时续航。
这周你在做什么?
一位 Lobsters 用户分享了他用 LLM 生成一个 Rust 编写的玩具操作系统内核的实验:他手动重构生成的代码以加深理解,并成功在笔记本电脑上从 U 盘启动了该内核。
裸启动Linux
本文介绍如何创建一个运行单进程的最小化Linux启动,精简通常的initramfs,使启动时间小于一秒。
用地道 Rust 重写的 Linux 0.11,可在 QEMU 中启动
一个用现代 Rust 重写的 Linux 0.11 内核,可在 i386 上的 QEMU 中启动,包含完整的用户空间(shell 和 coreutils),支持一键构建镜像和 devcontainer 环境。