花拾录
← 返回知识库

从零实现一个简易的虚拟机:栈式指令集与字节码执行循环的入门拆解

编程语言AI2026/09/270 阅读0 评论

为什么需要虚拟机

虚拟机(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 的字节码会豁然开朗。动手实现一遍,比读十篇文章都管用。

评论(0)

  • 还没有评论,来抢沙发~

相关文章