OHVM(Only for Happy VM)是一个极其简单的字节码解释器。
它不是通用虚拟机,也不模拟真实 CPU。这个项目只保留理解解释器所需的最小闭环:读取字节码、维护运行状态、逐条取指并执行,直到程序停止。
字节码文件 -> 加载 -> 取指 -> 解码 -> 执行 -> HALT
- 足够小:核心只有一个 VM 结构和一个
switch执行循环。 - 足够完整:支持整数运算、变量、分支、循环、函数调用和输出。
- 行为明确:所有整数与地址都使用小端编码,错误通过状态码返回。
- 容易验证:没有第三方依赖,并为主要执行路径提供单元测试。
不包含图形、声音、垃圾回收、即时编译、高级语言或汇编器。这些功能会遮住项目真正想说明的事情。
OHVM 是一台 32 位整数栈机:
| 资源 | 容量 | 用途 |
|---|---|---|
| 字节码 | 4096 字节 | 保存待执行指令 |
| 操作数栈 | 256 个 int32 |
传递参数和保存计算结果 |
| 全局槽位 | 256 个 int32 |
保存变量 |
| 返回地址栈 | 64 个地址 | 支持 CALL / RET |
ip 指向下一条指令。除 JMP、成功跳转的 JZ、CALL 和 RET 外,执行指令后它自然向前移动。
栈效果使用 (执行前 -- 执行后) 表示。a 是较早入栈的值,b 是栈顶值。
| 编码 | 指令 | 操作数 | 栈效果 / 行为 |
|---|---|---|---|
00 |
HALT |
- | 停止程序 |
01 |
PUSH |
i32 |
(-- value) |
02 |
POP |
- | (value --) |
03 |
DUP |
- | (value -- value value) |
04 |
LOAD |
u8 slot |
(-- globals[slot]) |
05 |
STORE |
u8 slot |
(value --),写入全局槽位 |
06 |
ADD |
- | (a b -- a+b) |
07 |
SUB |
- | (a b -- a-b) |
08 |
MUL |
- | (a b -- a*b) |
09 |
DIV |
- | (a b -- a/b) |
0a |
MOD |
- | (a b -- a%b) |
0b |
EQ |
- | (a b -- a==b) |
0c |
LT |
- | (a b -- a<b) |
0d |
GT |
- | (a b -- a>b) |
0e |
JMP |
u16 address |
无条件跳转 |
0f |
JZ |
u16 address |
(condition --),为零时跳转 |
10 |
CALL |
u16 address |
保存返回地址并跳转 |
11 |
RET |
- | 返回最近一次调用 |
12 |
PRINT |
- | (value --),输出十进制整数 |
文件头固定为 8 字节,后面紧跟指令:
| 偏移 | 大小 | 内容 |
|---|---|---|
| 0 | 4 | ASCII 魔数 OHVM |
| 4 | 1 | 格式版本,当前为 1 |
| 5 | 1 | 保留,必须为 0 |
| 6 | 2 | 入口地址 u16,相对于指令区,小端 |
| 8 | 其余 | 指令与操作数 |
所有 u16 和 i32 操作数均为小端,与宿主机字节序无关。
下面的完整字节流会输出 42:
4f 48 56 4d 01 00 00 00 # 文件头
01 2a 00 00 00 # PUSH 42
12 # PRINT
00 # HALT
下面使用助记符和标签展示字节码逻辑,方便阅读。OHVM 当前没有汇编器;生成字节码时,需要将标签替换为相对于指令区起点的 u16 字节地址。
计算 39:
PUSH 6
PUSH 7
ADD
PUSH 3
MUL
PRINT
HALT每条算术指令弹出栈顶的两个值,并把结果重新压栈。例如 SUB 执行的是 a - b,其中 b 是原来的栈顶值。
用全局槽位 0 保存总和,槽位 1 保存循环变量:
PUSH 0
STORE 0 ; sum = 0
PUSH 1
STORE 1 ; i = 1
loop:
LOAD 0
LOAD 1
ADD
STORE 0 ; sum += i
LOAD 1
PUSH 1
ADD
DUP
STORE 1 ; ++i,并保留一份用于比较
PUSH 101
LT ; i < 101
JZ done
JMP loop
done:
LOAD 0
PRINT ; 5050
HALTJZ 会弹出条件值,仅当它为 0 时跳转。因此这里在 i < 101 不成立时离开循环。
调用 square 计算
PUSH 7
CALL square
PRINT ; 49
HALT
square:
DUP ; (value -- value value)
MUL
RETCALL 把下一条指令的地址压入返回地址栈,再跳转到目标地址;RET 弹出该地址并继续执行。
Linux、macOS:
make
./ohvm program.ohvmbcWindows(MinGW):
mingw32-make
.\ohvm.exe program.ohvmbc使用 --trace 可查看每一步的指令地址、操作码和栈深度;--max-steps N 可以限制执行步数,避免错误程序无限循环。
./ohvm --trace --max-steps 1000 program.ohvmbcmake test
# Windows: mingw32-make test测试覆盖算术、循环、函数调用、字节码文件加载以及典型错误。实现入口见 vm.c,指令和机器状态定义见 vm.h。
生成四个可直接执行的字节码程序:
make examples
# Windows: mingw32-make examples| 文件 | 演示内容 | 输出 |
|---|---|---|
examples/answer.ohvmbc |
压栈与输出 | 42 |
examples/sum_1_to_100.ohvmbc |
全局变量、条件和循环 | 5050 |
examples/factorial.ohvmbc |
乘法与循环 | 120 |
examples/function_call.ohvmbc |
CALL、RET 与栈传参 |
49 |
例如:
./ohvm examples/sum_1_to_100.ohvmbc示例的可读源码位于 examples/make_examples.c。它直接使用操作码构造字节流,没有隐藏的编译或汇编阶段。
早期 OHVM 同时设想了寄存器、图形、键盘、声音、汇编器和高级语言,但大部分功能停留在规范或空实现中。当前版本主动舍弃这些范围,只表达最初、也最重要的想法:亲手做一台能够可靠执行简单程序的小机器。