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安全, 编译器工具链, 虚拟机, 解释器, 逆向工具