云南网站建设,企业信息化软件定制开发

专业提供昆明网站建设, 昆明软件开发, 云南网站建设,企业信息化软件定制开发服务免费咨询QQ932256355

博客园 首页 新随笔 联系 订阅 管理
  144 随笔 :: 4 文章 :: 3 评论 :: 914 阅读

Python 开发必备:GDB 调试 CPython 扩展与内部代码全攻略

在 Python 开发中,当涉及到 CPython 扩展或深入调试 CPython 解释器本身的底层问题时,GDB 调试器结合python-gdb.py扩展能发挥强大作用。本教程详细介绍如何利用这一工具组合进行高效调试,涵盖前提条件、环境设置、扩展功能使用及与 GDB 命令协同操作等内容,助力开发者深入理解和解决 Python 底层问题。

一、引言

在 Python 开发的复杂场景中,尤其是涉及 CPython 扩展或 CPython 解释器内部代码时,遇到崩溃、死锁等底层问题难以排查。GDB 作为强大的底层调试器,结合python-gdb.py扩展,能为开发者提供深入剖析程序运行状态的能力,帮助快速定位和解决问题。

二、前提条件

  1. GDB 版本:需要 GDB 7 或更高版本。若使用较低版本,可参考 Python 3.11 或更低版本源代码中的Misc/gdbinit文件进行相关配置。
  2. 调试信息:确保拥有针对 Python 和正在调试的扩展的 GDB 兼容调试信息,这是准确调试的基础。
  3. 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 gdbsudo dnf debuginfo-install python3;Ubuntu 系统使用sudo apt install gdb python3-dbg 。部分最新 Linux 系统虽可借助 debuginfod 自动下载调试符号,但python-gdb.py扩展仍需单独安装调试信息包。

(三)使用调试构建和开发模式

为便于调试,建议使用 Python 的调试构建版(从源代码构建时用configure --with-pydebug;在 Linux 发行版上,安装并运行python-debugpython-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}}}
类型 美化打印示例 实际投射查看示例 说明
intPyLongObject * (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}} 美化打印与常规整数显示类似,需投射查看内部结构
strPyUnicodeObject * (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-uppy-down命令

py-uppy-down命令类似 GDB 的常规updown命令,但在 CPython 帧层级上移动。不过,GDB 读取帧信息的能力受 CPython 编译优化级别的影响。内部通过查找执行默认帧求值函数的 C 帧,获取相关PyFrameObject *的值来实现。它们会显示线程内的帧编号(C 层级) 。在 Python 3.12 及更新版本中,同一 C 栈帧可能对应多个 Python 栈帧,py-uppy-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 扩展模块,用于计算整数的平方。在运行过程中,程序突然崩溃,怀疑是内存错误导致。

  1. 准备工作
    确保已安装 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
  1. 启动 GDB 调试
    在终端中启动 GDB,并加载 Python 程序和扩展模块:
gdb -ex "python from square_module import square" -ex "run" /usr/local/python3.12/bin/python

这里使用了 GDB 的-ex选项,在启动时自动执行 Python 导入扩展模块和运行相关代码的命令。

  1. 使用扩展命令调试
    程序崩溃后,使用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 解释器,在运行过程中出现死锁现象。

  1. 准备工作
    程序代码如下(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扩展。

  1. 启动 GDB 调试
    在终端中启动 GDB 并加载 Python 程序:
gdb -ex "run" /usr/local/python3.12/bin/python multithreaded_app.py
  1. 使用扩展命令调试
    程序死锁后,使用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,正在等待lock2thread2获取了lock2,正在等待lock1,这就是导致死锁的原因。可以通过调整加锁顺序来解决死锁问题,比如在两个线程中都先获取lock1,再获取lock2。修改代码后重新运行和调试,确认死锁问题已解决。

总结

本文详细介绍了使用 GDB 调试 CPython 扩展和内部代码的方法,包括前提条件、不同系统的设置方式、调试构建和开发模式的运用,以及python-gdb.py扩展的各项功能和与 GDB 命令的协同使用。通过掌握这些内容,开发者能在面对 CPython 底层问题时,更高效地进行调试和排查,提升开发效率和代码质量。在实际应用中,需根据具体问题灵活运用这些调试技巧,不断积累经验,以应对复杂的开发场景。

TAG:Python;GDB;调试;CPython 扩展;CPython 内部代码;python-gdb.py扩展

学习资源和 URL

  1. Python 官方文档:本文参考的官方文档,提供了基础的知识框架和使用说明。
  2. GDB 官方文档GDB 官方文档,深入了解 GDB 的各种功能和命令,有助于更好地结合python-gdb.py扩展进行调试。
  3. CPython 源代码仓库CPython 源代码仓库,可查看 CPython 的源代码,深入理解其内部机制,为调试工作提供更深入的知识支持。
posted on   TekinTian  阅读(9)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示