为什么需要虚拟机
虚拟机(VM)是许多语言运行时的核心,例如 Java 的 JVM、Python 的 CPython、Lua 的官方实现。它把源码编译成字节码(bytecode),再由一个执行循环(dispatch loop)解释执行。本文带你实现一个栈式虚拟机,理解字节码如何驱动计算。
栈式指令集设计
栈式 VM 不使用寄存器,所有操作数都从操作数栈(operand stack)中弹出,结果再压回栈。指令集极简,通常包含:
PUSH n:将常量 n 压栈ADD/SUB/MUL/DIV:弹出两个操作数,计算后压回结果LOAD i/STORE i:从局部变量槽 i 读写JMP addr/JZ addr:无条件跳转 / 栈顶为 0 时跳转PRINT:弹出栈顶并输出HALT:停机
例如计算 (1 + 2) * 3 的字节码为:
PUSH 1
PUSH 2
ADD
PUSH 3
MUL
PRINT
HALT
执行循环的实现
执行循环是一个 while 循环,用程序计数器(PC)遍历字节码数组。用 Python 示例(其他语言同理):
def run(bytecode, constants):
stack = []
locals_ = [0] * 10
pc = 0
while pc < len(bytecode):
op = bytecode[pc]
pc += 1
if op == 'PUSH':
stack.append(constants[bytecode[pc]])
pc += 1
elif op == 'ADD':
b = stack.pop(); a = stack.pop()
stack.append(a + b)
elif op == 'MUL':
b = stack.pop(); a = stack.pop()
stack.append(a * b)
elif op == 'PRINT':
print(stack.pop())
elif op == 'HALT':
break
注意:字节码中的 PUSH 后跟的是常量表索引,而不是直接数值,这样便于统一指令格式。
支持控制流
加入跳转指令后,VM 就能执行循环和条件。例如 while (x < 10) { x = x + 1 } 可编译为:
LOAD 0 # 把 x 压栈
PUSH 10
LT # 比较,压入 1 或 0
JZ end # 若为 0 跳出
LOAD 0
PUSH 1
ADD
STORE 0
JMP start
end: HALT
实现 LT 和 JZ 只需几行代码。
关键细节与常见坑
- 栈平衡:每条指令执行前后,操作数栈的变化必须确定,否则跳转后栈会错位。
- 常量表:将数字、字符串等常量统一存放,字节码只存索引,便于序列化。
- 局部变量槽:用固定长度数组或字典模拟,
LOAD i/STORE i的 i 是槽位编号。 - 错误处理:栈下溢、非法操作码、PC 越界都要检测,否则 VM 会崩溃。
从玩具到实用
这个简易 VM 已经具备图灵完备的潜力(配合跳转和内存读写)。你可以继续扩展:
- 增加函数调用(
CALL/RET,配合调用栈) - 引入堆内存和对象(
NEW/GETFIELD) - 做字节码验证器,防止恶意字节码破坏宿主
理解栈式 VM 后,再看 JVM、CPython 的字节码会豁然开朗。动手实现一遍,比读十篇文章都管用。