Skip to content

Latest commit

 

History

History
418 lines (319 loc) · 23.3 KB

使用gdb调试CPython进程.md

File metadata and controls

418 lines (319 loc) · 23.3 KB

原文:Debugging of CPython processes with gdb


当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大多数可用的调试器 (明显的例外是winpdbpydevd)并不支持在被调试的应用线程之间进行切换。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进程必然是与安装用于具体的解释器版本的调试符号一样简单,虽然有一些已知的陷阱,尤其是当使用虚拟机时。