从零构建寄存器式 JSVMP:实战教程导读

26 阅读4分钟

先说人话:这套教程到底在解决什么问题?

你大概率见过这种场景:

  • 业务代码一上线,核心逻辑很快就被别人扒走
  • 明明会用 Babel 写 AST 插件,但一碰到“怎么把 AST 变成可执行字节码”就卡住
  • 知道闭包、作用域链、this 这些概念,可一旦要自己实现一个运行时,脑子里全是结

这套教程就是奔着这个痛点来的。我们不会停在“JSVMP 是什么”的概念介绍,而是带你把一个能跑起来的寄存器式 JSVMP,从编译到执行,一步一步拆开。

你会亲手做出什么?

一个最小可用的 JavaScript 虚拟化保护编译器:

  • 输入是一段普通 JavaScript
  • 中间会经过 AST、IR、字节码几个阶段
  • 输出是一段自包含的 JS 文件,里面带着字节码和解释器
flowchart LR
    A["源码<br/>var x = 1 + 2"] --> B["Frontend<br/>解析成 AST"]
    B --> C["Lowering<br/>展平成 IR"]
    C --> D["Emit<br/>编码成数字字节码"]
    D --> E["Pack<br/>拼上运行时"]
    E --> F["最终输出<br/>一段可执行 JS"]

如果你把它类比成“翻译系统”,会更好理解:

  • AST 像语法分析后的句子结构
  • IR 像翻译过程中的中间稿
  • 字节码像只给内部员工看的工单编号
  • VM 解释器像真正干活的执行班组

外面的人看到的是一堆编号,但系统内部知道每个编号该怎么做。

为什么这条路线值得学?

因为它会把很多平时“会用但说不清”的东西,逼着你真正吃透。

  • 你会真正理解编译器后端在干什么,而不是只停在 AST 改写
  • 你会知道闭包为什么本质上是“函数 + 活着的环境对象”
  • 你会知道 var 提升、let 的 TDZ、this 绑定这些语义,运行时到底该怎么还原

但也先把丑话说前面:这条路线不轻松。它不像写个 Babel 插件那样当天就能见效,中间会反复遇到“看上去只差一行,结果整个 VM 跑偏”的问题。也正因为这样,这套教程才适合想进阶的人。

这套项目的真实边界

这里不装全能。

  • 项目核心目标是讲清楚 JSVMP 主链路,不是造一个完整 JS 引擎
  • 当前重点覆盖的是 ES5 核心语义,以及项目里已经实现的对象、异常、闭包、thisarguments 等能力
  • 某些高级语法、复杂解构、完整语言边角行为,不是这套代码当前阶段的重点

换句话说,它更像一台“教学级但能跑真代码”的样机,而不是拿来直接替代浏览器引擎。

这反而是它的价值所在。东西做得太大,读者只会被淹没;东西做得刚好,你才能看清每一个齿轮怎么咬合。

阅读方式建议

这套教程最好按顺序看,因为后面的章节会反复用到前面建立起来的几个核心心智模型:

  1. 寄存器是“中间结果的临时工位”
  2. slot 是“变量在环境里的固定抽屉”
  3. env 链是“运行时版本的作用域链”
  4. 字节码是“给 VM 执行的数字化操作清单”

如果你跳着看,单章也能读懂一部分,但很容易出现“每句话都认识,连起来不知道在说什么”的情况。

教程地图

阶段文件你会带走什么
0000-tutorial-guide.md先把整条路线和预期建立起来
0101-architecture-overview.md理解 JSVMP 是什么,为什么选寄存器机
0202-instruction-set-design.md搞懂 opcode、寄存器、slot、常量池怎么配合
0303-compiler-ast-to-ir.md看懂 AST 为什么要先降成 IR,以及 lowering 怎么写
0404-emit-and-runtime.md把符号化 IR 编成数字字节码,再交给 VM 跑起来
0505-es5-core-features.md闭包、thisarguments、提升这些硬骨头怎么落地
0606-testing-and-debugging.md怎么验证你的 VM 不是“看起来能跑,其实语义错了”

配套示例怎么跑?

仓库已经准备好了按章节拆开的示例。建议你一边看文档,一边跑对应例子,不要只看不动手。

pnpm build
node docs/examples/01-architecture/01-hello-vmp.js
node docs/examples/02-instruction-set/02-bytecode-decoder.js
node docs/examples/04-emit-and-runtime/01-step-by-step-execution.js

如果你把教程当成“视频字幕”来扫,收获会很有限。最有效的方式,是边读边猜结果,再运行示例验证自己的理解。

读代码前,先记住这几个核心文件

graph TD
    A["src/compiler/frontend.ts<br/>源码 -> AST"] --> B["src/compiler/lowering.ts<br/>AST -> IR"]
    B --> C["src/compiler/emit.ts<br/>IR -> 字节码"]
    C --> D["src/compiler/runtime-gen.ts<br/>生成解释器源码"]
    D --> E["src/compiler/pack.ts<br/>打成最终 JS"]
  • lowering.ts 是编译器最考验基本功的地方
  • emit.ts 负责把“人能看懂的指令”压成数字
  • runtime-gen.ts 是最容易出细碎 bug 的地方,因为这里的 pc、env、寄存器都得严丝合缝

你应该带着什么问题往下读?

我建议你边读边盯住这 4 个问题:

  1. AST 为什么不能直接执行,非得先变成 IR?
  2. 变量名为什么不直接塞进寄存器,而要分成 slot 和寄存器两套体系?
  3. 闭包捕获的到底是“值”,还是“环境对象”?
  4. 为什么 VM 里最难查的 bug,往往不是算法错,而是状态没对齐?

后面的每一章,都会围着这几个问题慢慢把账算清楚。

一句话记住这套教程

这不是一套“介绍 JSVMP”的文档,而是一套带你把编译器、字节码和运行时真正接起来的工程化拆解。