终于,我们进入了 _PyEval_EvalFrameDefault 函数,这是 Python VM 最核心部分,在这里我们将于 VM 的执行零距离接触。
opcode 和 opargs
ByteCode 的最小单元是 word,类型是 uinit16_t,一个 16 位的无符号整数。Python 充分的利用这 16 位空间,在其中压缩了两类数据:
- 操作码 opcode;
- 和操作参数 oparg
各 8 位。通过下面两个宏快速获得两个部分。
#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
ByteCode 是 VM 指令,实际情况下,直接存储指令名过于冗余,存储的是这些指令对应的编码,具体的编码定义在:
Include/opcode.h
Python 3.8 共计 122 个 opcode,或者说指令。
查找 print 函数
其实应该在开始之前对 Python 的 VM 模型有一定的了解,不过我觉得现在不用那么精确,有个大概影响就可以了,就边走边看吧。
我将 _PyEval_EvalFrameDefault 做了精简:
PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag){
// ··· ···
// 获取 co_code 中的第一条指令
first_instr = (_Py_CODEUNIT *) PyBytes_AS_STRING(co->co_code);
next_instr = first_instr
// ··· ···
// 获得操作码
opcode = _Py_OPCODE(*next_instr);
// ··· ···
// 获得操作参数
oparg = _Py_OPARG(*next_instr)
switch(opcode){
// cases here
}
随着 GDB 的指引,第一个 opcode 是 101,其定义是:
#define LOAD_NAME 101
我们看看这个 case 分支是如何处理的。LOAD_NAME 指令是加载一个名称到 VM 的运行栈上,根据 GDB 的提示,oparg 是 0, 首先从 names TUPLE 中取出索引为 0 的对象,一个 string: “print”,希望你还记得测试 py 文件里的代码,就是一行 print("hello world."), 显然这加载的是 print 函数。
Python 有作用域的概念,不同作用域可能有相同的 name,表示完全不同的对象,作用域的实现已经大致可以看出:
// 首先查找 local 域
PyObject *locals = f->f_locals;
// ··· ···
if (PyDict_CheckExact(locals)) {
v = PyDict_GetItemWithError(locals, name);
// ··· ···
}
else {
v = PyObject_GetItem(locals, name);
// ··· ···
}
// 否则
if (v == NULL) {
// 查找全局域
v = PyDict_GetItemWithError(f->f_globals, name);
// ··· ···
else {
// 查找 buildin 域
if (PyDict_CheckExact(f->f_builtins)) {
v = PyDict_GetItemWithError(f->f_builtins, name);
// ··· ···
}
// ··· ···
}
}
LOAD_NAME 操作码的处理过程表明, print 函数的查找,按照下图的先后顺序执行:
我们知道 print 函数是 Python 的内建对象,实际上它最终也是在 buildin 中被查找到。
压栈
Python VM 是栈式虚拟机,也就是计算过程伴随着一系列的入栈、出栈操作,除此之外有比较流行的寄存器式虚拟机。栈式虚拟机的优势是实现简单,但相比寄存器式虚拟机的字节码会更加冗余,寄存器式虚拟机则相反,早起 Java 采用的是栈式虚拟机,后来改为寄存器式,另外最快的脚本语言 Lua 采用的也是寄存器式虚拟机。这都是题外话,暂时按下不表。
查找到 print 函数的目的,是为了将其压入 VM 的运行栈中,以便稍后继续压入它的参数:一个 string object “hello world.”, 然后等待被调用。
使用下面的宏,将 print 函数对象压入 VM 运行栈:
// v 是前面找到的 print 函数对象
PUSH(v);
此时 VM 运行栈看起来像这样: