Python字节码

绝对清晰,是风格上唯一的美。既然是美的东西,我们总是要追求的,如果不熟悉Python虚拟机中执行的字节码,那么又怎么能被称作是绝对清晰呢?

首先是最简单的简单内建对象的创建。看下面四句语句。

[simple_obj.py]
i = 1
s = "Python"
d = {}
l = []
这个文件被编译后的对应的PyCodeObject对象的co_names和co_consts的xml结构如下
<consts>
    <int value="1" />
    <internStr index="0" length="6" value="Python" />
    <NoneObject />
</consts>
<names>
    <internStr index="1" length="1" value="i" />
    <internStr index="2" length="1" value="s" />
    <internStr index="3" length="1" value="d" />
    <internStr index="4" length="1" value="l" />
</names>
co_names存放了这段代码的所有局部变量名,co_consts存放了这段代码中的所有静态变量 Python解释器执行字节码的入口函数就是PyEval_EvalFrameEx()。这个函数中定义了大量的宏
GETITEM(v, i)   // 从tuple中获取元素
STACKADJ(n)     // 调整栈顶指针的位置
PUSH(v)         // 入栈
POP()           // 出栈
i = 1 对应的字节码如下
0 LOAD_CONST    0   (1)     // 加载偏移量为0的常量
3 STORE_NAME    0   (i)     // 将加载的值与偏移量为0的变量名形成约束,并存入local命名空间。

s = "Python" 对应的字节码如下
6 LOAD_CONST    1   ("Python")
9 STORE_NAME    1   (s)

d = {} 对应的字节码如下
12 BUILD_MAP    0
15 STORE_NAME   2   (d)

l = []
18 BUILD_LIST   0
21 STORE_NAME   3   (l)
24 LOAD_CONST   2   (none)  // 24和27的语句表示python执行完一段codeblock后必须有返回值,默认返回None。
27 RETURN_VALUE

而LOAD_CONST的实现可以当作下面的语句
x = GETITEM(consts, oparg); // 从consts中获取对应偏移量的元素值
Py_INCREF(x);               // 增加x变量的引用计数
PUSH(x);                    // 入栈

BUILD_MAP的实现如下
x = PyDict_New();
PUSH(x);

RETURN_VALUE的实现如下
retval = POP();
why = WHY_RETURN;       // why在Python虚拟机的消息循环中作一个标志,异常机制就是用它实现的
[adv_obj.py]
i = 1
s = "Python"
d = {"1": 1, "2": 2}
l = [1, 2]

adv_obj.py对应的PyCodeObject的names和consts的xml结构如下
<consts>
    <int value="1" />
    <internStr index="0" length="6" value="Python" />
    <internStr index="1" length="1" value="1" />
    <int value="2" />
    <internStr index="2" length="1" value="2" />
    <NoneObject />
</consts>
<names>
    <internStr index="3" length="1" value="i" />
    <internStr index="4" length="1" value="s" />
    <internStr index="5" length="1" value="d" />
    <internStr index="6" length="1" value="l" />
</names>
这里consts中只有一个<int value="1">是因为Python对整数的处理也有一套类似雨字符串的internal机制。

d = {"1": 1}
12 BUILD_MAP    0
15 DUP_TOP                  // 增加了map的引用计数,将map再一次压入栈中
16 LOAD_CONST   0   (1)
19 ROT_TWO                  // 将栈顶元素和第二个元素交换位置
20 LOAD_CONST   2   ("1")
23 STORE_SUBSCR             // 将("1": 1)这个item插入到上面的map中,并将栈顶指针上移三个
24 DUP_TOP
25 LOAD_CONST   3   (2)
28 ROT_TWO
29 LOAD_CONST   4   ("2")
32 STORE_SUBSCR
33 STORE_NAME   2   (d)     // 将上面的map与偏移量为2的变量名(d)生成约束,存入local空间中

[DUP_TOP]       // 注意 DUP_TOP不仅仅增加了栈顶元素的引用计数,还将栈顶元素再一次压入栈中
v = TOP();
Py_INCREF(v);
PUSH(v);

[ROT_TWO]
v = TOP()
w = SECOND();
SET_TOP(w);
SET_SECOND(v);

[STORE_SUBSCR]
w = TOP();
v = SECOND();
u = THIRD();
STACKADJ(-3);
PyObject_SetItem(v, w, u);
Py_DECREF(u);
Py_DECREF(v);
Py_DECREF(w);

l = [1, 2]
36 LOAD_CONST   0   (1)
39 LOAD_CONST   3   (2)
42 BUILD_LIST   2           // 创建list,并将前两个元素填入list。
45 STORE_NAME   3   (1)

list的创建比较有意思,他会先把list中的元素压入运行时栈,然后通过BUILD_LIST传贱list对象,并把前面的对象(参数为元素的数量)填入list中。
[normal.py]
a = 5
b = a
c = a + b
print c

<consts>
    <int value="5" />
    <NoneObject />
</consts>
<names>
    <internStr index="0" length="1" value="a" />
    <internStr index="1" length="1" value="b" />
    <internStr index="2" length="1" value="c" />
</names>

b = a
0 LOAD_NAME     0   (a)     // 会按顺序从local, globals, builtin三个命名空间中去寻找a
3 STORE_NAME    1   (b)

c = a + b
12 LOAD_NAME    0   (a)
15 LOAD_NAME    1   (b)
18 BINARY_ADD               // 这条指令,表示将上面两个数相加,这里又涉及到加法的快速通道(int和str专用)和慢速通道
19 STORE_NAME   2   (c)

print c
22 LOAD_NAME    2   (c)
25 PRINT_ITEM  
26 PRINT_NEWLINE

[PRINT_ITEM_TO]     // 这条语句主要是用于输出重定向
w = stream = POP();
[if_control.py]
a = 1
if a > 10:
    print "a > 10"
elif a <= -2:="" print="" "a="" <="-2"" elif="" a="" !="1:" 1:="" 1"="" else:="" "unknown="" a"="" if=""> 10:
6 LOAD_NAME     0   (a)
9 LOAD_CONST    1   (10)
12  COMPARE_OP  4   (>)         // 这个字节码为int对象实现了快速通道
15  JUMP_IF_FALSE   9   (to 27)
18  POP_TOP
    print "a > -1"
19  LOAD_CONST  2   ("a>-1")
22  PRINT_ITEM
23  PRINT_NEWLINE
24  JUMP_FORWARD    72  (to 99)
27  POP_TOP
elif a <= 0="" 1="" 3="" 9="" 28="" 31="" 34="" 37="" 40="" -2:="" load_name="" (a)="" load_const="" (-1)="" compare_op="" (<=")" jump_if_false="" (to="" 49)="" pop_top="" 其实大部分判断语句的结构都是相同的="" -="">  LOAD_CONST  ->  COMPARE_OP  ->  JUMP_*
从local命名空间中获得变量名a所对应的值,从常量表consts中读取参与该分支判断操作的常量对象,进行比较获取得结果true/false,根据结果进行指令跳转。
python中的判断语句除了比较语句,还包括了is, in, not in, is not,(还有一个叫PyCmp_EXC_MATCH的东西,好像是和异常处理有关。。。)

list = [1, 2, 3, 4]
if l in list:
LOAD_CONST      0   (1)
LOAD_NAME       0   (list)
COMPARE_OP      6   (in)
JUMP_IF_FALSE
POP_TOP         // 将比较结果移出栈顶

python的比较结果只有两个Py_False和Py_True,底层的实现其实就是0和1。
在Python中,有一些字节码通常都是成对顺序出现的,所以python中还有一个指令预测功能,预测功能通常会用到下面几个宏。
PREDICT(op)
PREDICTED(op)
PREDICTED_WITH_ARG(op)
PEEKARG()

比如PREDICT(JUMP_IF_FALSE)的意思就是检查下一条待处理的字节码是否是JUMP_IF_FALSE。如果是,则程序的流程会跳转到PRED_JUMP_IF_FALSE标志符对应的代码处。
这主要是通过goto语句实现的,也就是说,如果下一句语句是JUMP_IF_FALSE的话,JUMP_IF_FALSE这句字节码就不用再次顺序地进入Python虚拟机的消息循环了,直接通过goto语句跳到case JUMP_IF_FALSE:这个地方,并将指向下一条语句的指针向后移一位。相当于以更快速的方式执行了JUMP_IF_FALSE。

将COMPARE_OP中实现的PREDICT宏展开如下:
[COMPARE_OP]
...
if(*next_instr == JUMP_IF_FALSE)
    goto PRED_JUMP_IF_FALSE;
if(*next_instr == JUMP_IF_TRUE)
    goto PRED_JUMP_IF_TRUE;

JUMPBY(x)   表示下一条字节码的指针移动x个字节。

[for_control.py]
lst = [1, 2]
0   LOAD_CONST  0   (1)
3   LOAD_CONST  1   (2)
6   BUILD_LIST  2
9   STORE_NAME  0   (lst)
for i in lst:
12  SETUP_LOOP  19  (to 34)
15  LOAD_NAME   0   (lst)
18  GET_ITER                    // 获取迭代器
19  FOR_ITER    11  (to 33)
22  STORE_NAME  1   (i)
    print i
25  LOAD_NAME   1   (i)
28  PRINT_ITEM
29  PRINT_NEWLINE   
30  JUMP_ABSOLUTE   19          // 跳转到绝对地址19
33  POP_BLOCK                   // 跳出PyTryBlock,相当于是将PyFrameObject中的f_blockstack数组的当前index向前移动一个,这样简单的方式不对称但是很简单。
34  LOAD_CONST  2   (None)
37  RETURN_VALUE

之前我们谈到过PyFrameObject对象中有一个成员变量叫f_blockstack。
typedef struct _frame   {
    ...
    int f_iblock;
    PyTryBlock  f_blockstack[CO_MAXBLOCKS];
    ...
} PyFrameObject;

typedef struct {
    int b_type;     // block类型
    int b_handler;  // where to jump to find handler
    int b_level;    // value stack level to pop to
} PyTryBlock;
剧透一下PyTryBlock一般用于异常处理和循环O(^_^)O

1/0
#   LOAD_CONST  0
#   LOAD_CONST  1
#   BINARY_DIVIDE

BINARY_DIVIDE其实是调用了一个叫PyNumber_Divide(v, w);的一个函数,如果这个函数返回NULL,表示有异常产生,异常是通过PyErr_SetString(PyExc_ZeroDivisionError, "integer division or modulo by zero")产生。PyExc_ZeroDivisionError这其实是一个PyObject(好吧,这是废话),在Python环境初始化的时候就会被创建。
异常信息会被记录到线程状态对象PyThreadState中。当有异常发生的时候把Python消息循环的标志why设置为有异常发生。并且会沿着PyFrameObject传递,并且会沿反方向记录traceback。而try跟finally会创建两种PyTryBlock,在跳出blockstack后会进入except和finally的PyTryBlock。也就是说,try中的语句会在finnaly和exception的上层执行,退出后肯定会进入下面的PyTryBlock。
另外还有一个有意思的地方就是Python消息循环的函数是递归调用的,一般一个PyCodeObject回对应一个消息循环的入口函数。