使用 gdb 调试 CPython 进程
当 Python 程序员需要找到他们应用中的问题根源时, pdb 一直是,而且很可能永远是他们的面包和黄油,因为它是一个内置的,并且易于使用的调试器。但也有些情况时 pdb
无法帮你的,例如,如果你的应用在某些地方卡住了,而你需要在不重启它的情况下,连接到正在运行的进程来找出原因。而这就是 gdb 让人眼前一亮的原因。
为嘛是 gdb?
gdb
是一个通用调试器,它主要是用于 C 和 C++应用程序的调试(虽然它实际上支持阿 Ada,Objective-C, Pascal 等等)。
Python 程序员对使用 gdb
进行调试感兴趣有多种原因:
gdb
允许你在不以 debug 模式启动一个应用,或者以某些方式先修改该应用代码 (例如,把一些像import rpdb; rpdb.set_trace()
之类的东西放到代码里) 的情况下,连接到一个正在运行的进程。gdb
允许你获得一个进程的 核心转储(core dump) ,以便稍后分析。当你不希望停止该进程的持续时间,或者当你正在审视它的状态,以及当你对一个已经失败(e.g. crashed with a segmentation fault) 的程序进行 事后剖析 时,这是有用的。- Python 大多数可用的调试器 (明显的例外是 winpdb 和 pydevd ) 并不支持在被调试的应用线程之间进行切换。
gdb
允许这样,它还允许调试由非 Python 代码(例如,在一些使用的原生库中) 创建的线程
解释型语言的调试
所以,当使用 gdb
时,是什么让 Python 与众不同呢?
不像诸如 C 或 C++这样的编程语言,Python 代码并不会被编译成目标平台的本地二进制文件。取而代之的是,有一个解释器 (例如, CPython ,Python 的参考实现),它执行编译的 字节码 。
这实际上意味着,当你用 gdb
连接到一个 Python 进程时,你会在解释器级别调试解释器实例和内省进程状态,而不是应用程序级别:即你会看到解释器的函数和变量,而不是你的应用程序。
给你举个例子,让我们来看看一个 CPython(最流行 的 Python 解释器)进程的 gdb
回溯:
#0 0x00007fcce9b2faf3 in __epoll_wait_nocancel () at ../sysdeps/unix/syscall-template.S:81
#1 0x0000000000435ef8 in pyepoll_poll (self=0x7fccdf54f240, args=<optimized out>, kwds=<optimized out>) at ../Modules/selectmodule.c:1034
#2 0x000000000049968d in call_function (oparg=<optimized out>, pp_stack=0x7ffc20d7bfb0) at ../Python/ceval.c:4020
#3 PyEval_EvalFrameEx () at ../Python/ceval.c:2666
#4 0x0000000000499ef2 in fast_function () at ../Python/ceval.c:4106
#5 call_function () at ../Python/ceval.c:4041
#6 PyEval_EvalFrameEx () at ../Python/ceval.c:2666
以及一个通过 traceback.extract_stack()
工具获得的:
/usr/local/lib/python2.7/dist-packages/eventlet/greenpool.py:82 in _spawn_n_impl
`func(*args, **kwargs)`
/opt/stack/neutron/neutron/agent/l3/agent.py:461 in _process_router_update
`for rp, update in self._queue.each_update_to_next_router():`
/opt/stack/neutron/neutron/agent/l3/router_processing_queue.py:154 in each_update_to_next_router
`next_update = self._queue.get()`
/usr/local/lib/python2.7/dist-packages/eventlet/queue.py:313 in get
`return waiter.wait()`
/usr/local/lib/python2.7/dist-packages/eventlet/queue.py:141 in wait
`return get_hub().switch()`
/usr/local/lib/python2.7/dist-packages/eventlet/hubs/hub.py:294 in switch
`return self.greenlet.switch()`
照这样看,当你尝试找到你的 Python 代码中的错误时,前者没啥帮助,而你所看到的是解释器本身的当前状态。
然而, PyEval_EvalFrameEx 看起来很有趣:这是 CPython 的一个函数,它执行 Python 应用级别的函数的字节码,因此,可以访问它们的状态 —— 那个我们通常感兴趣的非常态。
gdb 和 Python
"gdb debug python"
的搜索结果可能会造成混淆。问题是,从 gdb
的版本 7 开始,使用 Python 代码 扩展 编译器成为了可能,例如,为了提供 C++ STL 类型的可视化,这用 Python 实现比在内置的 macro 语言实现容易得多。
为了能够调试 CPython 进程以及内省应用级别的状态,解释器开发者决定扩展 gdb
,为此编写一个 script ,当然,是用 Python!
所有有两种不同,但是相关的事:
gdb
版本 7+ 是可以用 Python 模块扩展的gdb
有一个用于 CPython 进程调试的 Python 扩展
使用 gdb 101 调试 Python
首先,你需要安装 gdb
:
# apt-get install gdb
或者
# yum install gdb
根据你正在使用的 Linux 不同发行版本。
下一步是为你的 CPython 安装 调试符号 :
# apt-get install python-dbg
或者
# yum install python-debuginfo
一些 Linux 发行版本,例如 CentOS 或者 RHEL 分别 从所有其他包自带了调试符号 separately ,推荐像这样安装那些:
# debuginfo-install python
为了分析 PyEval_EvalFrameEx
帧 (一个帧实际上是一个函数调用,以及以局部变量和 CPU 寄存器等形式的关联状态),并把它们映射到你的代码中的应用级别的函数,安装的调试符号将被 CPython 脚本 用于 gdb
。
没有调试符号,就会比较难了 - gdb
允许你以任何你想要的方式操纵进程内存,但是你不能很容易地理解什么数据结构驻留在哪个内存区域。
在所有准备步骤已经完成之后,你可以试一试 gdb
。例如,为了连接到一个运行着的 CPython 进程上,这样在:
gdb /usr/bin/python -p $PID
此时,你可以获得当前线程的应用级别的回溯 (注意,一些帧“缺失”了 - 这是意料之中的,因为 gdb
计算所有解释器级别的帧,而它们之中只有一些实在应用级别代码调用 - PyEval_EvalFrameEx
):
(gdb) py-bt
#4 Frame 0x1b7da60, for file /usr/lib/python2.7/sched.py, line 111, in run (self=<scheduler(timefunc=<built-in function time>, delayfunc=<built-in function sleep>, _queue=[<Event at remote 0x7fe1f8c74a10>]) at remote 0x7fe1fa086758>, q=[...], delayfunc=<built-in function sleep>, timefunc=<built-in function time>, pop=<built-in function heappop>, time=<float at remote 0x1a0a400>, priority=1, action=<function at remote 0x7fe1fa083aa0>, argument=(171657,), checked_event=<...>, now=<float at remote 0x1b8ec58>)
delayfunc(time - now)
#7 Frame 0x1b87e90, for file /usr/bin/dstat, line 2416, in main (interval=1, user='ubuntu', hostname='rpodolyaka-devstack', key='unit_hi', linewidth=150, plugin='page', mods=('page', 'page24'), mod='page', pluginfile='dstat_page', scheduler=<scheduler(timefunc=<built-in function time>, delayfunc=<built-in function sleep>, _queue=[<Event at remote 0x7fe1f8c74a10>]) at remote 0x7fe1fa086758>)
scheduler.run()
#11 Frame 0x7fe1fa0bc5c0, for file /usr/bin/dstat, line 2554, in <module> ()
main()
或者找出当前正在执行的应用代码的确切行:
(gdb) py-list
106 pop = heapq.heappop
107 while q:
108 time, priority, action, argument = checked_event = q[0]
109 now = timefunc()
110 if now < time:
>111 delayfunc(time - now)
112 else:
113 event = pop(q)
114 # Verify that the event was not removed or altered
115 # by another thread after we last looked at q[0].
116 if event is checked_event:
或者看看局部变量和它们的值:
(gdb) py-locals
self = <scheduler(timefunc=<built-in function time>, delayfunc=<built-in function sleep>, _queue=[<Event at remote 0x7fe1f8c74a10>]) at remote 0x7fe1fa086758>
q = [<Event at remote 0x7fe1f8c74a10>]
delayfunc = <built-in function sleep>
timefunc = <built-in function time>
pop = <built-in function heappop>
time = <float at remote 0x1a0a400>
priority = 1
action = <function at remote 0x7fe1fa083aa0>
argument = (171657,)
checked_event = <Event at remote 0x7fe1f8c74a10>
now = <float at remote 0x1b8ec58>
CPython 脚本 还为 gdb
提供了更多的 py-
命令。看看调试 指南 以获得详细信息。
陷阱
虽然所描述的技术应该开箱即用,但还有一些已知的陷阱。
python-dbg
Debian 和 Ubuntu 上的 python-dbg
包将不只为 python
安装调试符号 (这在变异的时候被去掉以节省磁盘空间),还提供一个额外的 CPython 库 python-dbg
。
后者本质上市带有许多运行时检查的 CPython 的单独构建(传递 --with-debug
给 ./configure
)。通常情况下,你不希望在生产环境上使用 python-dbg
,因为它可比 python
慢(得多),例如:
$ time python -c "print(sum(range(1, 1000000)))"
499999500000
real 0m0.096s
user 0m0.057s
sys 0m0.030s
$ time python-dbg -c "print(sum(range(1, 1000000)))"
499999500000
[18318 refs]
real 0m0.237s
user 0m0.197s
sys 0m0.016s
好处是,你不需要:它仍然可以使用 gdb
工具调试 python
可执行文件,只要安装相应的调试符号。所以 python-dbg
只是给 CPython/gdb 增加了更多的混乱 —— 你可以放心地忽略它的存在。
构建标志
一些 Linux 发行版本将 -g0
或者 -g1
选项 传递给 gcc
来构建 CPython:前者在完全不需要调试信息的情况下生成了一个二进制文件,而后者不允许 gdb
在运行时获得局部变量的信息。
这些选项都打破了使用 gdb
工具调试 CPython 进程的所述工作流。解决方法是使用 -g
或者 -g2
(当传递 -g
时,2 是默认值) 选项重新构建 CPython。
幸运的是,主要的 Linux 发行版本(Ubuntu Trusty, Debian Jessie, CentOS/RHEL 7) 的所有当前版本都自带了“正确的”已构建 CPython。
优化帧
要让内省正常工作,为每一次调用保存关于 PyEval_EvalFrameEx
参数的信息至关重要。取决于当构建 CPython 或者所使用的具体编译器版本时,在 gcc
中使用的 优化级别 ,在运行时这个信息有可能丢失 (特别是使用 -O3
启用的积极优化)。在这种情况下, gdb
会给你显示这些东东:
(gdb) bt
#0 0x00007fdf3ca31be3 in __select_nocancel () at ../sysdeps/unix/syscall-template.S:84
#1 0x00000000005d1da4 in pysleep (secs=<optimized out>) at ../Modules/timemodule.c:1408
#2 time_sleep () at ../Modules/timemodule.c:231
#3 0x00000000004f5465 in call_function (oparg=<optimized out>, pp_stack=0x7fff62b184c0) at ../Python/ceval.c:4637
#4 PyEval_EvalFrameEx () at ../Python/ceval.c:3185
#5 0x00000000004f5194 in fast_function (nk=<optimized out>, na=<optimized out>, n=<optimized out>, pp_stack=0x7fff62b185c0,
func=<optimized out>) at ../Python/ceval.c:4750
#6 call_function (oparg=<optimized out>, pp_stack=0x7fff62b185c0) at ../Python/ceval.c:4677
#7 PyEval_EvalFrameEx () at ../Python/ceval.c:3185
#8 0x00000000004f5194 in fast_function (nk=<optimized out>, na=<optimized out>, n=<optimized out>, pp_stack=0x7fff62b186c0,
func=<optimized out>) at ../Python/ceval.c:4750
#9 call_function (oparg=<optimized out>, pp_stack=0x7fff62b186c0) at ../Python/ceval.c:4677
#10 PyEval_EvalFrameEx () at ../Python/ceval.c:3185
#11 0x00000000005c5da8 in _PyEval_EvalCodeWithName.lto_priv.1326 () at ../Python/ceval.c:3965
#12 0x00000000005e9d7f in PyEval_EvalCodeEx () at ../Python/ceval.c:3986
#13 PyEval_EvalCode (co=<optimized out>, globals=<optimized out>, locals=<optimized out>) at ../Python/ceval.c:777
#14 0x00000000005fe3d2 in run_mod () at ../Python/pythonrun.c:970
#15 0x000000000060057a in PyRun_FileExFlags () at ../Python/pythonrun.c:923
#16 0x000000000060075c in PyRun_SimpleFileExFlags () at ../Python/pythonrun.c:396
#17 0x000000000062b870 in run_file (p_cf=0x7fff62b18920, filename=0x1733260 L"test2.py", fp=0x1790190) at ../Modules/main.c:318
#18 Py_Main () at ../Modules/main.c:768
#19 0x00000000004cb8ef in main () at ../Programs/python.c:69
#20 0x00007fdf3c970610 in __libc_start_main (main=0x4cb810 <main>, argc=2, argv=0x7fff62b18b38, init=<optimized out>, fini=<optimized out>,
rtld_fini=<optimized out>, stack_end=0x7fff62b18b28) at libc-start.c:291
#21 0x00000000005c9df9 in _start ()
(gdb) py-bt
Traceback (most recent call first):
File "test2.py", line 9, in g
time.sleep(1000)
File "test2.py", line 5, in f
g()
(frame information optimized out)
即,某些应用级别的帧将是可用的,而某些不可用。在这一点上,你基本没啥可做的,除了以一种较低的优化级别来重新构建 CPython,但这样通常不适用于生产(别说你回使用自定义的 CPython 构建,而不是你的 Linux 发行版提供的 CPython 构建)。
虚拟环境和自定义的 CPython 构建
当使用一个虚拟环境时,可能会出现 gdb
扩展不能工作的情况:
(gdb) bt
#0 0x00007ff2df3d0be3 in __select_nocancel () at ../sysdeps/unix/syscall-template.S:84
#1 0x0000000000588c4a in ?? ()
#2 0x00000000004bad9a in PyEval_EvalFrameEx ()
#3 0x00000000004bfd1f in PyEval_EvalFrameEx ()
#4 0x00000000004bfd1f in PyEval_EvalFrameEx ()
#5 0x00000000004b8556 in PyEval_EvalCodeEx ()
#6 0x00000000004e91ef in ?? ()
#7 0x00000000004e3d92 in PyRun_FileExFlags ()
#8 0x00000000004e2646 in PyRun_SimpleFileExFlags ()
#9 0x0000000000491c23 in Py_Main ()
#10 0x00007ff2df30f610 in __libc_start_main (main=0x491670 <main>, argc=2, argv=0x7ffc36f11cf8, init=<optimized out>, fini=<optimized out>,
rtld_fini=<optimized out>, stack_end=0x7ffc36f11ce8) at libc-start.c:291
#11 0x000000000049159b in _start ()
(gdb) py-bt
Undefined command: "py-bt". Try "help".
gdb
仍然可以遵循 CPython 帧框架,但是在 PyEval_EvalCodeEx
调用上的信息就不可用了。
如果你向上滚动一下 gdb
的输出,那么你会看到 gdb
没有为 python
可执行文件找到调试符号:
$ gdb -p 2975
GNU gdb (Debian 7.10-1+b1) 7.10
Copyright (C) 2015 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later < http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
< http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
< http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
Attaching to process 2975
Reading symbols from /home/rpodolyaka/workspace/venvs/default/bin/python2...(no debugging symbols found)...done.
一个虚拟环境有啥不同呢?为嘛 gdb
找不到调试符号?
首先, python
可执行文件的路径不同。注意,当连接到该进程时,我没有指定可执行文件。在这种情况下, gdb
将采用该进程的可执行文件 (例如,在 Linux 上的 /proc/$PID/exe
值)。
分开 调试符号的一种方法是将它们放到一个知名的目录 (默认是, /usr/lib/debug/
,虽然它是通过 gdb
中的 debug-file-directory
选项来配置的)。在我们的例子中, gdb
试图从 /usr/lib/debug/home/rpodolyaka/workspace/venvs/default/bin/python2
加载调试符号,而且显然在那里没找到任何东东。
解决方法是简单的 —— 当运行 gdb
时,在调试下明确指定可执行文件:
$ gdb /usr/bin/python2.7 -p $PID
因此, gdb
会在“正确的”地方查找调试符号 —— /usr/lib/debug/usr/bin/python2.7
。
另外,值得一提的是,有可能一个特定的可执行文件的调试符号由存储在 ELF 可执行头中的唯一的 build-id
所标识。例如,在我的 Debian 机器上的 CPython:
$ objdump -s -j .note.gnu.build-id /usr/bin/python2.7
/usr/bin/python2.7: file format elf64-x86-64
Contents of section .note.gnu.build-id:
400274 04000000 14000000 03000000 474e5500 ............GNU.
400284 8d04a3ae 38521cb7 c7928e4a 7c8b1ed3 ....8R.....J|...
400294 85e763e4
在这个例子中, gdb
会使用该 build-id
值来查找调试符号:
$ gdb /usr/bin/python2.7
GNU gdb (Debian 7.10-1+b1) 7.10
Copyright (C) 2015 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later < http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
< http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
< http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /usr/bin/python2.7...Reading symbols from /usr/lib/debug/.build-id/8d/04a3ae38521cb7c7928e4a7c8b1ed385e763e4.debug...done.
done.
这有一个很好的暗示 —— 怎样调用可执行文件不再是问题: virtualenv
刚创建了指定的解释器可执行文件的一个副本,因此,这两个可执行文件 —— 一个在 /usr/bin/
,和一个在你的虚拟环境中 —— 将使用同样的调试符号:
$ gdb -p 11150
GNU gdb (ebian 7.10-1+b1) 7.10
Copyright () 2015 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later < http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "how copying"
and "how warranty" for details.
This GDB was configured as "86_64-linux-gnu".
Type "how configuration" for configuration details.
For bug reporting instructions, please see:
< http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
< http://www.gnu.org/software/gdb/documentation/>.
For help, type "elp".
Type "propos word" to search for commands related to "ord".
Attaching to process 11150
Reading symbols from /home/rpodolyaka/sandbox/testvenv/bin/python2.7...Reading symbols from
/usr/lib/debug/.build-id/8d/04a3ae38521cb7c7928e4a7c8b1ed385e763e4.debug...done.
$ ls -la /proc/11150/exe
lrwxrwxrwx 1 rpodolyaka rpodolyaka 0 Apr 10 15:18 /proc/11150/exe -> /home/rpodolyaka/sandbox/testvenv/bin/python2.7
第一个问题解决了, bt
输出现在看起来好多了,但是 py-bt
命令仍然未定义:
(gdb) bt
#0 0x00007f3e95083be3 in __select_nocancel () at ../sysdeps/unix/syscall-template.S:84
#1 0x0000000000594a59 in floatsleep (secs=<optimized out>) at ../Modules/timemodule.c:948
#2 time_sleep.lto_priv () at ../Modules/timemodule.c:206
#3 0x00000000004c524a in call_function (oparg=<optimized out>, pp_stack=0x7ffefb5045b0) at ../Python/ceval.c:4350
#4 PyEval_EvalFrameEx () at ../Python/ceval.c:2987
#5 0x00000000004ca95f in fast_function (nk=<optimized out>, na=<optimized out>, n=<optimized out>, pp_stack=0x7ffefb504700,
func=0x7f3e95f78c80) at ../Python/ceval.c:4435
#6 call_function (oparg=<optimized out>, pp_stack=0x7ffefb504700) at ../Python/ceval.c:4370
#7 PyEval_EvalFrameEx () at ../Python/ceval.c:2987
#8 0x00000000004ca95f in fast_function (nk=<optimized out>, na=<optimized out>, n=<optimized out>, pp_stack=0x7ffefb504850,
func=0x7f3e95f78c08) at ../Python/ceval.c:4435
#9 call_function (oparg=<optimized out>, pp_stack=0x7ffefb504850) at ../Python/ceval.c:4370
#10 PyEval_EvalFrameEx () at ../Python/ceval.c:2987
#11 0x00000000004c32e5 in PyEval_EvalCodeEx () at ../Python/ceval.c:3582
#12 0x00000000004c3089 in PyEval_EvalCode (co=<optimized out>, globals=<optimized out>, locals=<optimized out>) at ../Python/ceval.c:669
#13 0x00000000004f263f in run_mod.lto_priv () at ../Python/pythonrun.c:1376
#14 0x00000000004ecf52 in PyRun_FileExFlags () at ../Python/pythonrun.c:1362
#15 0x00000000004eb6d1 in PyRun_SimpleFileExFlags () at ../Python/pythonrun.c:948
#16 0x000000000049e2d8 in Py_Main () at ../Modules/main.c:640
#17 0x00007f3e94fc2610 in __libc_start_main (main=0x49dc00 <main>, argc=2, argv=0x7ffefb504c98, init=<optimized out>, fini=<optimized out>,
rtld_fini=<optimized out>, stack_end=0x7ffefb504c88) at libc-start.c:291
#18 0x000000000049db29 in _start ()
(gdb) py-bt
Undefined command: "py-bt". Try "help".
再次,这是由在一个虚拟环境中的 python
二进制有不同的路径这个事实所引起的。默认, gdb
会尝试在调试的情况下,为特定的对象文件 自动加载 Python 扩展,如果它们存在的话。具体来说, gdb
会查找 objfile-gdb.py
,并尝试在开始的时候 source
它:
(gdb) info auto-load
gdb-scripts: No auto-load scripts.
libthread-db: No auto-loaded libthread-db.
local-gdbinit: Local .gdbinit file was not found.
python-scripts:
Loaded Script
Yes /usr/share/gdb/auto-load/usr/bin/python2.7-gdb.py
如果,处于某些语言,这还没有完成,那么你可以总是手工进行: (gdb) source /usr/share/gdb/auto-load/usr/bin/python2.7-gdb.py
例如,如果你想要测试 CPython 自带的 gdb
扩展的一个新版本。
PyPy, Jython, 等等
所描述的调试技术唯一对 CPython 解释器可行,因为 gdb
扩展是专门写于内省 CPython 内部状态(例如, PyEval_EvalFrameEx
调用)的。
对于 PyPy ,在 Bitbucket 上有一个未决 问题 ,其中,有人提议为用户提供对 gdb
的整合,不过貌似附带的补丁尚未合并,而且写这些的那个人对此失去了兴趣。
对于 Jython ,你可以使用用于调试 JVM
应用的标准工具,例如, VisualVM 。
总结
gdb
是一个强大的工具,它允许你调试崩溃或夯住的 CPython 进程,以及那些确实调用了原生库的 Python 代码的复杂问题。在现代的 Linux 发行版本中,使用 gdb
调试 CPython 进程必然是与安装用于具体的解释器版本的调试符号一样简单,虽然有一些已知的陷阱,尤其是当使用虚拟机时。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: Python 中断两个循环
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论