首页
学习
活动
专区
工具
TVP
发布

CPython 源码阅读笔记(4)

之前看了 a-python-interpreter-written-in-python 和 byterun,就想试试用 JAVA 解析 Python 生成的 pyc 文件,读取 bytecode 后在 JAVA 中实现解释执行。

要解析 pyc 文件,就需要知道其来龙去脉,以及是如何生成的。

pyc

根据平时编写 Python 代码的经验,pyc 文件是在我们 import 一个模块后生成的。

imp module

而官方文档中提到了 imp 模块是用来和 import 语句的具体实现机制交互的。其中:

find_module 函数负责到 sys.path 中寻找对应的 module

若存在需要的 module,则调用 load_module 加载对应 module

根据之前分析 CPython 源码的经验, 标准库模块中和运行逻辑相关的函数一般对应着一个 CPython 解释器中的 C 代码实现。

import.c

如 load_module 就位于 https://github.com/python/cpython/blob/2.7/Python/import.c#L1929

可以看到 load_module 会检查找到的 module 是还是 ,而这两个宏分别对应着 和 文件。

我们跟入在没有 文件时加载 源文件的 函数(只摘录了一部分)。

https://github.com/python/cpython/blob/2.7/Python/import.c#L1076

可以看到 同样会去找一次 文件,再找不到的情况下,会先解析源文件, 得到 codeobject 后调用 生成 文件,再执行 import 逻辑。

write_compiled_module

所以, 函数中应该就对应着我们的 pyc 文件生成逻辑了。

https://github.com/python/cpython/blob/2.7/Python/import.c#L951

可以看到, 文件的生成大致分下面几步:

1.创建目标 pyc 文件

2.首先调用 序列化 magic number 到文件中

3.然后序列化一个空的时间戳到文件中

4. 将 PyCodeObject 序列化到文件

5.写完 CodeObject 后,fseek 到时间戳的位置,填充真实的时间戳

其中,magic number 定义于 import.c 头部

而 和 定义于 marshal.c 中。

marshal.c

PyMarshal_WriteLongToFile

我们先来看 https://github.com/python/cpython/blob/2.7/Python/marshal.c#L462

创建了 WFILE,将打开的文件描述符赋值给 WFILE,并调用 w_long。

用于表示写入的 pyc 文件的 WFILE 结构如下。

跟入

可以看到 只是调用了四次 将一个 type 为 long ,长度为4字节的数写入到文件中。

宏简单的将传入的一字节内容写入到 WFILE->fp ,即对应的 pyc 文件中。 中的序列化写入操作都是基于 封装的。

PyMarshal_WriteObjectToFile

相对之前的 更加的复杂了,用于将 Python 对象序列化到文件中。

可以看到 调用的是 ,是 marshal 最复杂的一个函数。

https://github.com/python/cpython/blob/2.7/Python/marshal.c#L212

的主要逻辑为读取传入的 的具体类型,调用 写入一个字节的类型数据,然后调用不同的 系列函数序列化对应类型的数据。

这里我们省略其他类型的代码,重点看下 类型的处理。可以看到, 只是简单的讲 中每个类变量依次序列化到文件中,我们只需要按照 的顺序去反序列化即可得到对应的内容。

TYPE 相关的宏定义于marshal.c#L27

使用 JAVA 反序列化 文件参考 PycFile.java 。

pyc 文件结构(Struct of pyc)

根据上面的分析,我们可以得出 pyc 文件的格式如下,其中 部分为变长,需要参考 进行反序列化。

PyCodeObject

根据上面的分析,我们知道了 pyc 文件中最主要的内容为序列化的 ,接下来我们就分析一下 的结构,以及如何生成及如何被解释执行。

定义于 Include/code.h#L10

上面 中可以看到 文件的 是调用 生成的。

parse_source_module

我们在第一篇CPython源码阅读笔记(1) 中曾经分析从 开始的代码生成流程,这里的逻辑和之前一致。

即在 阶段划分好了 CFG ,然后按照 CFG 遍历生成 。其中最外层为一个入口的 Block,嵌套的生成多个 code object。

代码生成测试

创建 test.py 如下

在同级目录启动一个 Python 终端。

可以看到 test 函数的生成了单独的一个 code object。

查看最外层 code object 的 后找到了对应的 test 函数的 code object 。

接着我们可以根据 的各个属性的名字猜测并查看其内容。

调试

按照第一篇文章中的方法,我们可以试着调试一下 test.py 的编译过程。

compiler_mod

在编译的入口函数 处下断点,运行 。

compiler_body

单步跟入 函数,可以看到传入的 mod 为 ,所以接下来跟入。

在 处下断点,然后跟入该函数。

可以看到, 只是简单的讲 stmts 中的元素取出,通过 宏进行代码生成。

展开其实就是。

单步跟入循环中的 VISIT 调用,查看传入的 stmt 参数,为 中定义的 ,即 stmt AST Node(stmt 的语法树节点)。

中定义了 Python 中 stmt 的类型。 https://github.com/python/cpython/blob/2.7/Include/Python-ast.h#L62

compiler_visit_stmt

https://github.com/python/cpython/blob/2.7/Python/compile.c#L2074

因为这里第一次传入的 stmt 的类型为 ,这里会调用 。

compiler_function

跟入 , 这里即是真正的代码生成逻辑。

https://github.com/python/cpython/blob/2.7/Python/compile.c#L1351

这里逻辑比较复杂,就不贴调试的过程了。大致的流程为

将 FuncDef AST Node 中的一些 metadata 存储到 compiler 对象中。

调用 assemble 将函数体生成单独的 code object。

调用 生成 和 两个opcode。

调用 生成 opcode。

至此生成了下面的字节码

对应源码中的

Py_OPCODE

字节码对应的数字定于于 opcode.h 。

其中 宏定义了字节码是否带有参数(通过判断字节码对应的数字是否大于指定的值)。

在 Python2.7 中这个值为 90

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181212G0F58C00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券