tsatsulya/compact-bytecode-vm
GitHub: tsatsulya/compact-bytecode-vm
一个用 C++20 实现的紧凑型基于栈的字节码虚拟机,提供声明式 ISA、静态验证器、双分发策略和基准测试,专注于解释器工程的严谨实现。
Stars: 0 | Forks: 0
# 紧凑型 Bytecode VM
一个基于 C++20 实现的紧凑型基于栈的字节码虚拟机。该项目专注于解释器相关的工程实现——包括单一的规范 ISA、安全解码、静态验证、调用帧、带标签的值、显式失败语义,以及可衡量的分发策略——而不是单纯堆砌操作码。
## 架构
```
isa.yaml -> generate_isa.py -> isa_generated.h
|
source -> assembler -> bytecode -> verifier -> executor -> Value
| |-- switch dispatch
| `-- direct-threaded dispatch
`-> disassembler
```
- `isa.yaml` 是操作码编号、操作数和栈效果的唯一事实来源。当其发生更改时,CMake 会重新生成 C++ 枚举和元数据表。
- 汇编器支持注释、有符号整数操作数和符号标签。分支操作数是相对于下一条指令的有符号字节偏移量。
- 验证器会解码每一条指令,检查局部变量和调用目标,证明分支会落在指令边界上,检测控制流汇聚处的栈下溢和不一致的栈深度,并拒绝 fallthrough。
- `Program` 拥有带索引的 `Function`。每次调用都会创建一个帧,其中包含函数、返回 PC、栈基址和局部变量;参数从操作数栈中转移,而 `ret` 会恢复调用者的栈。
- `Value` 是一个带标签的值,包含 `null`、有符号 64 位整数和布尔变体。类型错误、格式错误的运行时状态、显式的 `throw_` 以及整数除以零都会变为带有函数名标注的 `VmException`。
- `Arena` 封装了 `std::pmr::monotonic_buffer_resource`:分配采用指针碰撞风格,特意去除了单独的释放操作,并且可以使用 `release()` 回收整个 VM 生命周期区域。运行时值目前可以直接内联存放;该 Arena 已为未来的堆对象模型做好准备。
## ISA
每条指令都以一个单字节的操作码开头。`u16` 和 `i32` 操作数采用小端序;没有操作数的指令占用一个字节宽度。
| 操作码 | 助记符 | 操作数 | 栈效果 | 语义 |
|---:|---|---|---:|---|
| 0 | `nop` | — | 0 | 空操作 |
| 1 | `const_i32` | i32 | +1 | 压入整数常量 |
| 2 | `load` | u16 | +1 | 压入局部变量 |
| 3 | `store` | u16 | -1 | 弹出至局部变量 |
| 4 | `add` | — | -1 | 整数加法 |
| 5 | `sub` | — | -1 | 整数减法 |
| 6 | `mul` | — | -1 | 整数乘法 |
| 7 | `div` | — | -1 | 带检查的整数除法 |
| 8 | `eq` | — | -1 | 压入带标签的相等结果 |
| 9 | `jump` | i32 | 0 | 相对无条件分支 |
| 10 | `jump_if` | i32 | -1 | 弹出条件并为真时分支 |
| 11 | `call` | u16 | 动态 | 通过表索引调用函数 |
| 12 | `ret` | — | 动态 | 将栈顶值返回给调用者 |
| 13 | `print` | — | -1 | 弹出并打印值 |
| 14 | `throw_` | — | -1 | 将值作为 VM 异常抛出 |
| 15 | `halt` | — | 0 | 停止程序 |
## 构建与测试
```
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build --output-on-failure
./build/vm_demo
```
## 示例程序
[`examples/sum_to_ten.vm`](examples/sum_to_ten.vm) 使用两个局部变量和符号分支标签计算 `1 + 2 + ... + 10`:
```
const_i32 0
store 0 ; sum = 0
const_i32 1
store 1 ; i = 1
loop:
load 0
load 1
add
store 0 ; sum += i
load 1
const_i32 1
add
store 1 ; ++i
load 1
const_i32 11
eq
jump_if done
jump loop
done:
load 0
ret ; returns 55
```
嵌入 API 负责汇编文本、声明局部变量的数量、在 `run()` 时自动验证程序,并选择分发策略:
```
std::string error;
vm::Bytecode code = vm::assemble(source, error);
vm::Program program{{{"sum_to_ten", 0, 2, std::move(code)}}, 0};
vm::RunResult result = vm::Executor(program).run(vm::Dispatch::Switch);
// result.value.as_int() == 55
```
## 分发基准测试
执行器提供了可移植的 `switch` 循环以及 GNU/Clang 的 computed-goto 直接线程化分发。两者都执行相同的已验证字节码并报告指令计数。运行多轮并取中位数:
```
./build/vm_benchmark 1000000 9
```
输出为 CSV 格式(`dispatch,median_ns,ns_per_instruction,result`),因此可以将结果记入报告或绘制成图。请在 `Release` 模式下构建,如果可能,将进程绑定到特定的 CPU,关闭繁杂的工作负载,并报告编译器、CPU、迭代次数和中位数。直接线程化消除了中央间接分支,但不能假定它一定会胜出:解码器开销、分支预测、编译器版本和工作负载组合都可能使 `switch` 更快。该基准测试旨在衡量这个问题,而不是预先决定其答案。
### 基准测试结果
Release 构建,GCC 11.4.0,AMD Ryzen 5 3500U (x86-64),1,000,000 次循环迭代,九轮测量;表格报告的是中位数。两种模式都执行了 9,000,003 条字节码指令并返回了 `1,000,000`。
| 分发方式 | 中位时间 | ns/instruction | 相对于 switch |
|---|---:|---:|---:|
| Switch | 174.061 ms | 19.340 | 1.00× |
| Direct-threaded | 164.286 ms | 18.254 | 0.944× |
对于此工作负载,直接线程化分发将中位运行时间减少了 5.6%。这些数据仅反映此机器和基准测试的情况,并不代表所有解释器;请使用上述命令来生成针对其他编译器或 CPU 的结果。
## 模糊测试
libFuzzer 目标通过解码器、反汇编器和验证器输入任意字节。使用 Clang 时:
```
cmake -S . -B build-fuzz -DCMAKE_CXX_COMPILER=clang++ -DVM_BUILD_FUZZER=ON
cmake --build build-fuzz -j
./build-fuzz/bytecode_fuzzer -max_total_time=60
```
该目标已启用 AddressSanitizer 和 UndefinedBehaviorSanitizer。
标签:Bash脚本, C++20, 字节码, 生成式AI安全, 编译器工具链, 虚拟机, 解释器, 逆向工具