最近我在研究 Python 的执行模型。我对 Python 内部的东西挺好奇,比如:类似 YIELDVALUE 和 YIELDFROM 此类操作码的实现;列表表达式、生成器表达式以及一些有趣的Python 特性是怎么编译的;异常触发之时,字节码层面发生了什么。
阅读 CPython 代码是相当有益的,但是我觉得要完全理解字节码的执行和堆栈的变化,光读源码是远远不够的。GDB 是个好选择,但我很懒,只想写一些高级的接口和 Python 代码。
因此我想做一个字节码级别的追踪 API,就像 sys.settrace 所提供的那样,但颗粒度更出色。这种练习完美地锻炼了我将 C 转化为 Python 的能力。我们所需的有以下几点:
- 一个新的CPython解释器操作码
- 一种将操作码注入Python字节码的方法
- 一些Python代码,用于在Python的角度处理操作码
注:在这篇文章中,Python版本是3.5
一种新的CPython操作码
我们的新操作码:DEBUG_OP
这个新的操作码DEBUG_OP是我第一次尝试用C代码来实现CPython。我会尽量使之保持简洁。
我想要达到的目的是,无论我的操作码何时执行,都有一种方式调用一些Python代码,与此同时,我们也想能够追踪一些与执行上下文有关的数据。我们的操作码会把这些信息当作参数传递给我们的回调函数。我能辨识出的有用信息如下:
- 堆栈的内容
- 执行DEBUG_OP的帧对象信息
因此我们 DEBUG_OP 所需做的所有事情是:
- 找到回调函数
- 创建堆栈内容的列表
- 调用回调函数,并将堆栈列表和当前帧作为参数传给它
听起来挺简单啊,让我们开始吧!
声明:以下的解释和代码都是经过大量段错误得到的。首先要做的事情,就是给我们的操作码命名并赋值,因此我们需要在Include/opcode.h中添加
1 2 3 4 5 6 7 8 9 10 11 12 |
/** My own comments begin by '**' **/ /** From: Includes/opcode.h **/ /* Instruction opcodes for compiled code */ /** We just have to define our opcode with a free value 0 was the first one I found **/ #define DEBUG_OP 0 #define POP_TOP 1 #define ROT_TWO 2 #define ROT_THREE 3 |
这简单的部分是完成了,现在我们必须真正去编写我们的操作码。
实现 DEBUG_OP
在考虑实现DEBUG_OP之前,我们需要问我们自己的第一个问题是:“我的接口应该是什么样的?”
拥有一个可以调用其他代码的新操作码是很酷的,但是它实际上会调用哪些代码呢?这个操作码怎么找到回调函数呢?我选择了一种看起来最简单的解决方案,在帧的全局区域写死函数名。
现在问题就变成了:“我怎么从一个字典中找到一个不变的C字符串?”
为了回答这个问题,我们可以寻找一些用在Python的main循环中的用到的和上下文管理相关的标识符**enter**和**exit**。
我们可以看到标识符被用在 SETUP_WITH 操作码中。
1 2 3 4 5 6 7 |
/** From: Python/ceval.c **/ TARGET(SETUP_WITH) { _Py_IDENTIFIER(__exit__); _Py_IDENTIFIER(__enter__); PyObject *mgr = TOP(); PyObject *exit = special_lookup(mgr, &PyId___exit__), *enter; PyObject *res; |
现在,看一下_Py_IDENTIFIER
的宏定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/** From: Include/object.h **/ /********************* String Literals ****************************************/ /* This structure helps managing static strings. The basic usage goes like this: Instead of doing r = PyObject_CallMethod(o, "foo", "args", ...); do _Py_IDENTIFIER(foo); ... r = _PyObject_CallMethodId(o, &PyId_foo, "args", ...); PyId_foo is a static variable, either on block level or file level. On first usage, the string "foo" is interned, and the structures are linked. On interpreter shutdown, all strings are released (through _PyUnicode_ClearStaticStrings). Alternatively, _Py_static_string allows to choose the variable name. _PyUnicode_FromId returns a borrowed reference to the interned string. _PyObject_{Get,Set,Has}AttrId are __getattr__ versions using _Py_Identifier*. */ typedef struct _Py_Identifier { struct _Py_Identifier *next; const char* string; >。 最近我在研究 Python 的执行模型。我对 Python 内部的东西挺好奇,比如:类似 YIELDVALUE 和 YIELDFROM 此类操作码的实现;列表表达式、生成器表达式以及一些有趣的Python 特性是怎么编译的;异常触发之时,字节码层面发生了什么。 阅读 CPython 代码是相当有益的,但是我觉得要完全理解字节码的执行和堆栈的变化,光读源码是远远不够的。GDB 是个好选择,但我很懒,只想写一些高级的接口和 Python 代码。 因此我想做一个字节码级别的追踪 API,就像 sys.settrace 所提供的那样,但颗粒度更出色。这种练习完美地锻炼了我将 C 转化为 Python 的能力。我们所需的有以下几点:
注:在这篇文章中,Python版本是3.5 一种新的CPython操作码我们的新操作码:DEBUG_OP这个新的操作码DEBUG_OP是我第一次尝试用C代码来实现CPython。我会尽量使之保持简洁。 我想要达到的目的是,无论我的操作码何时执行,都有一种方式调用一些Python代码,与此同时,我们也想能够追踪一些与执行上下文有关的数据。我们的操作码会把这些信息当作参数传递给我们的回调函数。我能辨识出的有用信息如下:
因此我们 DEBUG_OP 所需做的所有事情是:
听起来挺简单啊,让我们开始吧! 声明:以下的解释和代码都是经过大量段错误得到的。首先要做的事情,就是给我们的操作码命名并赋值,因此我们需要在Include/opcode.h中添加
这简单的部分是完成了,现在我们必须真正去编写我们的操作码。 实现 DEBUG_OP在考虑实现DEBUG_OP之前,我们需要问我们自己的第一个问题是:“我的接口应该是什么样的?” 拥有一个可以调用其他代码的新操作码是很酷的,但是它实际上会调用哪些代码呢?这个操作码怎么找到回调函数呢?我选择了一种看起来最简单的解决方案,在帧的全局区域写死函数名。 现在问题就变成了:“我怎么从一个字典中找到一个不变的C字符串?” 为了回答这个问题,我们可以寻找一些用在Python的main循环中的用到的和上下文管理相关的标识符**enter**和**exit**。 我们可以看到标识符被用在 SETUP_WITH 操作码中。
现在,看一下
|