[Python] 从另一个视角看Python
最近在B站上刷到了UP主码农高天讲解Python的视频,UP主将Python的细节讲述得非常有意思。受此启发,我准备在自己的网站上记录一些我对Python的理解,暂定计划是写一些Python解释器的实现细节,用到的解释器版本是CPython v3.10.4,系统环境是Ubuntu 20.04。
这篇文章尝试编译一个debug版的Cpython解释器,写一个最简单的Python脚本,用GDB来调试解释器执行脚本的过程,分析解释器的函数栈。
准备工作⌗
我们需要在Linux环境下编译并调试一个CPython解释器,所以GCC编译器、Make和GDB调试器、解释器源码都是必须的,解释器的一些依赖库是可选的。
下载CPython源码⌗
CPython解释器的源码可以从Github下载,国内可以从Gitee镜像。
# 从Github下载
git clone --depth 1 --branch v3.10.4 https://github.com/python/cpython.git
# 国内从Gitee镜像加速下载
git clone --depth 1 --branch v3.10.4 https://gitee.com/mirrors/cpython.git
下面列出了CPython源码中各个子目录的作用,更详细的介绍可以看官方文档:
Doc 文档
Grammar EBNF语法定义
Include 解释器所用的头文件
Lib 标准库中用Python实现的部分
Mac Mac系统特定的代码
Misc 杂项
Modules 标准库中用C实现的部分
Objects 所有内置类型的实现
PC Windows系统特定的代码
PCbuild MSVC构建文件
Parser 语法分析器
Programs 可执行文件的代码
Python Python运行时,包含了main函数
Tools 维护Python使用的工具
安装编译工具⌗
apt-get install gcc build-essential
安装debug工具⌗
apt-get install gdb
先用上面的命令安装GDB,为了方便debug,我们还需要一款IDE或者编辑器,我这里选用VSCode和C/C++插件。
安装依赖⌗
$ sudo apt-get update
$ sudo apt-get build-dep python3
$ sudo apt-get install pkg-config
以下一些依赖可以按需安装
$ sudo apt-get install build-essential gdb lcov pkg-config \
libbz2-dev libffi-dev libgdbm-dev libgdbm-compat-dev liblzma-dev \
libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev \
lzma lzma-dev tk-dev uuid-dev zlib1g-dev
编译debug版的CPython解释器⌗
在安装了上面的工具后,我们可以开始编译debug版的CPython解释器。
cd cpython
./configure --with-pydebug
make -j4 -s
用4线程并行编译,耗时大约在一分钟以内。编译完成后,会在当前目录下生成名为python
的可执行文件,这个文件就是我们的解释器,可以用GDB来调试。
从Hello world开始⌗
解释器主程序入口⌗
我们可以用grep
命令来搜索main
函数,另外还有一个更简单的办法,用gdb调试python
:
gdb python
输入start
开始运行解释器,可以看到程序暂停在了Programs/python.c
文件的14行,这就是main
函数。我们找到源码文件Programs/python.c
中,可以看到main函数的内容只有一行:
return Py_BytesMain(argc, argv);
为了更加直观,接下来我们改用VSCode的图形化界面进行调试。
调试hello world
脚本⌗
首先写一个python文件debug/main.py
,内容只有一行,用来输出字符串Hello world!
。
print("Hello world!")
我们想要在执行print
函数的时候暂停解释器,来查看解释器的调用栈,所以我们需要在print
函数的C代码中设置一个断点。print
函数的实现是Python/bltinmodule.c
文件中的builtin_print
函数,我们在这个函数中设置一个断点,接下来就可以开始用VSCode调试了,下面是我用的launch.json
配置:
{
"version": "0.2.0",
"configurations": [
{
"type": "cppdbg",
"request": "launch",
"name": "python",
"program": "${workspaceRoot}/python",
"cwd": "${workspaceRoot}",
"args": ["debug/main.py"]
}
]
}
效果和在shell里执行下面这行命令一样:
gdb --args python debug/main.py
接下来开始debug,程序会在builtin_print
函数中暂停,如下图:
点击这里放大图片
第2001行的PyFile_WriteObject
函数就是将字符串写到标准输出的函数,这里args
中的唯一一个元素就是我们在Python脚本中写的Hello world!
字符串,解释器层面的字符串是用PyUnicodeObject
表示的,我们可以在debug窗口中输入PyUnicode_AsUnicode(args[i])
将PyUnicodeObject
转为C字符串,证实了这个被打印的字符串就是我们在Pyhon代码中写的"Hello world!"
字符串。
如果程序继续执行下去,可以看到在builtin_print
函数中继续输出了一个换行符\n
然后返回,解释器会接下去做一些收尾工作,例如刷新缓冲区、释放内存、最后结束运行。这就是我们通常在shell中执行python debug/main.py
时CPython解释器所做的工作。
查看调用栈里的其他函数可以看到解释器执行脚本的主要过程,例如:
-
main.c
文件中的pymain_main
函数,首先用pymain_init
函数初始化了运行时,然后开始运行Py_RunMain()
。 -
在
run_mod
函数中,调用_PyAST_Compile
函数将文件编译为PyCodeObject
字节码对象,然后调用run_eval_code_obj
执行字节码。 -
解释器执行字节码的主循环都在
ceval.c
文件的_PyEval_EvalFrameDefault
函数中。
参考资料⌗
- Python开发者指南: https://devguide.python.org/setup/
- Python extension patterns: https://pythonextensionpatterns.readthedocs.io
- RealPython: https://realpython.com/cpython-source-code-guide/
- VizTracer: https://github.com/gaogaotiantian/viztracer