Appearance
在分析学习 Python 内核的时候,为什么选择先从字节码分析开始?因为字节码是 Python 的中间语言,也是 Python 的核心,它是 Python 的执行引擎的基础。如果我们能够理解字节码,那么我们就能够理解 Python 的执行引擎,也就能够理解 Python 的运行机制。
字节码更接近底层,像汇编指令一样更接近机器。Python3.10.7 共用 165 个字节码,主要分为这几大类:
- 加载和存储指令
- STORE_NAME
- DELETE_NAME
- LOAD_NAME
- LOAD_CONST
- 栈操作指令
- POP_TOP
- ROT_TWO
- ROT_THREE
- DUP_TOP
- DUP_TOP_TWO
- 控制流指令
- JUMP_FORWARD
- JUMP_IF_FALSE_OR_POP
- POP_JUMP_IF_FALSE
- 函数调用指令
- CALL_FUNCTION
- CALL_FUNCTION_KW
- CALL_FUNCTION_EX
- CALL_METHOD
- LOAD_METHOD
- 二进制运算指令
- BINARY_POWER
- BINARY_MULTIPLY
- BINARY_MODULO
- BINARY_ADD
- BINARY_SUBTRACT
- BINARY_SUBSCR
- BINARY_FLOOR_DIVIDE
- BINARY_TRUE_DIVIDE
- 比较指令
- COMPARE_OP
- IS_OP
- 异常处理指令
- POP_EXCEPT
- 基础数据操作指令
- BUILD_TUPLE
- BUILD_LIST
- BUILD_SET
- BUILD_MAP
- LOAD_ATTR
- 其他指令
具体指令的定义在Include/opcode.h
文件中,由于篇幅有点长,就不全部贴出来了,挑选了一些有代表性的指令,有兴趣的同学可以翻阅源码看看。Opcode 的代码是通过 Python 的脚本生成的。可以从文件的头部看到如下注释。是不是感到特别的神奇,用 Python 脚本生成 C 代码。
c
/* Auto-generated by Tools/scripts/generate_opcode_h.py from Lib/opcode.py */
指令的命名很容易看出它具体的作用,比如LOAD_NAME
指令就是加载一个变量名,STORE_NAME
指令就是存储一个变量名。LOAD_CONST
指令就是加载一个常量,POP_TOP
指令就是弹出栈顶元素。BINARY_ADD
指令就是执行两个对象的加法操作,BINARY_SUBTRACT
指令就是执行两个对象的减法操作。COMPARE_OP
指令就是执行两个对象的比较操作。BUILD_TUPLE
指令就是构建一个元组对象,BUILD_LIST
指令就是构建一个列表对象,BUILD_SET
指令就是构建一个集合对象,BUILD_MAP
指令就是构建一个字典对象。LOAD_ATTR
指令就是加载一个属性。
指令和指令参数是绑定在一起的,有的指令有参数,有的是没有参数的。字节码的数据类型是unsigned short int
,也就是 2 个字节,对于大端来说,前高位字节 8 位表示 opcode,低位字节 8 位表示操作数。指令参数的值是通过_Py_OPARG
宏来获取的,指令的值是通过_Py_OPCODE
宏来获取的,通过左移 8 位获取 opcode,按位与0b11111111
得到操作参数。这两个宏的定义如下所示:
c
#ifdef WORDS_BIGENDIAN
# define _Py_OPCODE(word) ((word) >> 8)
# define _Py_OPARG(word) ((word) & 255)
#else
# define _Py_OPCODE(word) ((word) & 255)
# define _Py_OPARG(word) ((word) >> 8)
#endif
#define NEXTOPARG() do { \
_Py_CODEUNIT word = *next_instr; \
opcode = _Py_OPCODE(word); \
oparg = _Py_OPARG(word); \
next_instr++; \
} while (0)
下面来个例子看看字节码长什么样子。从list(x.__code__.co_code)
的输出结果可以看出,奇数位(索引从 1 开始)为指令,偶数位为指令参数的值,有的 0 表示没有指令参数,有的 0 表示索引位置等。
c
>>> def x():
... x = 3
... print(x)
...
>>> x.__code__.co_code
b'd\x01}\x00t\x00|\x00\x83\x01\x01\x00d\x00S\x00'
>>> list(x.__code__.co_code)
[100, 1, 125, 0, 116, 0, 124, 0, 131, 1, 1, 0, 100, 0, 83, 0]
>>> import dis
>>> dis.dis(x)
2 0 LOAD_CONST 1 (3)
2 STORE_FAST 0 (x)
3 4 LOAD_GLOBAL 0 (print)
6 LOAD_FAST 0 (x)
8 CALL_FUNCTION 1
10 POP_TOP
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
>>> dis.opname[100]
'LOAD_CONST'
>>> dis.opname[125]
'STORE_FAST'
>>>
指令参数有什么作用?
例如LOAD_CONST
有一个指令参数 oparg,它的作用是从常量表中取出值,那么参数就是常量表的索引。之前版本有两个指令参数,随着设计者的不断优化,如今只有一个指令参数,提高了字节码的执行效率。
c
case TARGET(LOAD_CONST): { // 加载常量
PREDICTED(LOAD_CONST);
PyObject *value = GETITEM(consts, oparg);
Py_INCREF(value);
PUSH(value);
DISPATCH();
}