likes
comments
collection
share

带你一步步调试CPython源码(一、主流程)

作者站长头像
站长
· 阅读数 4

很早以前就打算写一系列关于cpython源码解析的文章了,奈何水平不够迟迟没有动笔。正值新年伊始,我打算今年是时候实现我这个想法了。一方面能分享给大家自己的学习心得,另一方面能督促自己持续创造,这种好事何乐而不为呢?

很多时候,阅读大型项目源码就像打galgame,分支繁多,逻辑复杂,从main函数一头扎进去很容易迷失在代码中。所以在第一篇文章中我会串一下流程,从交互模式(interactive)直观的体会cpython是如何解析并执行Python原文件的。当遇到关键分支时,我会把它作为存档在后续的文章中详细展开解释。

本系列文章为CPython源码解析的一周目,集中研究交互模式(interactive)下cpython运行方式。在二周目会介绍在文件模式(file)下cpython是如何运作的。

因为是在Windows环境下进行,调试的工具很简单。

  1. git:用来下载cpython依赖,比如sqlite、bzip、zlib等

  2. 低版本的Python:用来生成部分编译文件

  3. Visual Studio 2022:编译运行cpython项目,调试也离不开它

本系列采用了GitHub上当前最新的cpython分支,Python 3.13.0 alpha2版本。可能仍然有部分代码无法和读者下载的保持一致,我会用截图展示。

一、编译项目

Visual Studio 2022启动!

首先打开解决方案,双击PCBuild目录下的pcbuild.sln工程文件。

带你一步步调试CPython源码(一、主流程)

然后运行方式选择Debug模式,编译到Win32或者x64平台,点击“本地Windows调试器”调试即可启动项目。第一次运行会下载依赖,请保持网络通畅。

带你一步步调试CPython源码(一、主流程)

二、交互模式主流程

常见的Python运行模式有两种,一种是以py文件的模式运行,另一种以交互的模式运行。交互的模式也叫REPL模式,我们首先介绍这种模式。

把断点打到Modules目录下main.c文件的第731行,再运行程序。

带你一步步调试CPython源码(一、主流程)

程序会停留在Py_Main这个函数内。这个函数是cpython的主入口。该函数就做一件事,调用真正的入口函数pymain_main。该函数是Windows平台和Linux平台共用的入口函数。

带你一步步调试CPython源码(一、主流程)

pymain_main函数核心逻辑有两个,一个是初始化解释器需要的参数pymain_init函数,另一个是运行解释器Py_RunMain函数。

pymain_init函数处先存档SAVE 1。直接看Py_RunMain函数。

存档的意思是这里的逻辑太复杂,需要以后用大篇幅文章来介绍,所以暂时做个记号。记号用斜体加粗的SAVE+序号表示,后面会有相应的读档LOAD+序号,表示详细的介绍这块逻辑。

main.c文件553行打上断点,让程序停在这一行。

带你一步步调试CPython源码(一、主流程)

Py_RunMain函数会调用pymain_run_python函数。在这个函数内,解释器会根据前面的初始化信息,包括命令行传参、环境变量和配置文件,决定以何种模式运行。由于我们之前运行程序时没有提供参数,所以它会以REPL的模式运行。

让程序运行到576行。它会调用pymain_import_readline函数,即加载两个python模块,readline和rlcompleterSAVE 2

带你一步步调试CPython源码(一、主流程)

让程序运行到599行,调用pymain_header函数。这个函数会像下面这样打印Python程序的头信息,不影响主业务。

带你一步步调试CPython源码(一、主流程)

接着是5个分支的判断语句,它决定了Python以何种模式运行SAVE 3。由于该程序运行的时候没有带任何参数,所以走最后一个分支,即交互模式(interactive)。

带你一步步调试CPython源码(一、主流程)

在Python目录下pythonrun.c文件中的第95行打上断点,并让程序运行到这里。在经过一系列的初始化配置(包括执行初始脚本SAVE 4交互钩子SAVE 5异步通知SAVE 6审计事件SAVE 7),程序开始进入执行命令行代码的步骤。

带你一步步调试CPython源码(一、主流程)

在短暂处理了一下文件名称后,马上调用_PyRun_AnyFileObject函数执行真正的代码。

pythonrun.c的第114行打上断点,让程序运行到这一行。

带你一步步调试CPython源码(一、主流程)

该函数首先通过读取全局变量变量获得REPL模式下的prompt,默认的prompt分别是“>>>”和“...”。_Py_ID是获取全局变量的宏,在整个项目中非常常见。全局变量在cpython编译前通过bat脚本根据配置写入SAVE 8

让程序运行到138行,准备调用PyRun_InteractiveOneObjectEx函数。这是REPL模式下最核心的函数。

带你一步步调试CPython源码(一、主流程)

进入该函数。该函数先申请了Python运行环境的内存SAVE 9

带你一步步调试CPython源码(一、主流程)

然后调用pyrun_one_parse_ast函数编译用户输入的Python代码。执行完这个函数程序会被挂起且控制台会出现prompt,表示等待用户输入。该函数会将Python代码解析成AST树SAVE 10,存储到mod变量内。

带你一步步调试CPython源码(一、主流程)

当用户输入完并按下回车后程序恢复执行。

带你一步步调试CPython源码(一、主流程)

在第280行,程序会先导入__main__模块。由于是在命令行中输入程序,__main__模块本身是个dummy,这里只是为了填充后面的run_mod函数中的参数。同样,在第285行中,程序获取该模块的__dict__对象也是为了填充参数。

在第287行,程序调用run_mod函数解析AST树并生成字节码,然后根据字节码运行程序SAVE 11。执行完后命令行会输出运行结果。

带你一步步调试CPython源码(一、主流程)

然后程序释放了先前申请的Python运行内存,并处理了IO缓存。

当跳出这个函数后,会发现函数返回值ret为0,不为EOF(11)。因此,该程序会循环调用PyRun_InteractiveOneObjectEx函数处理用户的输入,直到出现EOF为止。

带你一步步调试CPython源码(一、主流程)

在命令行中输入exit()退出程序。

带你一步步调试CPython源码(一、主流程)

ret会返回为-1,程序判断后会直接调用PyErr_Print()退出程序。

带你一步步调试CPython源码(一、主流程)

至此,整个交互模式下的cpython运行流程就介绍完了。整个流程简单清晰明了,没有多余的步骤。后续二周目文件模式也是如此,我会在以后的篇幅介绍。

回过来看,核心函数PyRun_InteractiveOneObjectEx主要分为两个步骤,一个是编译Python源码生成AST树,另一个是解析AST树并生成字节码,然后执行。

在生成AST树的时候LOAD 10,程序会先将输入的字符串转化为Python的str对象,即PyUnicode。然后调用_PyParser_ASTFromFile函数,将字符串转化为AST树并返回SAVE 12

带你一步步调试CPython源码(一、主流程)

AST树指的是抽象语法树,一种通过树状图描绘代码结构的抽象表示。不止是Python,几乎所有的高级语言都会将程序抽象成AST树做进一步的解析。AST树由解释器(编译器)前端处理。

标准的解释器在处理源码时分为这几个阶段——词法分析、语法分析、IR生成与优化、IR执行,其中解释器前端指词法分析到IR生成与优化,而后端指IR执行。Python解释器的各个阶段没有明显的界限,词法分析和语法分析的逻辑全都在Parser/parser.cParser/pegen.c等几个文件中。Python的字节码可以看作是中间表示IR。

从代码角度来看,上图中的_PyParser_ASTFromFile函数是语法分析器的入口函数,它最终会调用parser.c中的函数生成AST树。

在AST树解析成字节码并运行的逻辑中LOAD 11,cpython会调用run_mod函数作为入口函数。

把断点打到pythonrun.c的1740行,让程序运行到这里。

带你一步步调试CPython源码(一、主流程)

程序会调用_PyAST_Compile函数将刚刚生成好的AST树转换成Python字节码SAVE 13

现在进入解释器后端部分,继续让程序运行到第1749行,它会调用run_eval_code_obj函数执行Python字节码SAVE 14,并将结果输出到屏幕上。

将断点达到Python/compile.c文件的第550行,让程序进入_PyAST_Compile函数LOAD 13

带你一步步调试CPython源码(一、主流程)

程序会先初始化一个compiler。它是整个cpython前端最核心的结构体,负责记录在编译过程中使用到的各种变量,也记录了最终生成的Python字节码SAVE 15

然后程序执行compiler_mod函数生成字节码。最后调用compiler_free释放compiler。

进入compiler_mod函数。

带你一步步调试CPython源码(一、主流程)

程序会先调用了compiler_codegen函数,它将AST树转换成原始的Python字节码SAVE 16。然后程序调用了optimize_and_assemble函数优化Python字节码SAVE 17。分别对应解释器的IR生成与IR优化两个阶段。

第一篇文章就到此结束了,本文还遗留了大量的存档,我在后续的文章会逐一介绍这些细节。

转载自:https://juejin.cn/post/7320655412075118602
评论
请登录