到目前为止,我们已经生成了 CodeObject 对象,Python 内置了 dis 模块可以获得可阅读的汇编代码。52 节已经对 dis 模块的使用做了演示:

def fun():
print("hello world.")
import dis
print(dis.dis(fun))

上面的代码保存在 test.py, 用 python3 运行它,可以得到输出:

2 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('hello world.')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
None

并不复杂。Python 的底层就是 Python VM,而具体的字节码在极大程度上说明了 Python 到底在做什么,因此我们需要更便捷的字节码反汇编技术,最好能在不侵入已有代码的前提下实现 “反汇编”。

PYC 文件与 CodeObject

上一节中,我们介绍了函数在 CodeObject 中的实现。Python 的函数、class、lambda、推导式等子代码块都会被编译为 CodeObject,然后保存在上一级代码块的 consts 列表中。Python 代码文件作为最顶层的 Module 最后也被编译为一个 CodeObject,可以说 Module 的 CodeObject 包含了 Pythong 程序所需要的一切信息。

回顾从 Python 代码文件 -> AST -> Symbol table -> CodeObject 的过程,实在是颇费周章,CodeObject 得来实在不易,Python 本着勤俭持家的传统,当然也不会浪费这一劳动成果,必须会做缓存工作。

如果编写过多个文件构成的 Python 程序就会发现,在每一个 Module 的目录中都有一个 "__pycache__" 目录,里面是一些 "xxx.xxx.pyc" 文件,它们其实就是前面好不容易生成的 CodeObject 的文件形态。CodeObejct 是 Python 的内部对象,如何用文件表示它呢?最直接的想法是将 CodeObject 转换为 json 之类的数据格式,而 Python 自己设计了一种更加高效的文件格式 pyc。不过我们的研究目标是 CodeObject,只需要知道 pyc 文件是另一种形态的 CodeObject,它们二者之间可以通过一些手段转换。

上图第一行和第二行的执行效果是相同的。

每当 import 一个模块的时候,模块文件编译好的 CodeObject 就会被 “序列化” 为文件流,保存在某个 pyc 文件里,那么直到源码被再次改变之前,只需要加载这个 pyc 文件,还原 CodeObject 就能得到同样的结果。

从 PYC 文件到 pyasm

如果不侵入(不修改) py 文件,那么就需要在文件外找到 CodeObject,再进行反汇编。既然 pyc 文件是 CodeObject 的文件表示,那么 pyc 文件中也等效的包含了 Python 程序所需要的一切:名称、常量、字节码等等。

可以反汇编 pyc 文件吗?

xdis 模块提供了从 pyc 到 pyasm(一种汇编格式)的转换。pip 安装 xdis 后,就可以用这样的指令反汇编 pyc 文件:

pydisasm -F xasm xxxx.pyc

虽然在一个 py 文件中 import 另一个 py 文件会出发对这个文件的缓存策略,生成它的 pyc 文件,下面的指令主动把 py 文件编译为 pyc 文件显然更方便一些:

python -m compileall xxxx.py

组合使用上面两个指令,我们可以得到前几节例程的汇编代码:

python -m compileall test.py
pydisasm -F xasm __pycache__/test.cpython-38.pyc > test.pyasm # 将结果写入文件

查看 test.pyasm 文件:

截图不是完整的汇编文件。其中以 “#” 开头的是一些附加信息。以空行分割,第一部分是一些版本信息,第二部分是 func 的 CodeObject,最后是 Moudle CodeObject 的一部分。

可以看到 pyasm 文件中如何表示 CodeObject,简而言之:数据 + 字节码

所有的数据都包含在 “#” 开头的行中;之后是汇编代码,它们由字节码转换而来,并且 pydisasm 自动将参数的值也显式的放在括号里,便于阅读,实际上字节码中的参数只有括号外面的数字。

借助 pydisasm 我们可以非常容易的从源码获得对应的汇编代码,了解人类编写的代码,对于 Python VM 到底是怎样的。

从 pyasm 编译到 pyc 文件

pydisasm 实现了从 pyc 文件到 pyasm 的转换,工具 pyc-xasm 可以直接将 pyasm 转换为 pyc 文件。

pyc 文件也是一种有效的 Python 文件,可以被直接执行。也就是说只要你愿意,绕过 Python 源码,修改 pyasm 文件,然后编译为 pyc 文件,也可以产生有效的修改,并获得预期的结果。这在某些没有源码,但需要修改功能的时候非常有用。

但是,pydisasm 和 pyc-xasm 目前无法在 python 3.8 下很好的工作,似乎对低版本支持良好,我尝试运行在 python 3.6 下是可以的。有兴趣的朋友可以试一试。

虽然有点遗憾,但至少对 Python 3.8 的研究可以继续推进了,能方便的看到 pyasm 汇编代码就很不错了。