IT狗工作室

第4篇:通过gdb走进CPython的内核

2020-07-03  本文已影响0人  铁甲万能狗

我们了解一些CPython一些内部机制,本篇会以gdb为例开始切入CPython的执行流程,我们现在用一个简单的示例开始吧,当你下载源代码后并在安装必要的C编译器和系统库后,在通过下面的命令编译源代码

./configure --with-pydebug
make -j3 -s

我们会得到一个带有调试模式的python解释器


我们通过在源代码的目录下,执行如下命令,一定要添加“./”,以指示shell执行当前目录下的python解释器
gdb ./python

然后在gdb环境下执行run命令就已经进入可调试的python解释器环境,如下图所示

我们在带调试模式的python交互命令行中,定义一个简单的函数

>>> def num(j):  
...     m=int(22)
...     return j*m
... 
>>> 

导入反编译模块dis,并通过dis模块的dis函数查看,num函数对应的操作码。如下图所示。


我们知道从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进程会暂时被挂起

我们这里需要知道num函数内m=int(22)语句对应在CPython内部的操作流程。那么接下来只需在Python/ceval.c源码文件中找到该语句对应的操作码的C代码定义并执行断点即可。然则对应的操作码的C代码定义,我们只举其中一个操作码定义的断点过程。

首先、使用你喜欢的编辑器在Python/ceval.c源码文件以LOAD_GLOBAL为关键字进行查找,我们知道该操作码的case分支位于第2570行,如下图所示

那么,在python进程挂起的所在shell窗口,执行所有相关操作码定义的断点命令,如下图所示

设定所有断点后,我们在gdb中执行continue命令,此时会重回python命令行的交互环境,并且python解析器自动执行到底层的C代码的第一个断点。

那么,剩下的问题就是我们通过gdb的next指令或step指令来单步执行断电之后的每条C代码,但我这里更希望知道当前python进程执行到第一个断点时的整个python进程的函数栈状态。那么我们执行bt指令,会得到一个非常详细函数栈上下文


正如我所料,我们查看到当Python解释器执行到第一个断点时,目前整个函数栈从栈底的第一个main函数到栈顶的_PyEval_EvalFrameDefault函数,一个存在16个栈帧,这样你当然能够对每个被执行的操作码它目前的栈状态一目了然,不至于迷失阅读C代码的方向。

需要注意的是我们这里目前还没有在Python交互环境中手动执行num函数,也即自调用continue命令后,CPython主循环驱动下执行断点的位置所产生函数栈帧状态跟我们num函数封装的操作码指令毫无关系。此时,需要做的是使用next指令条件跳过这些断点,直到从gdb命令行指示符(gdb)返回到python命令行指示符>>>

在第一个python命令指示符>>>出现后,此时我们可以手动执行num函数,如下图,随便传入一个整数,即num(32)后,回车,看看C代码的执行

在分析函数执行流程,不妨回顾一下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代码逻辑,下面代码省略了很多检测逻辑,保留了一些关键的执行流程和关键变量。

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编程经验,少废话,读者自己去尝试吧。

更新中.....

上一篇下一篇

猜你喜欢

热点阅读