pyhton interpreter byterun和底层code object
的简单了解
首先这是对于文章
这是一篇关于python虚拟机底层的一个文章
前部分是主要核心是500line那篇文章的笔记。
后部分主要是对于
byterun
的源码分析。
主要参考文档:
500 Lines or Less: python interpreter written in python-原文 、500 Lines or Less: python interpreter – 翻译、实验楼也有一个相关的教程、一个相关的ppt
这几个都是基于原文 讲的差不多的东西。
主要的研究对象:
另外还有一些文章,关注的位置就和我们这个byterun不同了,想进一步了解的朋友可以看下:
python interpreter
python interpreter
这里是两个点:
- 什么是python interpreter:
我们在这里提到的python解释器,是指python执行代码最后的一步
- python 程序运行:
在解释器接手之前。python会执行:词法分析,语法分析, 编译。
然后将python的源代码生成 code object
, 这里面就是python解释器可以识别的指令,然后解释器去解释(运行) 其中的指令。
另外Python的解释器是一个栈堆机器, 其底层使用的逻辑是用栈表示的,而我们普通的电脑底层是寄存器机器,使用的是数据存放在内存地址
这里说明:
- 编译 - compile
python尽管作为一个解释型语言,也存在编译的过程,只是相对于解释的部分占比更少一些。
- 解释器的定位
我们其实可以类比于c语言的过程,两者的源代码,先要形成底层机器可以识别的语言,这个
code object
就相当于汇编, 而c编译为了可执行文件运行在机器上,就相当于code object
运行在了我们的解释器上,其实个人感觉还是比较类似的过程,只不过解释器实现了一个类似虚拟机的的功能,也有点像java了,两者都是解释型语言。
其实这个所谓的解释器,和虚拟机,是一样的,只不过是两种语言的叫法不通,其定位是一致的。
同样的java虚拟机也是一个堆栈机器。
另外关于java虚拟机和python解释器, 在stack overflow中由一个 相关问题
stack 栈机器
我们的解释器是一个堆栈机器。而计算机是一个寄存器机器。
寄存器机器使用内存地址等保存数据, 运行时主要是使用寄存器去访问和操作各种地址,
堆栈机器的操作都是对堆栈的操作,而数据储存的问题上,pyhton就是在code object
的结构中,
这里简单讲一些指令实现思路,详细的可以看后面dis方法的使用,并获得一些结构或语句对比观察,或是翻看相关的详细讲解。
这里简单讲述一个顺序结构, 加法的实现:
python 对于一个加法的实现, 比如: 4 + 8
首先将 4 压栈, 然后将 8 压栈, 然后将栈顶的两位弹出、相加、再将结果压栈,最后将结果弹出:
上述就是个简单的循环结构的栈机器运行,当我们运行判断和循环结构,
了解过汇编我们知道,一般汇编中的大部分循环结构其实都是一个往回跳的判断语句,判断结构就是判断和往后跳过一个代码块的跳转语句,所以两者的关键在于判断和跳转的实现。
在指令里, 使用一种类似于汇编中的比较配合跳转的思路,COMPARE_OP
将栈顶两个数比较,并将对应的bool类型值压栈, POP_JUMP_IF_FALSE
, 讲栈顶的值弹出,由这个值的真假决定是否跳转。
这里我们讲述的这个栈称为数据栈(data stack)。顾名思义。
另外还有 一种块栈(block stack)。 用于特定的控制流,比如循环、异常处理。
另外还有一个每次运行唯一的调用栈,用于保存函数调用信息,详情查看 关于帧-frame的部分。
byte code
和 code object
在pyhton运行的时候,会将源代码转化为code object
, 其中的关键部分是 交由 解释器去运行的那一部分代码, 这就是byte code
, 其他的部分都是一些解释器必须的数据变量等部分 , 就比如我们前面提到的4和8,
这里特别提到这个byte code
是用Python语言转化为的、 python解释器可以运行的中间形态的代码,这类似于汇编和c的关系。
在另一个文档中看到对于byte code的解释:
the internal representation of python program in the interpreter .
python程序在解释器中的内部显示。
另外那些.pyc文件也是code object保存到硬盘中的表现,一般我们直接运行会编译然后运行,不会产生pyc文件,如果我们import 一个文件,则会将编译产生的code object保存,在下一次import就不需要重新编译,同时判定pyc和py文件是否一致是否需要重新编译,是通过检查时间戳。
关于import 的一些理解在这里可以看到:
而这个code object
的结构可以独立的运行在解释器上,已经包含了所有源代码中的数据代码变量名等,则类似于我们一个完整程序,可以独立的运行在我们的机器上,
python - dis模块:
首先我们看下在python中对应的上述两个概念:
这是ipython中的,但是截图里面的
__code__
会看不清
In [3]: def cond():
...: x = 1
...: y = 2
...: return x + y
...:
In [4]: cond.__code__
Out[4]: <code object cond at 0x7f037ebeb540, file "<ipython-input-3-27b19c0276da>", line 1>
In [5]: type(cond)
Out[5]: function
In [6]: type(cond.__code__)
Out[6]: code
In [7]: cond.__code__.co_code
Out[7]: b'd\x01}\x00d\x02}\x01|\x00|\x01\x17\x00S\x00'
In [8]: type(cond.__code__.co_code)
Out[8]: bytes
这里我们简述:
- 假设一个函数为
cond
- 其对应的
code object
, 是:cond.__code__
- 其对应的
byte code
, 是:cond.__code__.co__code
然后使我们的dis模块:
import dis
In [12]: dis.dis(cond.__code__)
2 0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (x)
3 4 LOAD_CONST 2 (2)
6 STORE_FAST 1 (y)
4 8 LOAD_FAST 0 (x)
10 LOAD_FAST 1 (y)
12 BINARY_ADD
14 RETURN_VALUE
In [13]: dis.dis(cond.__code__.co_code)
0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (0)
4 LOAD_CONST 2 (2)
6 STORE_FAST 1 (1)
8 LOAD_FAST 0 (0)
10 LOAD_FAST 1 (1)
12 BINARY_ADD
14 RETURN_VALUE
In [15]: dis.opname[100]
Out[15]: 'LOAD_CONST'
dis是一个字节码的反汇编器
-
dis.dis()
可以反汇编code object
, 将字节码和相关信息以一种人类可读方式输出,对于每个字节码做出解释。其中我们直接
dis.dis(cond)
和dis.dis(cond.__code__)
是一致的,但是dis.dis(cond.__code__.co_code)
, 就不会包含一些变量名等信息,这这位置我们在关于两结构时提到过,关于code object和byte code 的区别。 -
dis.opname()
可以返回指定的字节对应的字节码。
其实到这个位置我们就可以自己写一些语句来查看对应的字节码指令了。
frame 帧
我们已经可以大致的了解一个解释器对于函数得 运行,顺序的加减 条件的跳转循环等的实现,那么对于多个函数的调用, 我们要引入一个新的概念,帧, frame,
一个帧是一些信息集合和代码执行的上下文,frame在代码执行的时候可以动态的创建和销毁,每个frame都对应一次函数调用,并且只和对应的一个code object
关联,切拥有自己的数据栈和块栈。
这个概念的确是很像我们的汇编中栈帧的定位,但是这个结构自身的性质也类似于堆的创建的感觉。
另外帧存在于程序内的调用栈(call stack)中。每次函数内调用一次函数就会在当前调用栈上压入所调用的函数的帧,在所调用函数返回后将该帧弹出。
在我们的python 程序运行报错时出现的
Traceback (most recent call last)
就是在回溯调用栈的frame确定的错误位置。
另外函数返回值的传递,
当函数返回时,指令为RETURN_VALUE
, 这个指令是将调用栈栈顶的frame的数据栈栈顶值弹出,然后讲frame弹出栈并丢弃整个frame, 然后讲这个值压入下一个新调用栈栈顶的frame数据栈中。
最后简图:
byterun
下面开始这个源码分析,在文初已经给出了github链接,直接clone可以。
主要的代码部分在byterun文件夹中,
文件结构:
其中的虚拟机在文件pyvm2.py
, 使用到的对象定义在pyobj.py
, 这是最主要的两个文件。
顺序分析
main
在__init__.py
中没有任何东西,我们直接看下__mian__.py
文件:
import argparse
import logging
from . import execfile
....
if args.module:
run_fn = execfile.run_python_module
else:
run_fn = execfile.run_python_file
level = logging.DEBUG if args.verbose else logging.WARNING
logging.basicConfig(level=level)
argv = [args.prog]