本项目基于jsvmp启发,旨在实现一个轻量级的Python虚拟机。jsvmp是一个JavaScript虚拟机保护方案,主要用于保护JavaScript代码不被逆向工程和篡改。
web端通用的保护方案为将js代码编译为自定义字节码,并同时利用js实现了一个jsvmp解释器,从而达到了在v8等js引擎上运行受保护的javascript功能。
PythonVMP同样采用了类似的思路,旨在提供一个轻量级的Python虚拟机实现。
PythonVMP是一个Python版的Python虚拟机与编译器实现,所有功能都使用Python标准库实现
PyVM实现了从源代码到字节码的完整编译和执行流程,支持将Python语法编译为自定义指令集(注意,PyVM不是Python,仅实现了Python语言子集),可以执行自定义字节码,同时还具备有限的性能监控和函数调用支持。
支持将编译后的字节码保存为二进制文件和从二进制文件加载
以下是一个简单程序的Python程序:
# 源代码
x = 10
y = 20
print(x + y)
拉取项目后,我们使用如下方式编译程序:
python src/main.py <源文件.py> --compile --debug
编译后会生成一个二进制文件,该二进制文件实为我们自实现的python程序字节码编译结果
// 二进制文件内容
50 59 4D 56 01 00 00 00 03 00 00 00 02 00 00 00 0E 00 00 00
01 04 00 00 00 0A 00 00 00 01 04 00 00 00 14 00 00 00 03 06
00 00 00 70 72 69 6E 74 00 01 01 00 78 00 00 00 00 01 01 00
79 01 00 00 00 01 00 03 00 01 01 03 01 02 00 02 01 10 40 FF
可以看到,原始28个字符的python代码被编译为240+字节的二进制文件,且该二进制文件无法由Python解释器直接执行
程序会由compiler/
下的编译器模块,按照如下路径将原始python文件编译为字节码,
源代码 → 词法分析器 → 语法分析器 → 代码生成器 → 字节码
- 词法分析器 : 用于将Python代码切分为有意义的最小单元(Token),如关键字、标识符、数字、运算符等。该过程为后续语法分析提供结构化输入
- 语法分析器 : 用于构建抽象语法树(AST),根据预定义的语法规则(BNF/EBNF),将Token序列解析为抽象语法树(AST)。AST节点表示程序结构(如赋值、运算、条件、循环等),便于后续代码生成阶段遍历和处理
- 代码生成器 : 用于将AST转换为字节码,生成自定义字节码指令。代码生成器遍历上一步生成的AST节点,根据节点类型生成对应的虚拟机指令序列,可以将将高级语法映射为底层指令(如LOAD_CONST、STORE_VAR等),最终输出可供虚拟机执行的字节码
我们编译出的字节码无法由cpython解释器执行,仅能由我们自己实现的解释器运行,对于外部安全人员来说,未掌握可执行文件格式、指令集、解释器的前提下,无法还原出原始算法
那么如何执行这段字节码呢?参考elf的文件格式,我们实现了一个自定义的虚拟机可执行文件格式,包含文件头、常量池、符号表和指令序列。依据这个格式,我们可以解析二进制文件并找到其中的程序代码。
+------------------+
| 文件头(Header) |
+------------------+
| 常量池(Constants) |
+------------------+
| 符号表(Symbols) |
+------------------+
| 指令序列(Code) |
+------------------+
我们将文件头、常量池、符号表进行解析,这样就得出了整个文件的详细格式:
文件头:
50 59 4D 56 # 魔数 "PYMV"
01 00 # 版本 1.0
00 00 # 标志位
03 00 00 00 # 常量数量: 3
02 00 00 00 # 符号数量: 2
0E 00 00 00 # 代码大小: 14字节
常量池:
# 常量0: 整数10
01 04 00 00 00 0A 00 00 00
# 常量1: 整数20
01 04 00 00 00 14 00 00 00
符号表:
# 符号0: 变量x
01 01 00 78 00 00 00 00
# 符号1: 变量y
01 01 00 79 01 00 00 00
指令序列:
01 00 03 00 01 01 03 01 02 00 02 01 10 40 FF
其中的指令序列就是我们要执行的代码,包含了程序的完整逻辑
01 00 03 00 01 01 03 01 02 00 02 01 10 40 FF
然而,仅仅按照这个可执行文件格式进行解析是不够的,我们还需要按照指令集定义来解析指令序列。
指令序列:
01 00 # LOAD_CONST 0 (加载10)
03 00 # STORE_VAR 0 (存储到x)
01 01 # LOAD_CONST 1 (加载20)
03 01 # STORE_VAR 1 (存储到y)
02 00 # LOAD_VAR 0 (加载x)
02 01 # LOAD_VAR 1 (加载y)
10 # ADD (相加)
40 # PRINT (打印结果)
FF # HALT (结束程序)
解析出的指令序列会被推入我们的解释器,PyVM实现了一个在Python上运行的栈式虚拟机,根据指令集定义,逐条解析指令并执行。
执行以下命令来运行编译后的二进制文件:
python src/main.py <二进制文件.pvm> [--debug]
将文件加载入虚拟机执行后最终输出结果如下
30
栈式虚拟机只有一个栈和一堆字节码,如何实现的数学运算和逻辑处理呢?我们虚拟机的初始状态下
程序计数器(PC): 0
操作栈: [](空栈)
常量表: [10, 20]
随后执行指令序列时,程序计数器(PC)会逐条执行指令,操作栈会根据指令进行相应的操作。
步骤 | 指令 | 操作说明 | 栈状态 | 变量表 | 输出 |
---|---|---|---|---|---|
1 | LOAD_CONST 0 | 将常量表索引0的值(10)压入栈 | [10] | ||
2 | STORE_VAR 0 | 弹出栈顶值(10),存储到变量表索引0 | [] | [10] | |
3 | LOAD_CONST 1 | 将常量表索引1的值(20)压入栈 | [20] | ||
4 | STORE_VAR 1 | 弹出栈顶值(20),存储到变量表索引1 | [] | [10, 20] | |
5 | LOAD_VAR 0 | 将变量表索引0的值(10)压入栈 | [10] | ||
6 | LOAD_VAR 1 | 将变量表索引1的值(20)压入栈 | [10, 20] | ||
7 | ADD | 弹出栈顶两个值(20, 10),相加后压入结果 | [30] | ||
计算: 10 + 20 = 30 | |||||
8 | 弹出栈顶值(30)并打印 | [] | 30 |
栈式虚拟机使用操作栈来存储中间值,注意其中的第5、6、7步的操作指令和栈变化,可以看出a+b可以分解为弹出栈顶两个元素,进行加法操作后再将结果压入栈中。到程序结束时,栈为空,输出结果为30
在vmp混淆的代码流程中,防护者通常会实现一个虚拟机来解释和执行这些指令集,这种工作类似于将高级语言编译为汇编的过程,因此,除非得知指令集与虚拟机结构,对混淆代直接人肉分析几乎是不可能的
# 编译并运行Python代码
python src/main.py examples/hello.py
# 运行高级算法演示
python src/main.py examples/algorithms_demo.py
# 查看性能报告
python src/main.py examples/performance_suite.py --performance
# 查看字节码
python src/main.py examples/hello.py --show-bytecode
# 调试模式运行
python src/main.py examples/fibonacci.py --debug
# 编译源文件为二进制
python src/main.py <源文件.py> --compile [--debug]
# 执行二进制文件
python src/main.py <二进制文件.pvm> [--debug]
# 查看文件信息
python src/main.py <二进制文件.pvm> --info
# 查看字节码反汇编
python src/main.py <二进制文件.pvm> --show-bytecode
- 词法分析器 (
lexer.py
): 将源代码分解为token,支持Python基本token类型,完整错误处理 - 语法分析器 (
parser.py
): 构建抽象语法树(AST),支持表达式、语句、控制流 - 代码生成器 (
codegen.py
): 将AST转换为字节码,生成自定义字节码指令,优化常量池
- 指令集 (
instructions.py
): 40+条虚拟机指令,包括栈操作、算术、比较、控制流、函数调用 - 栈管理 (
stack.py
): VMStack和CallStack实现,支持深度函数调用 - 执行引擎 (
machine.py
): 指令分派和执行,性能监控集成
- 性能监控系统: 实时指令使用频率统计、执行时间追踪、性能分析工具
- 函数调用栈: 完整的CALL/RETURN指令实现和调用栈管理
- 开发工具: 命令行界面、字节码反汇编、调试模式、性能分析器
- 字节码比较: 不同实现的性能和效率对比工具
- 变量赋值和算术运算(+, -, *, /, %)
- 比较运算(>, <, >=, <=, ==, !=)
- 性能监控系统: 实时指令使用频率统计、执行时间追踪、性能分析工具
- 函数调用栈: 完整的CALL/RETURN指令实现和调用栈管理
- 开发工具: 命令行界面、字节码反汇编、调试模式、性能分析器
- 字节码比较: 不同实现的性能和效率对比工具
- 变量赋值和算术运算(+, -, *, /, %)
- 比较运算(>, <, >=, <=, ==, !=)
- 条件语句(if/else)
- 循环语句(while)
- 函数调用(目前仅实现输入输出函数:print, input)
- 字符串和数字字面量
- 基于栈的架构: 高效的栈操作和管理
- 函数调用支持: 完整的CALL/RETURN机制
- 性能监控: 实时统计和分析功能
- 错误处理: 详细的错误信息和恢复机制
项目包含完整的测试套件:
- 18个单元测试(10个编译器 + 8个虚拟机)全部通过
- 14个示例程序正常运行
- 涵盖基础功能、算法演示、性能测试、错误处理等方面
你可以在命令行中进入 tests 目录,然后运行以下命令来执行所有 Python 单元测试:
cd d:\project\临时测试\vm\tests
python -m unittest
如果只想运行某一个测试文件,比如 test_compiler.py,可以使用:
python -m unittest test_compiler.py
如果你想看到更详细的输出,可以加上 -v
参数:
python -m unittest -v
hello.py
- Hello World程序calculator.py
- 简单计算器variables.py
- 变量操作演示
algorithms_demo.py
- 冒泡排序、阶乘、最大公约数fibonacci.py
- 斐波那契数列计算loop.py
- 各种循环结构
performance_suite.py
- 综合性能测试套件benchmark.py
- 基准测试程序comprehensive_performance.py
- 复杂性能测试
advanced_error_handling.py
- 高级错误处理示例error_test.py
- 错误边界测试
运行综合性能测试:
python src/main.py examples/comprehensive_performance.py --performance
详细的指令集和字节码格式说明请参阅 docs/
目录下的文档:
instruction_set.md
- 完整的指令集参考bytecode_format.md
- 字节码格式规范
- ✅ 完整的编译流水线
- ✅ 栈式虚拟机执行引擎
- ✅ 函数调用和栈管理
- ✅ 性能监控和分析
- ✅ 18个单元测试
- ✅ 14个示例程序
- 数据类型扩展:数组、字典、列表支持
- 异常处理:try/catch语法支持
- 模块系统:import语句和模块管理
- 编译优化:死代码消除、常量折叠
- 并发支持:协程、多线程基础
- 确保所有测试通过
- 添加新功能时请同时添加相应测试
- 更新文档以反映代码变更
- 遵循现有的代码风格和架构模式