一、虚拟机设计方案
1. 架构风格
Python虚拟机实际上是一个解释器,对编译后的字节码进行解释、执行。因此解释器风格显然是最适合本项目的。
2. 分解视图
虚拟机输入为字节码.pyc文件,由字节码文件加载器将二进制的.pyc文件加载到内存,由执行引擎解释执行,输出为字节码文件的执行结果。虚拟机总共分为以下四个模块:
- 字节码加载器:python字节码是按固定格式存放的二进制文件,而在虚拟机进程中,字节码对象是codeobject类型,加载器的作用就是在类型体系模块的支持下,将固定格式的二进制文件解析成内存中的codeobject类型对象,然后将codeobject交由执行引擎运行。
- 执行引擎:本质是虚拟机的解释器部分,本次虚拟机设计为栈式执行,所以解释器维护一个操作栈,字节码所有的操作都在栈上执行。解释器主要运行在一个很大switch分支循环中,每个分支分别为对应字节码的解释,直到字节码执行结束跳出循环结束虚拟机。解释器还维护一个个虚拟机操作数栈帧frame,每个frame对应一个函数或方法的调用,且第一个frame为第一个codeobject。每个frame中有一个指向调用者frame的指针、当前frame字节码位置的pc、对应的codeobjcet等。在解释器中,在要操作栈上产生的类型对象都在堆中产生,依赖于堆区的内存管理,虚拟机可以不用管理对象的析构,而交由堆区自动垃圾回收。
- 类型体系:在python中,一切都是对象,虚拟机有着一个庞大的类型体系。这个模块也是最基本最重要的模块,大致上有Integer、Float、String、List、Set、Dict、Tuple这7个基本类型,还有运行时的codeobject、frame,其他的内建类型type、function、method、slice、iterator、module等。还有一个特殊的类型object,它是虚拟机中大部分类型的父类。
- 堆区内存管理:管理类型对象的关键,对象的分配权力交给了堆,在堆中使用自动内存管理算法,如标记-清除、标记-复制等算法。
3. 依赖视图
依赖视图展现了软件模块之间的依赖关系。
各个模块之间的依赖关系如下所示:
字节码加载器部分分为两个类,他们之间不存在依赖关系:
Type类存放了对象的类型信息和对应的方法,Object类依赖于Type类,其他的基本类型都继承自Object类:
4. 执行视图
执行视图展示了系统运行时的时序结构特点。
5. 实现视图
实现视图是描述软件架构与源文件之间的映射关系。
虚拟机源代码目录的结构如下所示:
pvm/
├── bin
├── include
│ ├── Code.h
│ ├── Dict.h
│ ├── Float.h
│ ├── GlobalVar.h
│ ├── Integer.h
│ ├── List.h
│ ├── Object.h
│ ├── OpCode.h
│ ├── Str.h
│ ├── Type.h
│ └── global.h
├── main.c
├── objects
│ ├── Code.c
│ ├── Dict.c
│ ├── Float.c
│ ├── Integer.c
│ ├── List.c
│ ├── Object.c
│ └── Str.c
├── runtime
└── utils
├── FileLoader.c
├── FileLoader.h
├── FileReader.c
├── FileReader.h
├── Log.c
└── Log.h
二、核心数据结构
Python2字节码文件结构
|
Magic number (4B) |
Modified date (4B) |
‘c‘ (1B,表示接下来是CodeObject) |
|
Argcount (4B) |
Nlocals (4B) |
Stacksize (4B) |
Flags (4B) |
|
字节码 |
‘s‘ (1B) |
字节码长度 (4B) |
字节码1,字节码2 … |
|
常量表 |
‘(‘ (1B) |
常量个数 (4B) |
常量1,常量2 … |
|
变量表 |
‘(‘ (1B) |
变量个数 (4B) |
变量1,变量2 … |
|
参数列表 |
‘(‘ (1B) |
|
|
|
Cell var |
‘(‘ (1B) |
|
|
|
Free var |
‘(‘ (1B) |
|
|
|
文件名 |
‘s‘ (1B) |
名字长度 (4B) |
|
|
模块名 |
‘t‘ (1B) |
模块名长度 (4B) |
|
|
行号表(调试) |
‘s‘ (1B) |
|
|
三、运行环境和技术选型
Python是跨平台语言,其生成的字节码文件在不同平台上具有相同的结构,因此虚拟机的运行环境可以是Windows,Linux,macOS。虚拟机的开发语言定为C语言。
四、核心工作机制
以运行一个内容为 “print(1 + 2 * 3)” 的python源文件为例,假设源文件经过编译生成的字节码是a.pyc,要执行这个文件,需要先读取a.pyc中的字节码列表和常量表。a.pyc的字节码列表中有LOAD_CONST、BINARY_ADD、PRINT_ITEM、PRINT_NEWLINE等指令,常量表中有常量1和6,我们根据字节码列表中的指令,在操作数栈上进行对应操作:指令为LOAD_CONST时从常量表取出数字放进栈里面,指令为BINARY_ADD时就从栈中取出数字相加,指令为PRINT_ITEM就打印计算结果,这样就完成了对a.pyc的运行。