1. 为什么需要GDB来调试Python?
很多Python开发者习惯了使用pdb或者IDE自带的调试器,觉得这些工具已经足够应付日常开发了。确实,对于纯Python代码的逻辑错误,pdb非常好用。但我在实际项目中遇到过几次让人头疼的情况,让我不得不请出GDB这个“大杀器”。
第一次是项目里用到了Cython编写的扩展模块,程序运行到某个复杂数据处理时会突然崩溃,只留下一句“Segmentation fault (core dumped)”。用pdb根本抓不到这种错误,因为崩溃发生在C语言层面,Python的调试器完全无能为力。第二次是使用PyTorch训练模型时,GPU内存操作出了问题,Python解释器直接卡死,没有任何错误信息输出。第三次是线上服务偶尔会僵死,进程还在但不响应请求,用常规方法根本不知道它卡在哪里。
这些场景都有一个共同点:问题出在Python解释器底层,或者Python与C扩展的交互边界上。这时候GDB就派上用场了。GDB原本是C/C++程序的调试工具,而CPython解释器本身就是用C写的,所以GDB可以直接调试Python解释器的运行状态。你可以把GDB想象成一个“外科手术刀”,它能切开Python这层“高级语言”的外壳,直接看到底层的C代码执行情况。
我刚开始用GDB调试Python时也觉得有点复杂,毕竟要同时理解Python和C两个层面的信息。但掌握之后发现,这其实是解决某些棘手问题的唯一途径。特别是当你写的Python代码需要和C扩展深度交互,或者你怀疑问题出在解释器本身时,GDB提供的视角是其他工具无法替代的。
2. 环境准备与基础配置
2.1 安装必要的软件包
在开始之前,我们需要确保系统里有正确的工具。不同的Linux发行版安装命令略有不同,我以Ubuntu和CentOS为例,这两个是我最常用的环境。
在Ubuntu上,你需要安装gdb和Python的调试符号包:
# 更新包列表
sudo apt update
# 安装gdb调试器
sudo apt install gdb -y
# 安装Python调试符号包
# 注意:这里要对应你的Python版本,比如Python 3.8就装python3.8-dbg
sudo apt install python3-dbg -y
在CentOS或RHEL系统上,命令是这样的:
# 安装gdb
sudo yum install gdb -y
# 安装Python调试符号
sudo debuginfo-install python3
有些新版的CentOS可能需要先启用debuginfo仓库:
sudo yum install yum-utils -y
sudo yum-config-manager --enable debug
sudo yum-config-manager --enable source
安装完成后,验证一下是否成功。运行gdb --version应该能看到版本信息,GDB 7.0以上版本才比较好地支持Python调试。然后你可以用这个命令检查Python调试符号:
# 启动gdb并加载Python解释器
gdb python3
# 在gdb提示符下输入
(gdb) info sharedlibrary
如果能看到python相关的库带有调试符号,比如显示“Yes”在“Debug symbols”那一列,就说明配置正确了。
2.2 加载Python的GDB扩展
CPython自带了一个GDB扩展脚本,这个脚本特别重要。它给GDB添加了一组以py-开头的命令,让你能在GDB里直接查看Python层面的信息,而不是只能看C代码。
这个脚本通常位于Python安装目录下,比如/usr/lib/debug/usr/bin/python3.8-gdb.py或者/usr/share/gdb/auto-load/usr/bin/python3.8-gdb.py。不过很多时候GDB会自动加载它,你不需要手动操作。
如果GDB没有自动加载扩展,你可能会遇到输入py-bt等命令时提示“Undefined command”。这时候可以手动加载:
# 在gdb中执行
(gdb) python
> import sys
> sys.path.insert(0, '/usr/lib/debug/usr/lib/python3.8')
> import libpython
> end
实际上更简单的方法是检查你的~/.gdbinit文件,确保包含了正确的路径。我通常会在.gdbinit里加上这么一行:
add-auto-load-safe-path /usr/bin/python3.8
这样GDB就会自动加载Python的调试扩展了。
3. 基础调试:从Python脚本崩溃开始
3.1 一个简单的崩溃示例
让我们从一个实际例子开始。假设你有这样一个Python脚本crash_demo.py:
import ctypes
# 故意制造一个非法内存访问
def cause_segmentation_fault():
# 获取一个NULL指针
null_pointer = ctypes.c_void_p(0)
# 尝试读取NULL指针指向的内存(这会导致段错误)
buffer = (ctypes.c_char * 100).from_address(null_pointer.value)
# 尝试读取内容
data = buffer[:10]
return data
if __name__ == "__main__":
print("准备触发段错误...")
result = cause_segmentation_fault()
print("这行不会被执行到")
这个脚本使用了ctypes库,直接操作内存地址。当它尝试读取地址0(NULL指针)的内容时,操作系统会阻止这个非法访问,导致段错误,Python解释器直接崩溃。
3.2 使用GDB捕获崩溃
要调试这个崩溃,我们可以用GDB直接运行Python脚本:
# 方法1:直接启动
gdb --args python3 crash_demo.py
# 方法2:先运行脚本,再附加调试器
# python3 crash_demo.py &
# 获取进程ID,假设是12345
# gdb python3 12345
在GDB中,输入run命令执行程序。当崩溃发生时,GDB会自动暂停,并显示崩溃的位置。这时候你可以输入bt(backtrace的缩写)查看C调用栈:
(gdb) run
Starting program: /usr/bin/python3 crash_demo.py
准备触发段错误...
Program received signal SIGSEGV, Segmentation fault.


3299

被折叠的 条评论
为什么被折叠?



