程序员在调试时每每分红两派,一派用debugger另外一派用print。至于本人嘛,是一个“机会主义者”,有时用print,有时却改投debugger阵营。python
实话说,print要比用debugger设下断点更为简单粗暴,有时甚至会更有用。不过debugger对比于print有三个优势:linux
无需从新编译ios
能够在调试时改变变量程序员
debugger能够实现print作不到的复杂操做segmentfault
在本文,我会介绍一些在gdb中自动化操做的技术,保证可让你大开眼界,见识下gdb真正的力量。less
一般咱们只有在程序出问题才会启动gdb,开始调试工做,调试完毕后退出。不过,让gdb一直开着何尝不是更好的作法。每一个gdb老司机都懂得,gdb在r
的时候会加载当前程序的最新版本。也便是说,就算不退出gdb,每次运行的也会是当前最新的版本。不退出当前调试会话有两个好处:编辑器
调试上下文能够获得保留。不用每次运行都从新设一轮断点。函数
一旦core dump了,能够显示core dump的位置,无需带着core从新启动一次。性能
在开发C/C++项目,我通常是这样的工做流程:一个窗口开着编辑器,编译也在这个窗口执行;另外一个窗口开着gdb,这个窗口同时也用来运行程序。一旦要调试了(或者,又segment fault了),随手就能够开始干活。debug
固然了,劳做一天以后,总须要关电脑回家。这时候只能退出gdb。不想明天一早再把断点设上一遍?gdb提供了保留断点的功能。输入save br .gdb_bp
,gdb会把本次会话的断点存在.gdb_bp
中。明天早上一回来,启动gdb的时候,加上-x .gdb_bp
,让gdb把.gdb_bp
当作命令文件逐条从新执行,一切又回到昨晚。
下面是一个带bug的二分查找实现:
#include <iostream> using std::cout; using std::endl; int binary_search(int *ary, unsigned int ceiling, int target) { unsigned int floor = 0; while (ceiling > floor) { unsigned int pivot = (ceiling + floor) / 2; if (ary[pivot] < target) floor = pivot + 1; else if (ary[pivot] > target) ceiling = pivot - 1; else return pivot; } return -1; } int main() { int a[] = {1, 2, 4, 5, 6}; cout << binary_search(a, 5, 7) << endl; // -1 cout << binary_search(a, 5, 6) << endl; // 4 cout << binary_search(a, 5, 5) << endl; // 指望3,实际运行结果是-1 return 0; }
你打算调试下binary_search(a, 5, 5)
这个组合。若若是用print大法,就在binary_search
中插入几个print,运行后扫一眼,看看target=5
的时候运行流是怎样的。
debugger大法看似会复杂一点,若是在binary_search
中插断点,那么前两次调用只能连按c
跳过。其实没那么复杂,gdb容许用户设置条件断点。你能够这么设置:
b binary_search if target == 5
如今就只有第三次调用会触发断点。
问题看上去跟floor
和ceiling
值的变化有关。要想观察它们的值,能够p floor
和p ceiling
。不过有个简单的方法,你能够对它们设置watch断点:wa floor if target == 5
。当floor
的值变化时,就会触发断点。
对于咱们的示例程序来讲,靠脑补也能算出这两个值的变化,专门设置断点彷佛小题大作。不过在调试真正的程序时,watch断点很是实用,尤为当你对相关代码不熟悉时。使用watch断点能够更好地帮助你理解程序流程,有时甚至会有意外惊喜。另外结合debugger运行时修改值的能力,你能够在值变化的下一刻设置目标值,观察走不一样路径会不会出现相似的问题。若是有须要的话,还能够给某个内存地址设断点:wa *0x7fffffffda40
。
除了watch以外,gdb还有一类catch断点,能够用来捕获异常/系统调用/信号。由于用途不大(我从没实际用过),就不介绍了,感兴趣的话在gdb里面help catch
看看。
gdb提供名为commands
的机制,能够给某个断点挂上待触发的命令。举个例子,b binary_search if target == 5
以后,输入:
comm i locals i args end
这样当上面的断点被触发时,i locals
和i args
命令会被触发,列出当前上下文内的变量。这个功能挺废的,由于你彻底能够在断点被触发后才敲入这几个命令。要不是有define
,commands
就真成摆设了。接下来咱们要介绍commands
的好基友、最强大的gdb命令之一,define
命令。
一如unix世界里面的许多程序同样,gdb内部实现了一门DSL(领域特定语言)。用户能够经过这门DSL来编写自定义的宏,甚至编写调试用的自动化脚本。咱们能够用define
命令编写自定义的宏。
继续上面的例子,你能够自定义一个命令代替b xxx comm ...
:
(gdb) define br_info Type commands for definition of "br_info". End with a line saying just "end". >b $arg0 >comm >i locals >i args >end (gdb) br_info binary_search if target == 5
当if target == 5
条件知足时,br_info binary_search
会被执行。br_info
展开成为一系列命令,并用binary_search
替换掉$arg0
。一行顶过去五行!
除了在会话内建立自定义宏外,咱们还能够用gdb的DSL编写宏文件,并导入到gdb中。
举个有实际意义的例子。因为源代码的改变,咱们须要更新断点的位置。一般的作法是删掉原来的断点,并新设一个。让咱们现学现用,用宏把这两步合成一步:
# gdb_macro define mv if $argc == 2 delete $arg0 # 注意新建立的断点编号和被删除断点的编号不一样 break $arg1 else print "输入参数数目不对,help mv以得到用法" end end # (gdb) help mv 会输出如下帮助文档 document mv Move breakpoint. Usage: mv old_breakpoint_num new_breakpoint Example: (gdb) mv 1 binary_search -- move breakpoint 1 to `b binary_search` end # vi:set ft=gdb ts=4 sw=4 et
使用方法:
(gdb) b binary_search Breakpoint 1 at 0x40083b: file binary_search.cpp, line 7. (gdb) source ~/gdb_macro (gdb) help mv Move breakpoint. Usage: mv old_breakpoint_num new_breakpoint Example: (gdb) mv 1 binary_search -- move breakpoint 1 to `b binary_search` (gdb) mv 1 binary_search.cpp:18 Breakpoint 2 at 0x4008ab: file binary_search.cpp, line 18.
还能够进一步,把source ~/gdb_macro
也省掉。你能够建立gdb配置文件~/.gdbinit
,让gdb启动时自动执行里面的指令。若是把本身经常使用的宏写在该文件中,就能直接在gdb里面使用了,用起来如内置命令通常顺滑。
在第一节会话/历史/命令文件
结尾,我提到用-x
指定命令文件来回放断点。那时的命令文件也算是一种用gdb的DSL编写的调试脚本。因为调试是件交互性的活,须要事先写好调试脚本的场景很少。即便如此,除了让gdb自动设置断点,依然有很多场景下能够用上调试脚本。其中之一,就是让gdb自动采集特定函数调用的上下文数据。我把这种方法称为“拖网法”,由于它就像拖网捕鱼同样,把逮到的东西都一股脑带上来。
设想以下的情景:某个项目出现内存泄露的迹象。事先分配好的内存池用着用着就满了,一再地吞噬系统的内存。内存管理是本身实现的,因此没法用valgrind来分析。鉴于内存管理部分代码最近几个版本都没有改动过,猜想是业务逻辑代码里面有谁借了内存又不还。如今你须要把它揪出来。一个办法是给内存的分配和释放加上日志,再编译,而后从新运行程序,谋求复现内存泄露的场景。不过更快的办法是,敲上这一段代码:
(假设分配内存的接口是my_malloc(char *p, size_t size)
,释放内存的接口是free(char *p)
)
# /tmp/malloc_free # 设置输出不要分屏 set pagination off b my_malloc comm silent printf "malloc 0x%x %lu\n", p, size bt c end b my_free comm silent printf "free 0x%x\n", p bt c end c
直接让gdb执行它:
sudo gdb -q -p $(pidof $your_project) -x /tmp/malloc_free > log
运行一段时间后kill掉gdb,打开log看看里面的内容:
$ less log Attaching to process 8738 Reading symbols from ...done. Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...Reading symbols from /usr/ lib/debug//lib/x86_64-linux-gnu/libc-2.19.so...done. done. Loaded symbols for /lib/x86_64-linux-gnu/libc.so.6 ...... malloc 0x0 82 #0 my_malloc (p=0x0, size=82) at memory.cpp:8 #1 0x0000000000400657 in write_buffer (p=0x0, size=82) at memory.cpp:17 #2 0x00000000004006b6 in main () at memory.cpp:25 malloc 0x852c39c0 13 #0 my_malloc (p=0x7ffd852c39c0 "\001", size=13) at memory.cpp:8 #1 0x0000000000400657 in write_buffer (p=0x7ffd852c39c0 "\001", size=13) at memory.cpp:17 #2 0x00000000004006b6 in main () at memory.cpp:25 free 0x400780 #0 my_free (p=0x400780 <__libc_csu_init> "AWA\211\377AVI\211\366AUI\211\325ATL\215%x\006 ") at memory.cpp:14 #1 0x0000000000400632 in read_buffer (p=0x400780 <__libc_csu_init> "AWA\211\377AVI\211\366AUI\211\325ATL\215%x\006 ") at memory.cpp:16 #2 0x00000000004006fe in main () at memory.cpp:28 free 0x0 ......
如今咱们能够写个脚本对下账。每次解析到malloc
时,在对应指针的名下记下一项借出。解析到free
时,表示销掉对应最近一次借出的还款。把所有输出解析完后,困扰已久的坏帐状况就将水落石出,欠钱不还的老赖也将无可遁形。这种“拖网法”真的是简单粗暴又有效。
咱们还能够用这种“拖网法”获取指定函数的调用者比例、调用参数的分布范围等等。注意,不要在生产环境撒网,毕竟这么作对性能有显著影响。并且要作统计的话,也有更好的方法能够选。
除了用gdb自身的DSL,咱们还可使用python来给gdb写脚本。凭借python的力量,咱们甚至能够在gdb里跟外部程序交互,展现更多的可能性。“大家对力量一无所知”。
欲知后事如何,请听下回分解。