做一个字节码追踪器,从内部理解 Python 的执行过程

581 查看

最近我在研究 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中添加

这简单的部分是完成了,现在我们必须真正去编写我们的操作码。

实现 DEBUG_OP

在考虑实现DEBUG_OP之前,我们需要问我们自己的第一个问题是:“我的接口应该是什么样的?”

拥有一个可以调用其他代码的新操作码是很酷的,但是它实际上会调用哪些代码呢?这个操作码怎么找到回调函数呢?我选择了一种看起来最简单的解决方案,在帧的全局区域写死函数名。

现在问题就变成了:“我怎么从一个字典中找到一个不变的C字符串?”

为了回答这个问题,我们可以寻找一些用在Python的main循环中的用到的和上下文管理相关的标识符**enter**和**exit**。

我们可以看到标识符被用在 SETUP_WITH 操作码中。

现在,看一下_Py_IDENTIFIER 的宏定义: