我们了解一些CPython一些内部机制,本篇会以gdb为例开始切入CPython的执行流程,我们现在用一个简单的示例开始吧,当你下载源代码后并在安装必要的C编译器和系统库后,在通过下面的命令编译源代码
./configure --with-pydebug
make -j3 -s
我们会得到一个带有调试模式的python解释器
![](https://img.haomeiwen.com/i16148197/436b742729a7bb2b.png)
我们通过在源代码的目录下,执行如下命令,一定要添加“./”,以指示shell执行当前目录下的python解释器
gdb ./python
然后在gdb环境下执行run命令就已经进入可调试的python解释器环境,如下图所示
![](https://img.haomeiwen.com/i16148197/303c30bcb91c47fa.png)
我们在带调试模式的python交互命令行中,定义一个简单的函数
>>> def num(j):
... m=int(22)
... return j*m
...
>>>
导入反编译模块dis,并通过dis模块的dis函数查看,num函数对应的操作码。如下图所示。
![](https://img.haomeiwen.com/i16148197/e7492ccdfb91bf21.png)
我们知道从LOAD_GLOBAL到STORE_FAST对应的是函数中m=int(22)这条python语句,这里需要说明的是字节码文件中的每个操作码(opcode)的具体行为定义在源代码Python/ceval.c文件中的_PyEval_EvalFrameDefault函数内部。该函数内部有一个python解释主循环(具体源码定义请参考这里),该主循环的具体定义如下代码轮廓,那么我们就知道在主循环内部的巨型switch控制结构中,任一个case分支的C代码定义就对应某个操作码。
_PyEval_EvalFrameDefault(....){
......
//解释器主循环
mainloop:
for (;;) {
....
switch (opcode) {
case TARGET(操作码1):{
....
}
case TARGET(操作码2){
....
}
....
case TARGET(操作码2){
....
}
}
}
}
我们回归正题,现在我们如何为python解释器底层的C代码设置断点呢?因为目前调试模式的python解释器运行在内存中,在设置断点前,我们需要对当前python进程发送一个挂起信号。操作步骤很简单。只需在开另一个shell命令行窗口,并执行
pkill python -SIGTRAP
那么上一个打开python进程的shell命令指示符会接受到一个SIGTRAP信号,该python进程会暂时被挂起
![](https://img.haomeiwen.com/i16148197/bfeec26c86f8b73e.png)
我们这里需要知道num函数内m=int(22)语句对应在CPython内部的操作流程。那么接下来只需在Python/ceval.c源码文件中找到该语句对应的操作码的C代码定义并执行断点即可。然则对应的操作码的C代码定义,我们只举其中一个操作码定义的断点过程。
首先、使用你喜欢的编辑器在Python/ceval.c源码文件以LOAD_GLOBAL为关键字进行查找,我们知道该操作码的case分支位于第2570行,如下图所示
![](https://img.haomeiwen.com/i16148197/a61db3457815a1c0.png)
那么,在python进程挂起的所在shell窗口,执行所有相关操作码定义的断点命令,如下图所示
![](https://img.haomeiwen.com/i16148197/0e376a7519704e5b.png)
设定所有断点后,我们在gdb中执行continue命令,此时会重回python命令行的交互环境,并且python解析器自动执行到底层的C代码的第一个断点。
![](https://img.haomeiwen.com/i16148197/38befe0317c6122b.png)
那么,剩下的问题就是我们通过gdb的next指令或step指令来单步执行断电之后的每条C代码,但我这里更希望知道当前python进程执行到第一个断点时的整个python进程的函数栈状态。那么我们执行bt指令,会得到一个非常详细函数栈上下文
![](https://img.haomeiwen.com/i16148197/73872e5a3a5d2ee0.png)
正如我所料,我们查看到当Python解释器执行到第一个断点时,目前整个函数栈从栈底的第一个main函数到栈顶的_PyEval_EvalFrameDefault函数,一个存在16个栈帧,这样你当然能够对每个被执行的操作码它目前的栈状态一目了然,不至于迷失阅读C代码的方向。
需要注意的是我们这里目前还没有在Python交互环境中手动执行num函数,也即自调用continue命令后,CPython主循环驱动下执行断点的位置所产生函数栈帧状态跟我们num函数封装的操作码指令毫无关系。此时,需要做的是使用next指令条件跳过这些断点,直到从gdb命令行指示符(gdb)返回到python命令行指示符>>>
在第一个python命令指示符>>>出现后,此时我们可以手动执行num函数,如下图,随便传入一个整数,即num(32)后,回车,看看C代码的执行
![](https://img.haomeiwen.com/i16148197/cee196ddae476484.png)
在分析函数执行流程,不妨回顾一下num函数对应的代码对象细节
>>>
>>> def num(j):
... m=int(22)
... return j*m
...
>>> num.__code__.co_name #函数名
'num'
>>> num.__code__.co_names #函数内Python关键字标签
('int',)
>>> num.__code__.co_consts #函数内的所有常量
(None, 22)
>>> num.__code__.co_varnames #函数内的变量名标签
('j', 'm')
函数中第一条语,m=int(22),我们对其每个C底层的操作码定义深入的剖析
首先、LOAD_GLOBAL 0(int),我们知道0是一个字节码参数,而括号中的int是一个Python关键字,实际上就是num.__code __.co_names元祖中的一个字符串常量,我们称为关键字标签.
LOAD_GLOBAL 0(int)就等价于下面简化后的C代码逻辑,下面代码省略了很多检测逻辑,保留了一些关键的执行流程和关键变量。
- 参数f是一个PyFrameObject指针类型,它指向堆内存的一个PyFrameObject的结构体,PyFrameObject下文再说。
- names是一个PyObject类型的指针,它指向了堆内存中由一个或多个Python语义的关键字标签组成的元祖,对于C来说实际上是一些特定的字符串聚合而成的数组,例如这里是“int”字符串是其中的一个元素。
- consts是一个PyObject类型的指针,它指向堆内存中由一个或多个C常量(数字或字符串)组成的元祖,对于C来说,通常组成该元祖的元素是一个右值。
PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag){
...
PyObject *names;
PyObject *consts;
_PyOpcache *co_opcache;
......
//当前字节码
int opcode;
//当前字节码参数
int oparg;
....
//代码对象
co = f->f_code;
//从代码对象中获取num函数内部Python关键字标签
names = co->co_names;
//从代码对象获取num函数内部的常量集
consts = co->co_consts;
//解释器主循环
mainloop:
for (;;) {
....
switch (opcode) {
....
case TARGET(LOAD_GLOBAL): {
PyObject *name;
PyObject *v;
if (PyDict_CheckExact(f->f_globals)
&& PyDict_CheckExact(f->f_builtins))
{
....
//获取关键字标签,这里是int
name = GETITEM(names, oparg);
v = _PyDict_LoadGlobal((PyDictObject *)f->f_globals,
(PyDictObject *)f->f_builtins,
name);
...
Py_INCREF(v);
}
else {
/* namespace 1: globals */
name = GETITEM(names, oparg);
v = PyObject_GetItem(f->f_globals, name);
......
PUSH(v);
DISPATCH();
}
....
}
}
主要关注的的关键点是__PyEval_EvalFrameDefault参数的的第二个参数是一个PyFrameObject类型的对象,而PyFrameObject是一个实现Python语义栈对象
好了,我没必要说太多,本篇是假定你有相当的C编程经验,少废话,读者自己去尝试吧。
更新中.....
网友评论