起步
上一章介绍了Python虚拟机的整理框架, 接下来将深入到 PyEval_EvalFrameEx
的细节中. 将分析python虚拟机是如果完成一般表达式的. 一般表达式包括基本的对象创建, 打印语句等. 像 if, while这类的属于控制语句.
内建对象的创建
对于一个简单的对象:
[simple_obj.py]
i = 1
s = 'python'
l = []
d = {}
利用前面提到的, 我们可以访问其code对象:
co_argcount : 0
co_cellvars : ()
co_consts : (1, 'python', None)
co_filename : simple_obj.py
co_firstlineno : 1
co_flags : 64
co_freevars : ()
co_kwonlyargcount : 0
co_name : <module>
co_names : ('i', 's', 'l', 'd')
co_nlocals : 0
co_stacksize : 1
co_varnames : ()
以及这段代码所需要执行的字节码命令:
1 0 LOAD_CONST 0 (1)
2 STORE_NAME 0 (i)
2 4 LOAD_CONST 1 ('python')
6 STORE_NAME 1 (s)
3 8 BUILD_LIST 0
10 STORE_NAME 2 (l)
4 12 BUILD_MAP 0
14 STORE_NAME 3 (d)
16 LOAD_CONST 2 (None)
18 RETURN_VALUE
在执行字节码指令时,会用到大量的宏, 本章可能用到的宏有:
[ceval.c]
// 获取tuple中的元素
#define GETITEM(v, i) PyTuple_GET_ITEM((PyTupleObject *)(v), (i))
// 调整栈顶指针
#define BASIC_STACKADJ(n) (stack_pointer += n)
// 入栈操作
#define BASIC_PUSH(v) (*stack_pointer++ = (v))
#define PUSH(v) BASIC_PUSH(v)
// 出栈操作
#define BASIC_POP() (*--stack_pointer)
#define POP() BASIC_POP()
先来看看simple_obj.py中第一行对应的字节码指令:
1 0 LOAD_CONST 0 (1)
2 STORE_NAME 0 (i)
PyFrameObject对象运行时, 字节码指令对符号或常量的操作最终都反应到运行时栈或local名字空间中f->f_locals
.
LOAD_CONST
指令的实现代码:
TARGET(LOAD_CONST) {
PyObject *value = GETITEM(consts, oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}
GETITEM(consts, oparg)
明显就是 GETITEM(consts, 0)
,即 PyTuple_GET_ITEM(consts, 0)
, 从consts中读取序号为0的元素, 然后将其压入运行时栈. 联系上下文可以得到consts实际上就是 f->f_code->co_consts
,f是当前活动的PyFrameObject对象. co_consts经常被称作是常量表.
第一条字节码指令只改变了运行时栈, 未对local名字空间产生影响, 而执行 i = 1
这个赋值表达式实际应该在local名字空间创建一个符号i到PyIntObject对象1的映射关系. python虚拟机通过 STORE_NAME
来改变local名字空间:
TARGET(STORE_NAME) {
// 从符号表中获取符号 这里 oparg = 0
PyObject *name = GETITEM(names, oparg);
// 从运行时栈中获得值
PyObject *v = POP();
PyObject *ns = f->f_locals;
int err;
if (PyDict_CheckExact(ns))
err = PyDict_SetItem(ns, name, v);
else
err = PyObject_SetItem(ns, name, v);
Py_DECREF(v);
if (err != 0)
goto error;
DISPATCH();
}
names是 f->f_code->co_names
也就是符号表 , ns = f->f_locals
可知操作的对象是local名字空间, 它是一个PyDictObject对象. 将(变量名, 变量值)元素加到字典里去.
再看看第二行python代码s = 'python'
的指令:
2 4 LOAD_CONST 1 ('python')
6 STORE_NAME 1 (s)
和上一行的字节码差不多, 只是参数获取的索引变了.
第三行 l = []
的指令:
3 8 BUILD_LIST 0
10 STORE_NAME 2 (l)
BUILD_LIST实现方式:
TARGET(BUILD_LIST) {
PyObject *list = PyList_New(oparg);
while (--oparg >= 0) {
PyObject *item = POP();
PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();
}
显然,如果要创建不是一个空的list, 就会从栈中一一弹出元素, 加入到刚创建的list中.
第四行代码 d = {}
对应的指令:
4 12 BUILD_MAP 0
14 STORE_NAME 3 (d)
16 LOAD_CONST 2 (None)
18 RETURN_VALUE
BUILD_MAP看样子是创建一格dict对象:
TARGET(BUILD_MAP) {
Py_ssize_t i;
PyObject *map = _PyDict_NewPresized((Py_ssize_t)oparg);
for (i = oparg; i > 0; i--) {
int err;
// 从栈中获取key和value
PyObject *key = PEEK(2*i);
PyObject *value = PEEK(2*i - 1);
err = PyDict_SetItem(map, key, value);
if (err != 0) {
Py_DECREF(map);
goto error;
}
}
// 参数出栈
while (oparg--) {
Py_DECREF(POP());
Py_DECREF(POP());
}
PUSH(map);
DISPATCH();
}
当创建不是一个空字典时, 会一一从栈中获取元素. 在python中, 执行一段Code Block后一定要返回一些值:
TARGET(RETURN_VALUE) {
retval = POP();
why = WHY_RETURN;
goto fast_block_end;
}
实际的返回值保存在 retval
中, 是从运行时栈中取得的, 因此它的值就是上一条的 LOAD_CONST 2 (None)
了, 即None值. 然后将虚拟机状态设为 WHY_RETURN
.
复杂的内建对象创建
前面说的当创建不是一个空list或空dict时, 是会从栈中获取元素, 那他们是怎么进入栈的呢, 看看测试代码:
l = [1, 2]
d = {"1": 1, "2": 2}
对应的code和字节码指令:
co_argcount : 0
co_cellvars : ()
co_consts : (1, 2, ('1', '2'), None)
co_filename : simple_obj.py
co_firstlineno : 1
co_flags : 64
co_freevars : ()
co_kwonlyargcount : 0
co_name : <module>
co_names : ('l', 'd')
co_nlocals : 0
co_stacksize : 3
co_varnames : ()
1 0 LOAD_CONST 0 (1)
2 LOAD_CONST 1 (2)
4 BUILD_LIST 2
6 STORE_NAME 0 (l)
2 8 LOAD_CONST 0 (1)
10 LOAD_CONST 1 (2)
12 LOAD_CONST 2 (('1', '2'))
14 BUILD_CONST_KEY_MAP 2
16 STORE_NAME 1 (d)
18 LOAD_CONST 3 (None)
20 RETURN_VALUE
code中比较重要的是常量表co_consts : (1, 2, ('1', '2'), None)
和符号表 co_names : ('l', 'd')
. list的创建之前会先load常量到栈中, BUILD_LIST 传入参数2, 表示需要两个参数, 正好从栈中获取2个.
创建dict前, 会先load他们values, 然后将keys入栈,再创建dict:
TARGET(BUILD_CONST_KEY_MAP) {
Py_ssize_t i;
PyObject *map;
PyObject *keys = TOP();
map = _PyDict_NewPresized((Py_ssize_t)oparg);
for (i = oparg; i > 0; i--) {
int err;
PyObject *key = PyTuple_GET_ITEM(keys, oparg - i);
PyObject *value = PEEK(i + 1);
err = PyDict_SetItem(map, key, value);
if (err != 0) {
Py_DECREF(map);
goto error;
}
}
Py_DECREF(POP());
while (oparg--) {
Py_DECREF(POP());
}
PUSH(map);
DISPATCH();
}
BUILD_CONST_KEY_MAP
和 BUILD_MAP
的实现差不多, 有个搞不懂的, 既然创建空dict和非空dict的指令不一样, 那为什么创建空dict需要参数呢? 这个没理解.
其他的一般表达式
如果对于需要运算的操作实现过程是怎样呢:
a = 5
b = a
c = a + b
print(c)
对应的code和字节码指令:
co_consts : (5, None)
co_filename : simple_obj.py
co_names : ('a', 'b', 'c', 'print')
co_nlocals : 0
co_stacksize : 2
1 0 LOAD_CONST 0 (5)
2 STORE_NAME 0 (a)
2 4 LOAD_NAME 0 (a)
6 STORE_NAME 1 (b)
3 8 LOAD_NAME 0 (a)
10 LOAD_NAME 1 (b)
12 BINARY_ADD
14 STORE_NAME 2 (c)
4 16 LOAD_NAME 3 (print)
18 LOAD_NAME 2 (c)
20 CALL_FUNCTION 1
22 POP_TOP
24 LOAD_CONST 1 (None)
26 RETURN_VALUE
符号搜索
有了一个新的指令 LOAD_NAME
, 从符号表中载入, 根据后面的STORE_NAME, 应该是将符号表对应的变量值取出放到栈里面:
// 有删改
TARGET(LOAD_NAME) {
PyObject *name = GETITEM(names, oparg);
PyObject *locals = f->f_locals;
PyObject *v;
v = PyDict_GetItem(locals, name);
Py_XINCREF(v);
if (v == NULL) {
v = PyDict_GetItem(f->f_globals, name);
Py_XINCREF(v);
if (v == NULL) {
v = PyDict_GetItem(f->f_builtins, name);
if (v == NULL) {
format_exc_check_arg(
PyExc_NameError,
NAME_ERROR_MSG, name);
goto error;
}
Py_INCREF(v);
}
}
PUSH(v);
DISPATCH();
}
LOAD_NAME
将以此从local, global, builtin 3个名字空间顺序查找, 如果都没找到说明名字未定义, 抛出异常, 终止python虚拟机的运行. 搜索规则也就是 LGB 规则.
数值运算
数值运算 c = a + b
中看到了新的指令 BINARY_ADD
, 虚拟机先通过load_name将a和b的变量值取出压入运行时栈, 然后再进行的加法:
TARGET(BINARY_ADD) {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *sum;
if (PyUnicode_CheckExact(left) &&
PyUnicode_CheckExact(right)) {
// 字符串拼接
sum = unicode_concatenate(left, right, f, next_instr);
}
else {
// 数值相加
sum = PyNumber_Add(left, right);
Py_DECREF(left);
}
Py_DECREF(right);
SET_TOP(sum);
if (sum == NULL)
goto error;
DISPATCH();
}
加法运算除了用于数字也同样用于字符串, 在 PyNumber_Add
中, 虚拟机进行大量的类型判断, 检查PyNumberMethods中的nb_add能否完成加法运算. 如果不能, 会检查PySequenceMethods中sq_concat能否完成. 如果不能,python就报错误了.
信息输出
最后看一看print的动作, python3中print是一个函数, 因此需要将函数和参数事先压入栈中:
16 LOAD_NAME 3 (print)
18 LOAD_NAME 2 (c)
20 CALL_FUNCTION 1
22 POP_TOP
CALL_FUNCTION
进行函数调用, 参数数量是1:
TARGET(CALL_FUNCTION) {
PyObject **sp, *res;
sp = stack_pointer;
// oparg为1
res = call_function(&sp, oparg, NULL);
stack_pointer = sp; // 重设栈指针, 模拟了CPU设计
PUSH(res);
if (res == NULL) {
goto error;
}
DISPATCH();
}
这里显然 print
是python的内建函数, 关于函数机制后面看, 这里先跳过它的获取过程, 看print的实现部分:
[bltinmoudule.c]
static PyMethodDef builtin_methods[] = {
...
{"print", (PyCFunction)builtin_print, METH_FASTCALL | METH_KEYWORDS, print_doc}
...
}
实现在 builtin_print 中:
static PyObject * builtin_print(PyObject *self, PyObject **args, Py_ssize_t nargs, PyObject *kwnames)
{
...
file = _PySys_GetObjectId(&PyId_stdout);
for (i = 0; i < nargs; i++) {
if (i > 0) {
if (sep == NULL)
err = PyFile_WriteString(" ", file);
else
err = PyFile_WriteObject(sep, file,
Py_PRINT_RAW);
if (err)
return NULL;
}
err = PyFile_WriteObject(args[i], file, Py_PRINT_RAW);
if (err)
return NULL;
}
if (end == NULL)
err = PyFile_WriteString("\n", file);
else
err = PyFile_WriteObject(end, file, Py_PRINT_RAW);
if (err)
return NULL;
...
Py_RETURN_NONE;
}
默认情况输出到 stdout
标准输出中, 使用PyFile_WriteObject将元素输出, 当有多个需要打印时, 用空格 " "
隔开, 并且最后输出一个换行 \n
.