翻译自《Python Virtual Machine》
Python 虚拟机
每个函数对象都和以下的三个结构:
1。包含参数的局部变量名称(in .__code__.varnames)
2。全局变量名称(in .__code__.co_names)
3。常数(in .__code__.co_consts)
在python定义函数的时候创建这些结构,它们被定义在函数对应的__code__对象。
如果我们定义如下:
Def minimum(alist):
m=None if let(alist) ==0 else Alist[0]
for v in alist[1:]:
if vim:
m = v
return m
我们得到
minimum.__code__.co_varnames is ('alist','m','v')
minimum.__code__.co_names is ('len','None')
minimum.__code__.co_consts is (None,0,1)
用于索引的数字+load 运算符(LOAD_FAST、LOAD_GLOBAL、LOAD_CONST都会在之后讨论)。
在PVM中主要的数据结构式“regular”栈(由一连串的push、pop组成)。对栈的主要操作就是load/push和store/pop。我们在栈顶load/push一个值,栈向上扩展,伴随着栈指针上移指向栈顶。同样,store/pop一个栈顶值时,栈指针下移。
还有一个次要的block栈用于从循环嵌套、try和指令中取出值。比如,一个断点指令在block栈中被编码,用于判断哪个循环块被n断下(并如何继续执行循环外的指令)。当循环,try/except和指令开始执行时,他们的信息被push到block栈上;当它们结束时从堆栈上弹出。这种块block stack对于现在来说太过麻烦,不必要去理解:所以当我们遇到有关block stack 的指令时,会指出将其忽略的原因。
这儿有个有关栈操作的简单例子,计算 d=a+b*c。假设a、b、c、d都是一个函数中的局部变量:co_varnames =('a','b','c','d')且这些符号对应的实际值被存放在并行元组中:(1,2,3,none)。符号在元组中的位置与其值在元组的位置是一一对应的。
LOAD_FAST N
load/push 将co_varnames[N]对应的值压入栈,stackp+=1,stack[stackp] = co_varnames[N]
STORE_FAST N
store/pop 将栈顶的值放入co_varnames[N], co_varnames[N] = stack[stackp], stackp-=1
BINARY_MULTIPLY
将‘*’的两个运算数压入栈,stack[stackp-1]=stack[stackp-1]*stack[stack];stackp-=1(将栈顶的两个值转化为它们的乘积)
BINARY_ADD
将‘+’的两个运算数压入栈,stack[stackp-1]=stack[stackp-1]+stack[stack];stackp-=1(将栈顶的两个值转化为它们的和)
d = a+b*c 的PVM code:
LOAD_FAST 0
LOAD_FAST 1
LOAD_FAST 2
BINARY_MULTIPLY
BINARY_ADD
STORE_FAST 3
初始状态:
co_varnames =('a','b','c','d')
values=(1,2,3,none)
+--------------------+
3 | |
+--------------------+
2 | |
+--------------------+
1 | |
+--------------------+
0 | |
+--------------------+
stack (with stackp=-1,it is an empty stack)
LOAD_FAST 0:
+--------------------+
3 | |
+--------------------+
2 | |
+--------------------+
1 | |
+--------------------+
0 | 1: value of a |
+--------------------+
stack(with stackp=0)
LOAD_FAST 1:
+--------------------+
3 | |
+--------------------+
2 | |
+--------------------+
1 | 2: value of b |
+--------------------+
0 | 1: value of a |
+--------------------+
stack (with stackp=1)
LOAD_FAST 2:
+--------------------+
3 | |
+--------------------+
2 | 3: value of c |
+--------------------+
1 | 2: value of b |
+--------------------+
0 | 1: value of a |
+--------------------+
stack (with stackp=2)
BINARY_MULTIPLY:
+--------------------+
3 | |
+--------------------+
2 | |
+--------------------+
1 | 6: value of b*c |
+--------------------+
0 | 1: value of a |
+--------------------+
stack (with stackp=1)
BINARY_ADD:
+--------------------+
3 | |
+--------------------+
2 | |
+--------------------+
1 | |
+--------------------+
0 | 7: value of a+b*c |
+--------------------+
stack (with stackp=0)
STORE_FAST 3:
+--------------------+
3 | |
+--------------------+
2 | |
+--------------------+
1 | |
+--------------------+
0 | |
+--------------------+
stack (with stackp=-1)
co_varnames =('a','b','c','d')
values=(1,2,3,7)
PVM的控制流
在PVM的每个指令都包含了1~3字节的信息。第一个字节是操作标识或字节码,后面的两字节是字节码的操作数(但并不是所有的字节码都需要操作数:BINARY_ADD就不需要)。两字节能够表示0~65536:所以python的函数中不能有超过65536个不同的局部变量。
指令被储存在内存中:把内存也看作一种储存有次序的数据的列表结构。
Memory Instruction
Location
0 LOAD_FAST 0
3 LOAD_FAST 1
6 LOAD_FAST 2
9 BINARY_MULTIPLY
10 BINARY_ADD
11 STORE_FAST 3
把内存列表命名为m
第一条指令被存储在m[0],后一指令存储在高3或高1的位置处(占3字节:有些指令有明确操作数的:load/store。有些指令有隐含的操作数:stack 、pc。占1字节:没有操作数的指令:binary运算)
一旦这些指令被加载进内存后,PVM按照一个简单的规则执行他们。执行周期赋予了计算机生命,这是计算机科学的基础。
(1)从m [pc]开始获取操作及其操作数(如果存在)
(2)pc + = 3(如果操作数存在)或pc + = 1(如果没有操作数存在)
(3)执行操作码(可能更改其操作数,堆栈,堆栈或pc)
(4)转到步骤1
一些运算会操作stack/stackp和存变量值的元组,一些会改变pc(比如jump指令)。
所以pc初始时0,PVM执行上述代码以以下流程:
1.获取操作m [0],操作数m [1]和m [2]
2.将pc递增至3
3.操纵堆栈(见上文)
4.回到步骤1
1.取m [3]的操作,m [4]和m [5]
2.将pc增加到6
3.操纵堆栈(见上文)
4.回到步骤1
1.取m [6]和m [7]和m [8]的操作数,
2.将pc增加到9
3.操纵堆栈(见上文)
4.回到步骤1
1.获取操作a m [9]:它没有操作数
2.将pc增加到10
3.操纵堆栈(见上文)
4.回到步骤1
1.获取操作m [10]:它没有操作数
2.将pc增加到11
3.操纵堆栈(见上文)
4.回到步骤1
内存中指向此处时,没有代码可以执行。在下一个例子中我们可以看到PVM如何执行一个更复杂的代码。
如简要介绍的那样,我们可以用dis.py模块中使用dis函数打印任何Python函数(和模块/类也可以)的注释描述;这里我们打印函数。
def addup(alist): sum=0 for v in alist: sum = sum + v return sum
这个例子用来显示一般函数对象的有用的信息(它的名称,它的三个元组,和反编译信息)
def func_obj(fo): print(fo.__name__) print(' co_varnames:',fo.__code__.co_varnames) print(' co_names :',fo.__code__.co_names) print(' co_consts :',fo.__code__.co_consts,' ') print('Source Line m operation/byte-code operand (useful name/number) '+69*'-') dis.dis(fo) calling func_obj(addup) prints addup co_varnames: ('alist', 'sum', 'v') co_names : () co_consts : (None, 0) Source Line m op/byte-code operand (useful name/number) --------------------------------------------------------------------- 2 0 LOAD_CONST 1 (0) 3 STORE_FAST 1 (sum) 3 6 SETUP_LOOP 24 (to 33) 9 LOAD_FAST 0 (alist) 12 GET_ITER >> 13 FOR_ITER 16 (to 32) 16 STORE_FAST 2 (v) 4 19 LOAD_FAST 1 (sum) 22 LOAD_FAST 2 (v) 25 BINARY_ADD 26 STORE_FAST 1 (sum) 29 JUMP_ABSOLUTE 13 >> 32 POP_BLOCK 5 >> 33 LOAD_FAST 1 (sum) 36 RETURN_VALUE
有>>标识的行说明有其他指令会jump到此行。
更详细的描述:
第2行:
m [0]:在堆栈上加载值0(co_consts [1])
m [3]:将值0存入sum(co_varnames [1])
第3行:
m [6]:通过将循环块的大小压入栈来设置循环
m [9]:从栈中加载alist(co_varnames [0])的值
m [12]:通过迭代器替换堆栈上的值(通过弹出和推送)
m [13]:在堆栈中加载下一个迭代器值,如果StopIteration引起,则跳转到m [32]
(m [29]中的代码跳回此位置进行循环)
m [16]:将下一个值存储到v(co_varnames [2])中,将其从堆栈中弹出
第4行:
m [19]:在栈中加载sum(co_varnames [1])的值
m [22]:将v(co_varnames [2])的值加载到栈上
m [25]:将栈顶的两个值进行相加操作后,将结果加载到栈上
m [26]:栈顶弹出值,存储在sum(co_varnames [1])中
m [29]:将pc设置为13,因此在m [13]中执行的下一条指令
(跳回到前一个位置使循环循环)
m [32]:弹出m[6]对循环块的设置而压入栈的值
(m [13]中的代码在这里跳转到StopIteration,终止循环)
第5行:
m [33]:将sum(co_varnames [1])的值压入栈,用于返回
m [36]:从函数返回结果在堆栈顶部