GDB的全称是GNU project debugger,是类Unix系统上一个十分强大的调试器。这里通过一个简单的例子(插入算法)来介绍如何使用gdb进行调试,特别是如何通过中断来高效地找出死循环;我们还可以看到,在修正了程序错误并重新编译后,我们仍然可以通过原先的GDB session进行调试(而不需要重开一个GDB),这避免了一些重复的设置工作;同时,在某些受限环境中(比如某些实时或嵌入式系统),往往只有一个Linux字符界面可供调试。这种情况下,可以使用job在代码编辑器、编译器(编译环境)、调试器之间做到无缝切换。这也是高效调试的一个方法。
先来看看这段插入排序算法(a.cpp),里面有一些错误。
// a.cpp #include <stdio.h> #include <stdlib.h> int x[10]; int y[10]; int num_inputs; int num_y = 0; void get_args(int ac, char **av) { num_inputs = ac - 1; for (int i = 0; i < num_inputs; i++) x[i] = atoi(av[i+1]); } void scoot_over(int jj) { for (int k = num_y-1; k > jj; k++) y[k] = y[k-1]; } void insert(int new_y) { if (num_y = 0) { y[0] = new_y; return; } for (int j = 0; j < num_y; j++) { if (new_y < y[j]) { scoot_over(j); y[j] = new_y; return; } } } void process_data() { for (num_y = 0; num_y < num_inputs; num_y++) insert(x[num_y]); } void print_results() { for (int i = 0; i < num_inputs; i++) printf("%d\n",y[i]); } int main(int argc, char ** argv) { get_args(argc,argv); process_data(); print_results(); return 0; }
代码就不分析了,稍微花点时间应该就能明白。你能发现几个错误?
使用gcc编译:
gcc -g -Wall -o insert_sort a.cpp"-g"告诉gcc在二进制文件中加入调试信息,如符号表信息,这样gdb在调试时就可以把地址和函数、变量名对应起来。在调试的时候你就可以根据变量名查看它的值、在源代码的某一行加一个断点等,这是调试的先决条件。“-Wall”是把所有的警告开关打开,这样编译时如果遇到warning就会打印出来。一般情况下建议打开所有的警告开关。
运行编译后的程序(./insert_sort),才发现程序根本停不下来。上调试器!(有些bug可能一眼就能看出来,这里使用GDB只是为了介绍相关的基本功能)
TUI模式
现在版本的GDB都支持所谓的终端用户接口模式(Terminal User Interface),就是在显示GDB命令行的同时可以显示源代码。好处是你可以随时看到当前执行到哪条语句。之所以叫TUI,应该是从GUI抄过来的。注意,可以通过ctrl + x + a来打开或关闭TUI模式。
gdb -tui ./insert_sort死循环
进入GDB后运行run命令,传入命令行参数,也就是要排序的数组。当然,程序也是停不下来:
为了让程序停下来,我们可以发送一个中断信号(ctrl + c),GDB捕捉到该信号后会挂起被调试进程。注意,什么时候发送这个中断有点技巧,完全取决于我们的经验和程序的特点。像这个简单的程序,正常情况下几乎立刻就会执行完毕。如果感觉到延迟就说明已经发生了死循环(或其他什么),这时候发出中断肯定落在死循环的循环体中。这样我们才能通过检查上下文来找到有用信息。大型程序如果正常情况下就需要跑个几秒钟甚至几分钟,那么你至少需要等到它超时后再去中断。
此时,程序暂停在第44行(第44行还未执行),TUI模式下第44行会被高亮显示。我们知道,这一行是某个死循环体中的一部分。
因为暂停的代码有一定的随机性,可以多运行几次,看看每次停留的语句有什么不同。后面执行run命令的时候可以不用再输入命令行参数(“12 5”),GDB会记住。还有,再执行run的时候GDB会问是否重头开始执行程序,当然我们要从头开始执行。
基本确定位置后(如上面的44行),因为这个程序很小,可以单步(step)一条条语句查看。不难发现问题出在第24行,具体的步骤就省略了。
无缝切换
在编码、调试的时候,除非你有集成开发环境,一般你会需要打开三个窗口:代码编辑器(比如很多人用的VIM)、编译器(新开的窗口运行gcc或者make命令、执行程序等)、调试器。集成开发环境当然好,但某些倒闭的场合下你无法使用任何GUI工具,比如一些仅提供字符界面的嵌入式设备——你只有一个Linux命令行可以使用。显然,如果在VIM中修改好代码后需要先关闭VIM才能敲入gcc的编译命令,或者调试过程中发现问题需要先关闭调试器才能重新打开VIM修改代码、编译、再重新打开调试器,那么不言而喻,这个过程太痛苦了!