Skip to content

在分析学习 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();
    }