
CPython 结合 GDB 与 python-gdb.py 扩展调试 C 扩展与解释器内部【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本文基于 CPython 仓库的官方指南 Doc/howto/gdb_helpers.rst系统讲解如何使用python-gdb.py扩展把 GDB 变成懂 Python的调试器既能按 C 帧做底层排障也能查看 Python 调用栈、Python 源码位置和局部变量。读完后你将掌握从源码构建、发行版包两种场景下的完整配置步骤并会用py-list、py-up/py-down、py-bt、py-print、py-locals等命令定位崩溃、死锁与 C API 扩展中的底层问题。1. 背景为什么需要 GDB python-gdb.py调试崩溃crash、死锁deadlock等低层问题时需要一个 GDB 这样的底层调试器来诊断并定位问题。但 GDB及其各类前端默认并不理解 CPython 解释器的高层信息在它的backtrace里一个 Python 字典对象只是一条不起眼的PyObject *指针。python-gdb.py扩展把 CPython 解释器的信息注入 GDB提供两大核心能力栈内省查看当前正在执行的 Python 函数调用栈对象内省给定一个PyObject *指针直接展示该对象的类型和值按repr()风格渲染而不是裸指针。这份文档面向正在开发 CPython 扩展、或以 C 语言维护 CPython 内部代码的开发者。文档自身声明了前提读者应熟悉 GDB 基础以及 CPython C API该指南整合自 CPython 开发者指南与 Python wiki 上的 GDB 调试资料。2. 前提条件使用之前需要准备三样东西GDB 7 或更高版本。更早版本的 GDB 需要使用 Python 3.11 及以前源码树中的Misc/gdbinit当前仓库已不再提供该文件。Python 本身及被调试扩展的 GDB 兼容调试信息即带调试符号的构建。python-gdb.py扩展本身。扩展随 Python 一起构建但发行版可能将其单独打包甚至不打包因此下面的安装步骤按常见系统给出示例文档同时提醒即便步骤与你的系统吻合也可能已经过时。2.1 当前仓库中扩展的构建来源源码级补充从源码结构看当前 CPython 源码树里扩展的源文件位于 Tools/gdb/libpython.py约 2200 行 Python 脚本构建系统通过 Makefile 规则把它安装到仓库根目录、改名为python-gdb.py# Makefile.pre.in 第 1034–1039 行 .PHONY: gdbhooks gdbhooks: $(BUILDPYTHON)-gdb.py SRC_GDB_HOOKS$(srcdir)/Tools/gdb/libpython.py $(BUILDPYTHON)-gdb.py: $(SRC_GDB_HOOKS) $(INSTALL_DATA) $(SRC_GDB_HOOKS) $(BUILDPYTHON)-gdb.py见 Makefile.pre.in。由于BUILDPYTHON展开为python目标文件正是文档所说的构建时应出现在仓库根目录的python-gdb.py。也就是说从源码构建后若根目录没有该文件可以显式执行make gdbhooks生成它发行版则很可能把它安装到别处例如与共享库的 DWARF 调试数据相对的路径Makefile 注释原话。仓库还配套了一个专门验证该扩展的测试套件 Lib/test/test_gdb/其中test_backtrace.py、test_pretty_print.py、test_misc.py等分别覆盖回退跟踪、美化打印与杂项命令gdb_sample.py等则是被 GDB 附着调试的示例脚本——如果你想验证自己环境里python-gdb.py是否生效跑一下test_gdb是最直接的办法。3. 环境搭建3.1 场景一从源码构建的 Python从源码构建 CPython 时调试信息应当已经可用构建也会在仓库根目录生成python-gdb.py见 2.1 节的gdbhooks规则。启用扩展的关键一步把包含python-gdb.py的目录加入 GDB 的auto-load-safe-path自动加载安全路径。如果没做较新版本的 GDB 会打印一条警告并附带配置方法。如果 GDB 没给出针对你版本的提示把下面这行写入你的 GDB 配置文件~/.gdbinit或~/.config/gdb/gdbinitadd-auto-load-safe-path /path/to/cpython也可以添加多个路径用:分隔。3.2 场景二发行版提供的 Python多数 Linux 系统把系统 Python 的调试信息放在python-debuginfo、python-dbg之类的包里。例如Fedorasudo dnf install gdb sudo dnf debuginfo-install python3Ubuntusudo apt install gdb python3-dbg几个较新的 Linux 系统上GDB 可以通过debuginfod自动下载调试符号但这不会安装python-gdb.py扩展——你通常仍需要单独安装 debug info 包并从源码树/对应包中拿到扩展文件。4. 推荐Debug 构建 开发模式为了让调试更容易官方建议使用 Python 的debug 构建从源码构建时加configure --with-pydebug发行版上则安装并运行python-debug/python-dbg之类的包若可用。使用运行时开发模式development mode-X dev。两者都会启用额外的断言assertions并禁用部分优化。偶尔这么做会掩盖你要找的 bug但绝大多数情况下会让排障过程更顺利。5. 核心功能一Pretty-printersPython 值美化打印扩展加载后会为PyObject *类型注册自定义打印例程。启用后一条普通的 GDB 回溯就长这样文档给出的截断示例#0 0x000000000041a6b1 in PyObject_Malloc (nbytesCannot access memory at address 0x7fffff7fefe8 ) at Objects/obmalloc.c:748 #1 0x000000000041b7c0 in _PyObject_DebugMallocApi (id111 o, nbytes24) at Objects/obmalloc.c:1445 #2 0x000000000041b717 in _PyObject_DebugMalloc (nbytes24) at Objects/obmalloc.c:1412 #3 0x000000000044060a in _PyUnicode_New (length11) at Objects/unicodeobject.c:346 #4 0x00000000004466aa in PyUnicodeUCS2_DecodeUTF8Stateful (s0x5c2b8d __lltrace__, size11, errors0x0, consumed 0x0) at Objects/unicodeobject.c:2531 ... #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(i0) at remote 0xaacd80, dict: {0: 0, 1: 1, 2: 2, 3: 3}, __name__: __main__, z: Yuck(i0) at remote 0xaace60, __doc__: None}, key 0x5c2b8d __lltrace__) at Objects/dictobject.c:2171注意PyDict_GetItemString的字典参数直接以repr()形式展示而不是一条不透明的PyObject *指针。5.1 想看到底层结构用 C 强制转换美化打印器遮住了结构体细节。需要看底层字段时把值强转为对应 C 类型即可。例如(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}}}一个容易踩坑的细节美化打印器并不会真的去调用repr()——对基本类型它只是尽量让输出贴近repr()的结果这一点很重要意味着打印过程本身不会触发 Python 层的副作用。另一个常见困惑某些类型的美化输出与 GDB 内建打印几乎无法区分。比如 PythonintPyLongObject *(gdb) p some_machine_integer $3 42 (gdb) p some_python_integer $4 42想看内部结构强转为PyLongObject *(gdb) p *(PyLongObject*)some_python_integer $5 {ob_base {ob_base {ob_refcnt 8, ob_type 0x3dad39f5e0}, ob_size 1}, ob_digit {42}}str类型同理输出很像 GDB 对char *的打印(gdb) p ptr_to_python_str $6 __builtins__区别在于str美化打印器默认用单引号与 Python 的repr一致而 GDB 对char *的打印用双引号并带十六进制地址(gdb) p ptr_to_char_star $7 0x6d72c0 hello world同样强转为PyUnicodeObject *即可看到实现细节(gdb) p *(PyUnicodeObject*)$6 $8 {ob_base {ob_refcnt 33, ob_type 0x3dad3a95a0}, length 12, str 0x7ffff2128500, hash 7065186196740147912, state 1, defenc 0x0}这些打印例程的具体实现就在 Tools/gdb/libpython.py 中如PyObjectPrinter及相关类型特化类。另外值得一提当前源码树的 changelogMisc/NEWS.d/next/Tools-Demos/记录了一个针对该扩展的近期修复——在非 ASCII 字符串的美化打印中当主机字符集如 C locale无法编码它时python-gdb.py曾抛出UnicodeEncodeError现已修复。6. 核心功能二扩展提供的调试命令6.1py-list显示当前 Python 帧的源码列出选中线程中当前帧对应的 Python 源码若存在当前行以标记(gdb) py-list 901 if options.profile: 902 options.profile False 903 profile_me() 904 return 905 906 u UI() 907 if not u.quit: 908 try: 909 gtk.main() 910 except KeyboardInterrupt: 911 # properly quit on a keyboard interrupt...py-list START从指定行号开始列出py-list START,END列出指定行范围。6.2py-up与py-down按 Python 帧移动两者与 GDB 的up/down类似但移动的单位是CPython 帧而非 C 帧。原理上GDB 能否读到相关帧信息取决于 CPython 的编译优化级别。这两个命令内部会查找那些正在执行默认帧求值函数即 CPython 核心字节码解释循环的 C 帧再取出关联的PyFrameObject *。命令会输出线程内 C 层帧号与标准backtrace显示的帧号一致并跳过那些不在执行 Python 代码的 C 帧。示例向上(gdb) py-up #37 Frame 0x9420b04, for file /usr/lib/python2.6/site-packages/ gnome_sudoku/main.py, line 906, in start_game () u UI() (gdb) py-up #40 Frame 0x948e82c, for file /usr/lib/python2.6/site-packages/ gnome_sudoku/gnome_sudoku.py, line 22, in start_game(mainmodule at remote 0xb771b7f4) main.start_game() (gdb) py-up Unable to find an older python frame此时已到达 Python 栈顶。向下py-down则依次回到game_selector.py的run_swallowed_dialog#14、dialog_swallower.py的run_dialog#8 附近直到提示Unable to find a newer python frame即 Python 栈底。注意示例中会出现(unable to read python frame information)的帧——这些是扩展读不到帧信息的 C 帧属正常现象。Python 3.12 及以上的重要变化从 3.12 开始由于帧内联frames in-place设计同一个 C 栈帧可以对应多个 Python 栈帧因此py-up/py-down可能一次跨越多层 Python 帧(gdb) py-up #6 Frame 0x7ffff7fb62b0, for file /tmp/rec.py, line 5, in recursive_function (n0) time.sleep(5) #6 Frame 0x7ffff7fb6240, for file /tmp/rec.py, line 7, in recursive_function (n1) recursive_function(n-1) #6 Frame 0x7ffff7fb61d0, for file /tmp/rec.py, line 7, in recursive_function (n2) recursive_function(n-1) #6 Frame 0x7ffff7fb6160, for file /tmp/rec.py, line 7, in recursive_function (n3) recursive_function(n-1) #6 Frame 0x7ffff7fb60f0, for file /tmp/rec.py, line 7, in recursive_function (n4) recursive_function(n-1) #6 Frame 0x7ffff7fb6080, for file /tmp/rec.py, line 7, in recursive_function (n5) recursive_function(n-1) #6 Frame 0x7ffff7fb6020, for file /tmp/rec.py, line 9, in module () recursive_function(5) (gdb) py-up Unable to find an older python frame可以看到所有帧都对应同一个 C 帧号#6扩展一次性把整个 Python 递归链打印了出来。6.3py-btPython 层回溯尝试显示当前线程的 Python 级调用栈同样以 C 层帧号标识每一层与标准backtrace对应。例如(gdb) py-bt #8 (unable to read python frame information) #11 Frame 0x9aead74, for file .../gnome_sudoku/dialog_swallower.py, line 48, in run_dialog (...) gtk.main() #14 Frame 0x99262ac, for file .../gnome_sudoku/game_selector.py, line 201, in run_swallowed_dialog (...) swallower.run_dialog(self.dialog) #19 (unable to read python frame information) #23 (unable to read python frame information) #34 (unable to read python frame information) #37 Frame 0x9420b04, for file .../gnome_sudoku/main.py, line 906, in start_game () u UI() #40 Frame 0x948e82c, for file .../gnome_sudoku/gnome_sudoku.py, line 22, in start_game (mainmodule at remote 0xb771b7f4) main.start_game()6.4py-print按名称查值查找一个 Python 名称并打印其值查找顺序为当前线程的局部变量 → 全局变量 → 内建builtins(gdb) py-print self local self SwappableArea(runninggtk.Dialog at remote 0x98faaa4, main_page0) at remote 0x98fa6e4 (gdb) py-print __name__ global __name__ gnome_sudoku.dialog_swallower (gdb) py-print len builtin len built-in function len (gdb) py-print scarlet_pimpernel scarlet_pimpernel not found注意若当前 C 帧对应多个 Python 帧3.12 场景py-print只考虑其中的第一个。6.5py-locals列出当前 Python 帧的全部局部变量(gdb) py-locals self SwappableArea(runninggtk.Dialog at remote 0x98faaa4, main_page0) at remote 0x98fa6e4 d gtk.Dialog at remote 0x98faaa4与py-print不同若当前 C 帧对应多个 Python 帧py-locals会把这些帧的局部变量全部展示(gdb) py-locals Locals for recursive_function n 0 Locals for recursive_function n 1 Locals for recursive_function n 2 Locals for recursive_function n 3 Locals for recursive_function n 4 Locals for recursive_function n 5 Locals for module6.6 源码层面命令是如何注册的从源码结构看上述命令在 Tools/gdb/libpython.py 中均以gdb.Command子类实现例如py-list类约 L1934 起带py-list START/py-list START, END文档串、py-upL2046、py-downL2058、py-bt-fullL2110、py-btL2124、py-printL2137、py-localsL2171。py-list的实现注释明确写着requires an actual PyEval_EvalFrameEx frame印证了 6.2 节所述的查找执行帧求值函数的 C 帧这一机制。7. 与 GDB 内建命令配合使用扩展命令是 GDB 内建命令的补充而非替代最典型的组合如下。7.1 用py-bt的帧号跳帧 py-list看源码py-bt输出的帧号可直接喂给frame命令跳到选中线程内的特定 C 帧(gdb) py-bt (output snipped) #68 Frame 0xaa4560, for file Lib/test/regrtest.py, line 1548, in module () main() (gdb) frame 68 #68 0x00000000004cd1e6 in PyEval_EvalFrameEx (fFrame 0xaa4560, for file Lib/test/regrtest.py, line 1548, in module (), throwflag0) at Python/ceval.c:2665 2665 x call_function(sp, oparg); (gdb) py-list 1543 # Run the tests in a context manager that temporary changes the CWD to a 1544 # temporary and writable directory. If its not possible to create or 1545 # change the CWD, the original CWD will be used. The original CWD is 1546 # available from test_support.SAVEDCWD. 1547 with test_support.temp_cwd(TESTCWD, quietTrue): 1548 main()这一步完成了Python 栈 → C 帧 → C 源码行 → Python 源码行的完整闭环。7.2 多线程info threadsthread apply all py-btinfo threads列出进程内所有线程用thread命令选中其一(gdb) info threads 105 Thread 0x7fffefa18710 (LWP 10260) sem_wait () at ../nptl/sysdeps/unix/sysv/linux/x86_64/sem_wait.S:86 104 Thread 0x7fffdf5fe710 (LWP 10259) 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 COMMAND简写t a a COMMAND在每个线程上执行同一条命令配合py-bt就能一眼看清每个线程在 Python 层各在做什么——排查死锁时这是最有用的视角(gdb) t a a py-bt Thread 105 (Thread 0x7fffefa18710 (LWP 10260)): #5 Frame 0x7fffd00019d0, for file .../Lib/threading.py, line 155, in _acquire_restore (self_RLock(...) at remote 0xd7ff40, ...) self.__block.acquire() #8 Frame 0x7fffac001640, for file .../Lib/threading.py, line 269, in wait (self_Condition(...) at remote 0xd7fd10, timeoutNone, waiterthread.lock at remote 0x858a90, saved_state(1, 140737213728528)) self._acquire_restore(saved_state) #12 Frame 0x7fffb8001a10, for file .../Lib/test/lock_tests.py, line 348, in f () cond.wait() #16 Frame 0x7fffb8001c40, for file .../Lib/test/lock_tests.py, line 37, in task (tid140737213728528) f() Thread 104 (Thread 0x7fffdf5fe710 (LWP 10259)): #5 ... _acquire_restore ... #8 ... wait ... #12 ... cond.wait() #16 ... task ... Thread 1 (Thread 0x7ffff7fe2700 (LWP 10145)): #5 Frame 0xcb5380, for file .../Lib/test/lock_tests.py, line 16, in _wait () time.sleep(0.01) #8 ... _check_notify ...上例中三个线程分别卡在threading.py的条件变量wait/_acquire_restore与time.sleep上——正是死锁/等待类问题的典型现场。8. 小结与检查清单目标命令/操作让扩展自动加载~/.gdbinit中add-auto-load-safe-path /path/to/cpython生成扩展文件源码构建make gdbhooks产物即根目录python-gdb.py源自 Tools/gdb/libpython.py更宽松的调试环境configure --with-pydebug构建运行时加-X dev看对象内部结构p *(PyDictObject*)obj/p *(PyLongObject*)obj/p *(PyUnicodeObject*)obj看当前 Python 源码py-list [START[,END]]按 Python 帧上/下移动py-up/py-down3.12 一次可能跨多个 Python 帧Python 层全栈py-bt全部线程则t a a py-bt按名取值 / 列局部变量py-print name/py-locals跳到特定帧py-bt取 C 帧号 →frame N→py-list需要留意的适用前提GDB 需为 7 及以上版本调试器必须能读到与构建匹配的调试符号帧命令的可读性受 CPython 编译优化级别影响读不到帧信息时会显示(unable to read python frame information)而非报错Python 3.12 起一个 C 帧对应多个 Python 帧会改变py-up/py-down/py-print/py-locals的行为理解这一点对阅读回溯输出至关重要。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考