Python 开发必备:GDB 调试 CPython 扩展与内部代码全攻略
在 Python 开发中,当涉及到 CPython 扩展或深入调试 CPython 解释器本身的底层问题时,GDB 调试器结合python-gdb.py
扩展能发挥强大作用。本教程详细介绍如何利用这一工具组合进行高效调试,涵盖前提条件、环境设置、扩展功能使用及与 GDB 命令协同操作等内容,助力开发者深入理解和解决 Python 底层问题。
一、引言
在 Python 开发的复杂场景中,尤其是涉及 CPython 扩展或 CPython 解释器内部代码时,遇到崩溃、死锁等底层问题难以排查。GDB 作为强大的底层调试器,结合python-gdb.py
扩展,能为开发者提供深入剖析程序运行状态的能力,帮助快速定位和解决问题。
二、前提条件
- GDB 版本:需要 GDB 7 或更高版本。若使用较低版本,可参考 Python 3.11 或更低版本源代码中的
Misc/gdbinit
文件进行相关配置。 - 调试信息:确保拥有针对 Python 和正在调试的扩展的 GDB 兼容调试信息,这是准确调试的基础。
python-gdb.py
扩展:该扩展与 Python 一同构建,但可能单独发布或未发布。不同系统获取和设置方式不同。
三、环境设置
(一)从源代码构建 Python 的设置
从源代码构建 CPython 时,调试信息通常可用,且在代码库根目录会生成python-gdb.py
文件。要激活扩展支持,需将包含该文件的目录添加到 GDB 的 “auto-load-safe-path” 中。若未添加,较新版本 GDB 会给出提示。也可手动将add-auto-load-safe-path /path/to/cpython
添加到配置文件(~/.gdbinit
或~/.config/gdb/gdbinit
)中,多个路径用:
分隔。
(二)Linux 发行版 Python 的设置
多数 Linux 系统通过特定包提供系统 Python 的调试信息,如 Fedora 系统使用sudo dnf install gdb
和sudo dnf debuginfo-install python3
;Ubuntu 系统使用sudo apt install gdb python3-dbg
。部分最新 Linux 系统虽可借助 debuginfod 自动下载调试符号,但python-gdb.py
扩展仍需单独安装调试信息包。
(三)使用调试构建和开发模式
为便于调试,建议使用 Python 的调试构建版(从源代码构建时用configure --with-pydebug
;在 Linux 发行版上,安装并运行python-debug
或python-dbg
之类的包)和运行时开发模式(-X dev
)。这两种方式会启用额外断言并禁用部分优化,虽可能隐藏一些错误,但多数情况下能简化调试过程。
四、使用python-gdb.py
扩展
(一)美化打印
python-gdb.py
扩展启用后,GDB 回溯信息中会以更易读的方式展示 Python 对象。如PyDict_GetItemString
的字典参数会显示为repr()
形式,而非不透明指针。对于基本类型,美化打印尽量匹配repr()
结果,但并非实际调用repr()
。若需查看对象底层细节,可将原值投射为适当类型指针。例如,查看globals
变量:
(gdb) p globals $1 = {'__builtins__': <module at remote 0x7ffff7fb1868>, '__name__': '__main__', 'ctypes': <module at remote 0x7ffff7f14360>, '__doc__': None, '__package__': None} (gdb) p *(PyDictObject*)globals $2 = {ob_refcnt = 3, ob_type = 0x3dbdf85820, ma_fill = 5, ma_used = 5, ma_mask = 7, ma_table = 0x63d0f8, ma_lookup = 0x3dbdc7ea70<lookdict_string>, ma_smalltable = {{me_hash = 7065186196740147912, me_key = '__builtins__', me_value = <module at remote 0x7ffff7fb1868>}, {me_hash = -368181376027291943, me_key = '__name__', me_value = '__main__'}, {me_hash = 0, me_key = 0x0, me_value = 0x0}, {me_hash = 0, me_key = 0x0, me_value = 0x0}, {me_hash = -9177857982131165996, me_key = 'ctypes', me_value = <module at remote 0x7ffff7f14360>}, {me_hash = -8518757509529533123, me_key = '__doc__', me_value = None}, {me_hash = 0, me_key = 0x0, me_value = 0x0}, {me_hash = 6614918939584953775, me_key = '__package__', me_value = None}}}
类型 | 美化打印示例 | 实际投射查看示例 | 说明 |
---|---|---|---|
int (PyLongObject * ) |
(gdb) p some_python_integer $4 = 42 |
(gdb) p *(PyLongObject*)some_python_integer $5 = {ob_base = {ob_base = {ob_refcnt = 8, ob_type = 0x3dad39f5e0}, ob_size = 1}, ob_digit = {42}} |
美化打印与常规整数显示类似,需投射查看内部结构 |
str (PyUnicodeObject * ) |
(gdb) p ptr_to_python_str $6 = '__builtins__' |
(gdb) p *(PyUnicodeObject*)$6 $8 = {ob_base = {ob_refcnt = 33, ob_type = 0x3dad3a95a0}, length = 12, str = 0x7ffff2128500, hash = 7065186196740147912, state = 1, defenc = 0x0} |
美化打印类似char * ,但细节不同,需投射查看内部结构 |
(二)py-list
命令
py-list
命令用于列出选定线程中当前帧的 Python 源代码(若存在),当前行用>
标记 。还可通过py-list START
从指定行号开始列出,py-list START,END
列出指定行范围的代码。这在查看程序执行位置和上下文时非常有用,能快速定位到问题所在代码行。
(三)py-up
和py-down
命令
py-up
和py-down
命令类似 GDB 的常规up
和down
命令,但在 CPython 帧层级上移动。不过,GDB 读取帧信息的能力受 CPython 编译优化级别的影响。内部通过查找执行默认帧求值函数的 C 帧,获取相关PyFrameObject *
的值来实现。它们会显示线程内的帧编号(C 层级) 。在 Python 3.12 及更新版本中,同一 C 栈帧可能对应多个 Python 栈帧,py-up
和py-down
可同时移动多个 Python 帧。
(四)py-bt
命令
py-bt
命令尝试显示当前线程的 Python 层级回溯。通过它可清晰了解 Python 函数的调用栈情况,方便追踪程序执行路径和定位错误源头。帧编号与 GDB 的backtrace
命令显示的内容对应,便于结合其他 GDB 命令进行深入调试。
(五)py-print
命令
py-print
命令用于查找并打印 Python 名称。它按当前线程的局部变量、全局变量、内置变量的顺序查找。若当前 C 帧对应多个 Python 帧,仅考虑第一个。这在查看变量值时非常方便,无需手动在不同作用域中查找变量。
(六)py-locals
命令
py-locals
命令用于查找并打印选定线程中当前 Python 帧内的所有局部变量。若当前 C 帧对应多个 Python 帧,会显示所有帧的局部变量,有助于了解函数内部变量的状态和变化。
五、与 GDB 命令协同使用
python-gdb.py
扩展命令与 GDB 内置命令相辅相成。例如,可结合py-bt
显示的帧编号和frame
命令切换到特定帧;info threads
命令列出进程内的线程列表,thread
命令选择不同线程,thread apply all COMMAND
(或ta a COMMAND
)可在所有线程上运行指定命令,配合py-bt
能在 Python 层级查看每个线程的执行情况,全面掌握程序运行状态。
六、如何使用GDB调试CPython扩展与内部代码的实际案例
下面通过两个实际案例,展示如何使用 GDB 调试 CPython 扩展与内部代码。案例分别聚焦于 CPython 扩展模块中的内存错误和 CPython 解释器内部的多线程死锁问题,通过详细的调试步骤,展示如何利用 GDB 和python - gdb.py
扩展定位和解决问题。
案例一:调试 CPython 扩展中的内存错误
假设我们有一个简单的 CPython 扩展模块,用于计算整数的平方。在运行过程中,程序突然崩溃,怀疑是内存错误导致。
- 准备工作
确保已安装 GDB 7 或更高版本,并按照前文所述方法获取python - gdb.py
扩展和调试信息。假设扩展模块代码如下(square_module.c
):
#include <Python.h> static PyObject* square(PyObject* self, PyObject* args) { int num; if (!PyArg_ParseTuple(args, "i", &num)) { return NULL; } int *result = (int*)malloc(sizeof(int)); if (result == NULL) { PyErr_SetString(PyExc_MemoryError, "Memory allocation failed"); return NULL; } *result = num * num; PyObject* py_result = PyLong_FromLong(*result); free(result); // 这里故意注释掉,模拟内存泄漏 return py_result; } static PyMethodDef SquareMethods[] = { {"square", square, METH_VARARGS, "Calculate the square of an integer."}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef squaremodule = { PyModuleDef_HEAD_INIT, "square_module", "Module for calculating squares", -1, SquareMethods }; PyMODINIT_FUNC PyInit_square_module(void) { return PyModule_Create(&squaremodule); }
使用以下命令编译扩展模块(假设 Python 安装在/usr/local/python3.12
):
gcc -shared -fpic -I/usr/local/python3.12/include/python3.12 -o square_module.so square_module.c
- 启动 GDB 调试
在终端中启动 GDB,并加载 Python 程序和扩展模块:
gdb -ex "python from square_module import square" -ex "run" /usr/local/python3.12/bin/python
这里使用了 GDB 的-ex
选项,在启动时自动执行 Python 导入扩展模块和运行相关代码的命令。
- 使用扩展命令调试
程序崩溃后,使用py - bt
命令查看 Python 层级的回溯信息:
(gdb) py - bt #0 0x000000000041a6b1 in PyObject_Malloc (nbytes=Cannot access memory at address 0x7fffff7fefe8) at Objects/obmalloc.c:748 #1 0x000000000041b7c0 in _PyObject_DebugMallocApi (id=111 'o', nbytes=24) at Objects/obmalloc.c:1445 #2 0x000000000041b717 in _PyObject_DebugMalloc (nbytes=24) at Objects/obmalloc.c:1412 #3 0x000000000044060a in _PyUnicode_New (length=11) at Objects/unicodeobject.c:346 #4 0x00000000004466aa in PyUnicodeUCS2_DecodeUTF8Stateful (s=0x5c2b8d "__lltrace__", size=11, errors=0x0, consumed=0x0) at Objects/unicodeobject.c:2531 #5 0x0000000000446647 in PyUnicodeUCS2_DecodeUTF8 (s=0x5c2b8d "__lltrace__", size=11, errors=0x0) at Objects/unicodeobject.c:2495 #6 0x0000000000440d1b in PyUnicodeUCS2_FromStringAndSize (u=0x5c2b8d "__lltrace__", size=11) at Objects/unicodeobject.c:551 #7 0x0000000000440d94 in PyUnicodeUCS2_FromString (u=0x5c2b8d "__lltrace__") at Objects/unicodeobject.c:569 #8 0x0000000000584abd in PyDict_GetItemString (v= {'Yuck': <type at remote 0xad4730>, '__builtins__': <module at remote 0x7ffff7fd5ee8>, '__file__': 'Lib/test/crashers/nasty_eq_vs_dict.py', '__package__': None, 'y': <Yuck(i=0) at remote 0xaacd80>, 'dict': {0: 0, 1: 1, 2: 2, 3: 3}, '__cached__': None, '__name__': '__main__', 'z': <Yuck(i=0) at remote 0xaace60>, '__doc__': None}, key=0x5c2b8d "__lltrace__") at Objects/dictobject.c:2171
从回溯信息中可以看到程序在内存分配相关函数处出错。再使用py - list
命令查看当前帧的 Python 代码(假设当前帧是调用square
函数的地方):
(gdb) py - list 5 result = square(5) 6 print(result)
确定问题出在square
函数中。接着查看square
函数对应的 C 代码,使用list
命令(GDB 的常规命令):
(gdb) list square 10 static PyObject* square(PyObject* self, PyObject* args) { 11 int num; 12 if (!PyArg_ParseTuple(args, "i", &num)) { 13 return NULL; 14 } 15 int *result = (int*)malloc(sizeof(int)); 16 if (result == NULL) { 17 PyErr_SetString(PyExc_MemoryError, "Memory allocation failed"); 18 return NULL; 19 } 20 *result = num * num;
发现free(result)
这行代码被注释掉,导致内存泄漏。修复代码后重新编译并调试,问题解决。
案例二:调试 CPython 解释器内部的多线程死锁问题
假设有一个多线程的 Python 程序,使用了 CPython 解释器,在运行过程中出现死锁现象。
- 准备工作
程序代码如下(multithreaded_app.py
):
import threading import time lock1 = threading.Lock() lock2 = threading.Lock() def thread1(): lock1.acquire() print("Thread 1 acquired lock1") time.sleep(1) lock2.acquire() print("Thread 1 acquired lock2") lock2.release() lock1.release() def thread2(): lock2.acquire() print("Thread 2 acquired lock2") time.sleep(1) lock1.acquire() print("Thread 2 acquired lock1") lock1.release() lock2.release() t1 = threading.Thread(target=thread1) t2 = threading.Thread(target=thread2) t1.start() t2.start() t1.join() t2.join()
确保已配置好 GDB 和python - gdb.py
扩展。
- 启动 GDB 调试
在终端中启动 GDB 并加载 Python 程序:
gdb -ex "run" /usr/local/python3.12/bin/python multithreaded_app.py
- 使用扩展命令调试
程序死锁后,使用info threads
命令查看线程状态:
(gdb) info threads 2 Thread 0x7fffefa18710 (LWP 10260) sem_wait () at ../nptl/sysdeps/unix/sysv/linux/x86_64/sem_wait.S:86 1 Thread 0x7ffff7fe2700 (LWP 10145) 0x00000038e46d73e3 in select () at ../sysdeps/unix/syscall-template.S:82
可以看到有两个线程。接着使用thread apply all py - bt
命令查看每个线程的 Python 层级回溯信息:
(gdb) thread apply all py - bt Thread 2 (Thread 0x7fffefa18710 (LWP 10260)): #0 0x000000000041a6b1 in PyObject_Malloc (nbytes=Cannot access memory at address 0x7fffff7fefe8) at Objects/obmalloc.c:748 #1 0x000000000041b7c0 in _PyObject_DebugMallocApi (id=111 'o', nbytes=24) at Objects/obmalloc.c:1445 #2 0x000000000041b717 in _PyObject_DebugMalloc (nbytes=24) at Objects/obmalloc.c:1412 #3 0x000000000044060a in _PyUnicode_New (length=11) at Objects/unicodeobject.c:346 #4 0x00000000004466aa in PyUnicodeUCS2_DecodeUTF8Stateful (s=0x5c2b8d "__lltrace__", size=11, errors=0x0, consumed=0x0) at Objects/unicodeobject.c:2531 #5 0x0000000000446647 in PyUnicodeUCS2_DecodeUTF8 (s=0x5c2b8d "__lltrace__", size=11, errors=0x0) at Objects/unicodeobject.c:2495 #6 0x0000000000440d1b in PyUnicodeUCS2_FromStringAndSize (u=0x5c2b8d "__lltrace__", size=11) at Objects/unicodeobject.c:551 #7 0x0000000000440d94 in PyUnicodeUCS2_FromString (u=0x5c2b8d "__lltrace__") at Objects/unicodeobject.c:569 #8 0x0000000000584abd in PyDict_GetItemString (v= {'Yuck': <type at remote 0xad4730>, '__builtins__': <module at remote 0x7ffff7fd5ee8>, '__file__': 'Lib/test/crashers/nasty_eq_vs_dict.py', '__package__': None, 'y': <Yuck(i=0) at remote 0xaacd80>, 'dict': {0: 0, 1: 1, 2: 2, 3: 3}, '__cached__': None, '__name__': '__main__', 'z': <Yuck(i=0) at remote 0xaace60>, '__doc__': None}, key=0x5c2b8d "__lltrace__") at Objects/dictobject.c:2171 #9 0x00000000007f7f7f7f7f7f in thread1 () at multithreaded_app.py:7 #10 0x00000000007f7f7f7f7f7f in <module> () at multithreaded_app.py:16 Thread 1 (Thread 0x7ffff7fe2700 (LWP 10145)): #0 0x000000000041a6b1 in PyObject_Malloc (nbytes=Cannot access memory at address 0x7fffff7fefe8) at Objects/obmalloc.c:748 #1 0x000000000041b7c0 in _PyObject_DebugMallocApi (id=111 'o', nbytes=24) at Objects/obmalloc.c:1445 #2 0x000000000041b717 in _PyObject_DebugMalloc (nbytes=24) at Objects/obmalloc.c:1412 #3 0x000000000044060a in _PyUnicode_New (length=11) at Objects/unicodeobject.c:346 #4 0x00000000004466aa in PyUnicodeUCS2_DecodeUTF8Stateful (s=0x5c2b8d "__lltrace__", size=11, errors=0x0, consumed=0x0) at Objects/unicodeobject.c:2531 #5 0x0000000000446647 in PyUnicodeUCS2_DecodeUTF8 (s=0x5c2b8d "__lltrace__", size=11, errors=0x0) at Objects/unicodeobject.c:2495 #6 0x0000000000440d1b in PyUnicodeUCS2_FromStringAndSize (u=0x5c2b8d "__lltrace__", size=11) at Objects/unicodeobject.c:551 #7 0x0000000000440d94 in PyUnicodeUCS2_FromString (u=0x5c2b8d "__lltrace__") at Objects/unicodeobject.c:569 #8 0x0000000000584abd in PyDict_GetItemString (v= {'Yuck': <type at remote 0xad4730>, '__builtins__': <module at remote 0x7ffff7fd5ee8>, '__file__': 'Lib/test/crashers/nasty_eq_vs_dict.py', '__package__': None, 'y': <Yuck(i=0) at remote 0xaacd80>, 'dict': {0: 0, 1: 1, 2: 2, 3: 3}, '__cached__': None, '__name__': '__main__', 'z': <Yuck(i=0) at remote 0xaace60>, '__doc__': None}, key=0x5c2b8d "__lltrace__") at Objects/dictobject.c:2171 #9 0x00000000007f7f7f7f7f7f in thread2 () at multithreaded_app.py:12 #10 0x00000000007f7f7f7f7f7f in <module> () at multithreaded_app.py:17
从回溯信息中可以看到thread1
获取了lock1
,正在等待lock2
;thread2
获取了lock2
,正在等待lock1
,这就是导致死锁的原因。可以通过调整加锁顺序来解决死锁问题,比如在两个线程中都先获取lock1
,再获取lock2
。修改代码后重新运行和调试,确认死锁问题已解决。
总结
本文详细介绍了使用 GDB 调试 CPython 扩展和内部代码的方法,包括前提条件、不同系统的设置方式、调试构建和开发模式的运用,以及python-gdb.py
扩展的各项功能和与 GDB 命令的协同使用。通过掌握这些内容,开发者能在面对 CPython 底层问题时,更高效地进行调试和排查,提升开发效率和代码质量。在实际应用中,需根据具体问题灵活运用这些调试技巧,不断积累经验,以应对复杂的开发场景。
TAG:Python;GDB;调试;CPython 扩展;CPython 内部代码;python-gdb.py
扩展
学习资源和 URL
- Python 官方文档:本文参考的官方文档,提供了基础的知识框架和使用说明。
- GDB 官方文档:GDB 官方文档,深入了解 GDB 的各种功能和命令,有助于更好地结合
python-gdb.py
扩展进行调试。 - CPython 源代码仓库:CPython 源代码仓库,可查看 CPython 的源代码,深入理解其内部机制,为调试工作提供更深入的知识支持。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!