- 堆栈的内容
- 执行 DEBUG_OP 的帧对象信息
所以呢,我们的操作码需要做的事情是:
- 找到回调函数
- 创建一个包含堆栈内容的列表
- 调用回调函数,并将包含堆栈内容的列表和当前帧作为参数传递给它
听起来挺简单的,现在开始动手吧!声明:下面所有的解释说明和代码是经过了大量段错误调试之后总结得到的结论。首先要做的是给操作码定义一个名字和相应的值,因此我们需要在Include/opcode.h中添加代码。
这部分工作就完成了,现在我们去编写操作码真正干活的代码。
实现 DEBUG_OP
在考虑如何实现DEBUG_OP之前我们需要了解的是DEBUG_OP提供的接口将长什么样。 拥有一个可以调用其他代码的新操作码是相当酷眩的,但是究竟它将调用哪些代码捏?这个操作码如何找到回调函数的捏?我选择了一种最简单的方法:在帧的全局区域写死函数名。那么问题就变成了,我该怎么从字典中找到一个固定的 C 字符串?为了回答这个问题我们来看看在 Python 的 main loop 中使用到的和上下文管理相关的标识符__enter__和__exit__。
我们可以看到这两标识符被使用在操作码SETUP_WITH中:
现在,看一眼宏_Py_IDENTIFIER的定义
嗯,注释部分已经说明得很清楚了。通过一番查找,我们发现了可以用来从字典找固定字符串的函数_PyDict_GetItemId,所以我们操作码的查找部分的代码就是长这样滴。
为了方便理解,对这一段代码做一些说明:
- f是当前的帧,f->f_globals是它的全局区域
- 如果我们没有找到op_target,我们将会检查这个异常是不是KeyError
- goto error;是一种在 main loop 中抛出异常的方法
- PyErr_Clear()抑制了当前异常的抛出,而DISPATCH()触发了下一个操作码的执行
下一步就是收集我们想要的堆栈信息。
最后一步就是调用我们的回调函数!我们用call_function来搞定这件事,我们通过研究操作码CALL_FUNCTION的实现来学习怎么使用call_function。
有了上面这些信息,我们终于可以捣鼓出一个操作码DEBUG_OP的草稿了:
在编写 CPython 实现的 C 代码方面我确实没有什么经验,有可能我漏掉了些细节。如果您有什么建议还请您纠正,我期待您的反馈。
编译它,成了!
一切看起来很顺利,但是当我们尝试去使用我们定义的操作码DEBUG_OP的时候却失败了。自从 2008 年之后,Python 使用预先写好的 goto(你也可以从 这里获取更多的讯息)。故,我们需要更新下 goto jump table,我们在 Python/opcode_targets.h 中做如下修改。
这就完事了,我们现在就有了一个可以工作的新操作码。唯一的问题就是这货虽然存在,但是没有被人调用过。接下来,我们将DEBUG_OP注入到函数的字节码中。
在 Python 字节码中注入操作码 DEBUG_OP
有很多方式可以在 Python 字节码中注入新的操作码:
- 使用 peephole optimizer, Quarkslab就是这么干的
- 在生成字节码的代码中动些手脚
- 在运行时直接修改函数的字节码(这就是我们将要干的事儿)
为了创造出一个新操作码,有了上面的那一堆 C 代码就够了。现在让我们回到原点,开始理解奇怪甚至神奇的 Python!
我们将要做的事儿有:
- 得到我们想要追踪函数的 code object
- 重写字节码来注入DEBUG_OP
- 将新生成的 code object 替换回去
和 code object 有关的小贴士
如果你从没听说过 code object,这里有一个简单的 介绍网路上也有一些相关的文档可供查阅,可以直接Ctrl+F查找 code object
还有一件事情需要注意的是在这篇文章所指的环境中 code object 是不可变的:
但是不用担心,我们将会找到方法绕过这个问题的。
使用的工具
为了修改字节码我们需要一些工具:
- dis模块用来反编译和分析字节码
- dis.BytecodePython 3.4 新增的一个特性,对于反编译和分析字节码特别有用
- 一个能够简单修改 code object 的方法
用dis.Bytecode反编译 code bject 能告诉我们一些有关操作码、参数和上下文的信息。
为了能够修改 code object,我定义了一个很小的类用来复制 code object,同时能够按我们的需求修改相应的值,然后重新生成一个新的 code object。
这个类用起来很方便,解决了上面提到的 code object 不可变的问题。
测试我们的新操作码
我们现在拥有了注入DEBUG_OP的所有工具,让我们来验证下我们的实现是否可用。我们将我们的操作码注入到一个最简单的函数中:
看起来它成功了!有一行代码需要说明一下new_nop_code.co_stacksize += 3
- co_stacksize 表示 code object 所需要的堆栈的大小
- 操作码DEBUG_OP往堆栈中增加了三项,所以我们需要为这些增加的项预留些空间
现在我们可以将我们的操作码注入到每一个 Python 函数中了!
重写字节码
正如我们在上面的例子中所看到的那样,重写 Pyhton 的字节码似乎 so easy。为了在每一个操作码之间注入我们的操作码,我们需要获取每一个操作码的偏移量,然后将我们的操作码注入到这些位置上(把我们操作码注入到参数上是有坏处大大滴)。这些偏移量也很容易获取,使用dis.Bytecode ,就像这样 。
基于上面的例子,有人可能会想我们的insert_op_debug会在指定的偏移量增加一个"\x00",这尼玛是个坑啊!我们第一个DEBUG_OP注入的例子中被注入的函数是没有任何的分支的,为了能够实现完美一个函数注入函数insert_op_debug我们需要考虑到存在分支操作码的情况。
Python 的分支一共有两种:
- 绝对分支:看起来是类似这样子的Instruction_Pointer = argument(instruction)
- 相对分支:看起来是类似这样子的Instruction_Pointer += argument(instruction)
相对分支总是向前的
我们希望这些分支在我们插入操作码之后仍然能够正常工作,为此我们需要修改一些指令参数。以下是其逻辑流程:
- 对于每一个在插入偏移量之前的相对分支而言
- 如果目标地址是严格大于我们的插入偏移量的话,将指令参数增加 1
- 如果相等,则不需要增加 1 就能够在跳转操作和目标地址之间执行我们的操作码DEBUG_OP
- 如果小于,插入我们的操作码的话并不会影响到跳转操作和目标地址之间的距离
做了那么多年开发,自学了很多门编程语言,我很明白学习资源对于学一门新语言的重要性,这些年也收藏了不少的Python干货,对我来说这些东西确实已经用不到了,但对于准备自学Python的人来说,或许它就是一个宝藏,可以给你省去很多的时间和精力。
别在网上瞎学了,我最近也做了一些资源的更新,只要你是我的粉丝,这期福利你都可拿走。
我先来介绍一下这些东西怎么用,文末抱走。
(1)Python所有方向的学习路线(新版)
这是我花了几天的时间去把Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。
最近我才对这些路线做了一下新的更新,知识体系更全面了。
(2)Python学习视频
包含了Python入门、爬虫、数据分析和web开发的学习视频,总共100多个,虽然没有那么全面,但是对于入门来说是没问题的,学完这些之后,你可以按照我上面的学习路线去网上找其他的知识资源进行进阶。
(3)100多个练手项目
我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了,只是里面的项目比较多,水平也是参差不齐,大家可以挑自己能做的项目去练练。
(4)200多本电子书
这些年我也收藏了很多电子书,大概200多本,有时候带实体书不方便的话,我就会去打开电子书看看,书籍可不一定比视频教程差,尤其是权威的技术书籍。
基本上主流的和经典的都有,这里我就不放图了,版权问题,个人看看是没有问题的。
(5)Python知识点汇总
知识点汇总有点像学习路线,但与学习路线不同的点就在于,知识点汇总更为细致,里面包含了对具体知识点的简单说明,而我们的学习路线则更为抽象和简单,只是为了方便大家只是某个领域你应该学习哪些技术栈。
(6)其他资料
还有其他的一些东西,比如说我自己出的Python入门图文类教程,没有电脑的时候用手机也可以学习知识,学会了理论之后再去敲代码实践验证,还有Python中文版的库资料、MySQL和HTML标签大全等等,这些都是可以送给粉丝们的东西。
这些都不是什么非常值钱的东西,但对于没有资源或者资源不是很好的学习者来说确实很不错,你要是用得到的话都可以直接抱走,关注过我的人都知道,这些都是可以拿到的。