从底层理解Python的执行

更新时间:2015-06-01 10:06:43点击次数:2210次

编者按】下面博文将带你创建一个字节码级别的追踪API以追踪Python的一些内部机制,比如类似YIELDVALUE、YIELDFROM操作码的实现,推式构造列表(List Comprehensions)、生成器表达式(generator expressions)以及其他一些有趣Python的编译。


关于译者:赵斌, OneAPM工程师,常年使用 Python/Perl 脚本,从事 DevOP、测试开发相关的开发工作。业余热爱看书,喜欢 MOOC。


以下为译文

近我在学习 Python 的运行模型。我对 Python 的一些内部机制很是好奇,比如 Python 是怎么实现类似 YIELDVALUE、YIELDFROM 这样的操作码的;对于 递推式构造列表(List Comprehensions)、生成器表达式(generator expressions)以及其他一些有趣的 Python 特性是怎么编译的;从字节码的层面来看,当异常抛出的时候都发生了什么事情。翻阅 CPython 的代码对于解答这些问题当然是很有帮助的,但我仍然觉得以这样的方式来做的话对于理解字节码的执行和堆栈的变化还是缺少点什么。GDB 是个好选择,但是我懒,而且只想使用一些比较高阶的接口写点 Python 代码来完成这件事。

所以呢,我的目标就是创建一个字节码级别的追踪 API,类似 sys.setrace 所提供的那样,但相对而言会有更好的粒度。这充分锻炼了我编写 Python 实现的 C 代码的编码能力。我们所需要的有如下几项,在这篇文章中所用的 Python 版本为 3.5。


  • 一个新的 Cpython 解释器操作码

  • 一种将操作码注入到 Python 字节码的方法

  • 一些用于处理操作码的 Python 代码

一个新的 Cpython 操作码

新操作码:DEBUG_OP

这个新的操作码 DEBUG_OP 是我次尝试写 CPython 实现的 C 代码,我将尽可能的让它保持简单。 我们想要达成的目的是,当我们的操作码被执行的时候我能有一种方式来调用一些 Python 代码。同时,我们也想能够追踪一些与执行上下文有关的数据。我们的操作码会把这些信息当作参数传递给我们的回调函数。通过操作码能辨识出的有用信息如下:

  • 堆栈的内容

  • 执行 DEBUG_OP 的帧对象信息

所以呢,我们的操作码需要做的事情是:

  • 找到回调函数

  • 创建一个包含堆栈内容的列表

  • 调用回调函数,并将包含堆栈内容的列表和当前帧作为参数传递给它

听起来挺简单的,现在开始动手吧!声明:下面所有的解释说明和代码是经过了大量段错误调试之后总结得到的结论。首先要做的是给操作码定义一个名字和相应的值,因此我们需要在Include/opcode.h中添加代码。


[py] view plaincopy

  1. /** My own comments begin by '**' **/  

  2. /** From: Includes/opcode.h **/  

  3.   

  4. /* Instruction opcodes for compiled code */  

  5.   

  6. /** We just have to define our opcode with a free value  

  7.     0 was the first one I found **/  

  8. #define DEBUG_OP                0  

  9.   

  10. #define POP_TOP                 1  

  11. #define ROT_TWO                 2  

  12. #define ROT_THREE               3  

这部分工作就完成了,现在我们去编写操作码真正干活的代码。 


实现 DEBUG_OP

在考虑如何实现DEBUG_OP之前我们需要了解的是DEBUG_OP提供的接口将长什么样。 拥有一个可以调用其他代码的新操作码是相当酷眩的,但是究它将调用哪些代码捏?这个操作码如何找到回调函数的捏?我选择了一种简单的方法:在帧的全局区域写死函数名。那么问题就变成了,我该怎么从字典中找到一个固定的 C 字符串?为了回答这个问题我们来看看在 Python 的 main loop 中使用到的和上下文管理相关的标识符__enter____exit__。

我们可以看到这两标识符被使用在操作码SETUP_WITH中:


[py] view plaincopy

  1. /** From: Python/ceval.c **/  

  2. TARGET(SETUP_WITH) {  

  3. _Py_IDENTIFIER(__exit__);  

  4. _Py_IDENTIFIER(__enter__);  

  5. PyObject *mgr = TOP();  

  6. PyObject *exit = special_lookup(mgr, &PyId___exit__), *enter;  

  7. PyObject *res;  

现在,看一眼宏_Py_IDENTIFIER的定义 




[py] view plaincopy

  1. /** From: Include/object.h **/  

  2.   

  3. /********************* String Literals ****************************************/  

  4. /* This structure helps managing static strings. The basic usage goes like this:  

  5.    Instead of doing  

  6.   

  7.        r = PyObject_CallMethod(o, "foo""args", ...);  

  8.   

  9.    do  

  10.   

  11.        _Py_IDENTIFIER(foo);  

  12.        ...  

  13.        r = _PyObject_CallMethodId(o, &PyId_foo, "args", ...);  

  14.   

  15.    PyId_foo is a static variable, either on block level or file level. On first  

  16.    usage, the string "foo" is interned, and the structures are linked. On interpreter  

  17.    shutdown, all strings are released (through _PyUnicode_ClearStaticStrings).  

  18.   

  19.    Alternatively, _Py_static_string allows to choose the variable name.  

  20.    _PyUnicode_FromId returns a borrowed reference to the interned string.  

  21.    _PyObject_{Get,Set,Has}AttrId are __getattr__ versions using _Py_Identifier*.  

  22. */  

  23. typedef struct _Py_Identifier {  

  24.     struct _Py_Identifier *next;  

  25.     const char* string;  

  26.     PyObject *object;  

  27. } _Py_Identifier;  

  28.   

  29. #define _Py_static_string_init(value) { 0, value, 0 }  

  30. #define _Py_static_string(varname, value)  static _Py_Identifier varname = _Py_static_string_init(value)  

  31. #define _Py_IDENTIFIER(varname) _Py_static_string(PyId_##varname, #varname)  

嗯,注释部分已经说明得很清楚了。通过一番查找,我们发现了可以用来从字典找固定字符串的函数_PyDict_GetItemId,所以我们操作码的查找部分的代码就是长这样滴。



[py] view plaincopy

  1.  /** Our callback function will be named op_target **/  

  2. PyObject *target = NULL;  

  3. _Py_IDENTIFIER(op_target);  

  4. target = _PyDict_GetItemId(f->f_globals, &PyId_op_target);  

  5. if (target == NULL && _PyErr_OCCURRED()) {  

  6.     if (!PyErr_ExceptionMatches(PyExc_KeyError))  

  7.         goto error;  

  8.     PyErr_Clear();  

  9.     DISPATCH();  

  10. }  

为了方便理解,对这一段代码做一些说明:



  • f是当前的帧,f->f_globals是它的全局区域

  • 如果我们没有找到op_target,我们将会检查这个异常是不是KeyError

  • goto error;是一种在 main loop 中抛出异常的方法

  • PyErr_Clear()抑制了当前异常的抛出,而DISPATCH()触发了下一个操作码的执行

下一步就是收集我们想要的堆栈信息。 



[py] view plaincopy

  1. /** This code create a list with all the values on the current stack **/  

  2. PyObject *value = PyList_New(0);  

  3. for (i = 1 ; i <= STACK_LEVEL(); i++) {  

  4.     tmp = PEEK(i);  

  5.     if (tmp == NULL) {  

  6.         tmp = Py_None;  

  7.     }  

  8.     PyList_Append(value, tmp);  

  9. }  


后一步就是调用我们的回调函数!我们用call_function来搞定这件事,我们通过研究操作码CALL_FUNCTION的实现来学习怎么使用call_function 。


[py] view plaincopy

  1. /** From: Python/ceval.c **/  

  2. TARGET(CALL_FUNCTION) {  

  3.     PyObject **sp, *res;  

  4.     /** stack_pointer is a local of the main loop.  

  5.         It's the pointer to the stacktop of our frame **/  

  6.     sp = stack_pointer;  

  7.     res = call_function(&sp, oparg);  

  8.     /** call_function handles the args it consummed on the stack for us **/  

  9.     stack_pointer = sp;  

  10.     PUSH(res);  

  11.     /** Standard exception handling **/  

  12.     if (res == NULL)  

  13.         goto error;  

  14.     DISPATCH();  

  15. }  

有了上面这些信息,我们终于可以捣鼓出一个操作码DEBUG_OP的草稿了:



[py] view plaincopy

  1. TARGET(DEBUG_OP) {  

  2.     PyObject *value = NULL;  

  3.     PyObject *target = NULL;  

  4.     PyObject *res = NULL;  

  5.     PyObject **sp = NULL;  

  6.     PyObject *tmp;  

  7.     int i;  

  8.     _Py_IDENTIFIER(op_target);  

  9.   

  10.     target = _PyDict_GetItemId(f->f_globals, &PyId_op_target);  

  11.     if (target == NULL && _PyErr_OCCURRED()) {  

  12.         if (!PyErr_ExceptionMatches(PyExc_KeyError))  

  13.             goto error;  

  14.         PyErr_Clear();  

  15.         DISPATCH();  

  16.     }  

  17.     value = PyList_New(0);  

  18.     Py_INCREF(target);  

  19.     for (i = 1 ; i <= STACK_LEVEL(); i++) {  

  20.         tmp = PEEK(i);  

  21.         if (tmp == NULL)  

  22.             tmp = Py_None;  

  23.         PyList_Append(value, tmp);  

  24.     }  

  25.   

  26.     PUSH(target);  

  27.     PUSH(value);  

  28.     Py_INCREF(f);  

  29.     PUSH(f);  

  30.     sp = stack_pointer;  

  31.     res = call_function(&sp, 2);  

  32.     stack_pointer = sp;  

  33.     if (res == NULL)  

  34.         goto error;  

  35.     Py_DECREF(res);  

  36.     DISPATCH();  

  37. }  

在编写 CPython 实现的 C 代码方面我确实没有什么经验,有可能我漏掉了些细节。如果您有什么建议还请您纠正,我期待您的反馈。


编译它,成了!

一切看起来很顺利,但是当我们尝试去使用我们定义的操作码DEBUG_OP的时候却失败了。自从 2008 年之后,Python 使用预先写好的 goto(你也可以从 这里获取更多的讯息)。故,我们需要更新下 goto jump table,我们在 Python/opcode_targets.h 中做如下修改。


[py] view plaincopy

  1. /** From: Python/opcode_targets.h **/  

  2. /** Easy change since DEBUG_OP is the opcode number 1 **/  

  3. static void *opcode_targets[256] = {  

  4.     //&&_unknown_opcode,  

  5.     &&TARGET_DEBUG_OP,  

  6.     &&TARGET_POP_TOP,  

  7.     /** ... **/  

这就完事了,我们现在就有了一个可以工作的新操作码。的问题就是这货虽然存在,但是没有被人调用过。接下来,我们将DEBUG_OP注入到函数的字节码中。 



在 Python 字节码中注入操作码 DEBUG_OP

有很多方式可以在 Python 字节码中注入新的操作码:

  • 使用 peephole optimizer, Quarkslab就是这么干的

  • 在生成字节码的代码中动些手脚

  • 在运行时直接修改函数的字节码(这就是我们将要干的事儿)

为了创造出一个新操作码,有了上面的那一堆 C 代码就够了。现在让我们回到原点,开始理解奇怪甚至神奇的 Python!

本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息