字节码和虚拟机

Python会将代码先编译成字节码,然后在虚拟机中动态得依次解释执行字节码。编译好的字节码存储在硬盘中以.pyc.pyd等为扩展名。而在运行态,这些字节码会作为Python的一种对象PyCodeObject存在。PyCodeObject可以理解为C语言中的文本段,用于存储编译后的字节码、调试信息、常量值、变量名等。

本文不会讲述代码如何一步步编译成PyCodeObject,只会简单介绍PyCodeObject中各个域的含义,而把重点放在介绍Python的虚拟机和执行流。

Python中的伪码PyCodeObject

PyCodeObject保存代编译后的静态信息,在运行时再结合上下文形成一个完整的运行态环境。让我们看看静态编译后的信息都有哪些。

typedef struct {
    PyObject_HEAD
    int co_argcount;    // co_argcount 参数,不包括不定参数
    int co_nlocals;		// co_nlocals 变量个数,co_argcount + 
                        // 可变参数个数 + co_kwonlyargcount(py3.0) + 局部变量个数
    int co_stacksize;   // 栈的大小 (编译后需要的最大栈深度) 
    int co_flags;		// PyCodeObject的一些标志位,用来优化运行时的性能
    PyObject *co_code;		// 编译后的字节码字符串
    PyObject *co_consts;	// 常量的列表
    PyObject *co_names;		// 常量中的字符串对象
    PyObject *co_varnames;	// 变量名字的元组
    PyObject *co_freevars;	// 自由变量的元组
    PyObject *co_cellvars;      // cell变量的元组
    /* The rest doesn't count for hash/cmp */
    PyObject *co_filename;	// 文件名
    PyObject *co_name;		// 对象的名字,例如函数的名字、类的名字等
    int co_firstlineno;		// 对应的代码在源码文件中的起始行号
    PyObject *co_lnotab;	// 伪码与行号的映射
    void *co_zombieframe;     // 对于一些特殊情况下的优化
    PyObject *co_weakreflist;   // 支持弱引用
} PyCodeObject;

其中有些域需要特别解释。

  • co_flags 用来保存一些编译信息,主要用于优化工作。例如co_VARARGS(0x0004)表示有可变参数等,具体见code.h文件。

  • co_freevars 自由变量是一些在作用域内使用,但是没有在本作用域定义的变量。

  • co_cellvars 当前作用域定义,而在闭包等内部使用的变量。

  • co_lnotab 字节码的偏移值与对应的源码的行号的相对值。

那么实际上co_lnotab记录的是(0, 0), (6, 1), (44, 5),当然实际记录中没有括号。具体偏移值和真实行号的对应关系可以通过下面的算法计算出来。

  • co_code 记录编译后的字节码,以字符串的形式保存,而实际上就是数字。后面我们通过一个例子详细描述。

PyCodeObject的示例

先给定一个Python代码示例,然后打印出其中的各个域。

需要先解释一下co_kwonlyargcount,这个域在PY3才有,用于支持在不定参数后定义的位置参数,例如def func(*args, kwonly=None)

这个实例的输出可以看到对应的各个域的详细内容。

从这个例子中可以清楚了解常量、变量、自由变量以及cell变量的含义。接下来我们看下co_code的含义,使用linux的xdd工具将其转换成十六进制,并且使用dis模块反编译其字节码。

  • 十六进制的第一个为64100,查阅opcode.h可以看到起对应的字节码#define LOAD_CONST 100,与反编译中的命令LOAD_CONST相符。

  • 十六进制的第二个为0101,对应的是字节码LOAD_CONST的参数1

  • 十六进制的第三个为0000,此值表示STOP_CDOE,一个完整字节码的结束标志。

同理可以解析接下来的字节码和对应的操作的含义。至此,我们明白字节码的格式为

到现在为止我们明白了字节码的数据结构、各域值的含义,co_code字节码的格式以及如何与操作命令对应。下面我们看看这些字节码如何运行。

PyFrameObject

Python模拟了C语言中的运行栈作为运行时的环境,每个栈用PyFrameObject结构表示。

对应的结构

当执行函数调用时会进入新的栈帧,那么当前栈帧就作为下一个栈帧的f_back字段

多个栈帧链属于一个线程,而同时可能存在多个线程,每个线程拥有一个栈帧链。这样形成了Python的虚拟机运行环境。

image

Python执行字节码

字节码的执行就像上图所示,由一个大的循环和选择语句构成,逻辑骨干比较简单。

接下来,我们通过反编译代码追踪其如何一步步执行。

通过追踪每个指令码的执行过程以及对应的PyFrameObject的栈帧变化,可以一步步看到虚拟机的执行过程。

初始化以及分别执行03字节码的PyFrameObject结构变化。

  • LOAD_CONST 将co_consts中对应的值压栈

  • STORE_DEREF 解引用,设置栈中的变量值

image
  • LOAD_CLOSURE 将freevars中的对象压栈

  • BUILD_TUPLE 用栈帧中的元素创建元组,并压栈

  • BUILD_CLOSURE 创建PyFunction对象,并设置其中的f_closure

image
  • STORE_FAST 将栈中的一个元素设置到对应的本地变量域中

  • RETURN_VALUE return,并且设置退出原因WHY_RETURN

从上面的代码和过程图,整个代码的执行过程清楚的显现出来:)

最后更新于

这有帮助吗?