1.调试技术的几个准则
- 惊喜准则:找到错误是一种惊喜,心理上不要畏惧而是要怀着感恩的心去面对。
- 从小处开始准则:刚开始测试的使用从小处着手,暂时不涉及边界数据,虽然这样可能会掩盖一些Bug,但是这样或许能查到最主要的Bug,例如你的程序包含了一个巨大的循环体,最容易发现的Bug在第一个循环或第二次循环执行的时候。
- 自顶向下准则:优先选择step over而不是step into,以节省时间。
- Segmentation Fault准则:出现段错误时,第一个想到的不应该是printf而是Debugger,因为在调试器中你能看到你的哪一行代码导致了错误,更重要的是你可以通过backtrace等工具得到更多有用的信息。
- 折半查找准则:在寻找bug时可以充分利用编辑器等工具来进行折半查找,具体在后边有例子说明。
2.Linux下代码调试工具
主要使用的GDB,以及基于GDB的图形化工具,如DDD或eclipse,选择上看个人习惯了。
命令行式的GDB启动较快,可以在ssh终端下使用,操作简洁,并且在调试GUI程序时不会崩溃,但较之图形化则在单步调试或设置断点时非常不方便。
当然你可以使用Vim等编辑器的插件或者补丁(clewn or vimGDB)来弥补这一缺憾,并且在GDB6.1以上的版本你可以使用GDB -tui这个模式(或者在GDB的命令行模式下按CTRL-x-a)打开一个类似于图形界面的文本界面模式,在这个界面中你可以使用上下键查看源代码 (CTRL-P 和 CTRL-N完成输入过的命令的查看).
或者你还可以使用cGDB这 个工具(很庆幸这个项目在停止了三年后又有人开始维护了),这个工具是将GDB用curses包装了一下,提供了一些很好用的feature(Esc和i 键在代码和命令框间切换;在代码框中支持vim型的操作;在命令框中支持tab键补全命令;在移动到想加入断点的行(行号为高亮白色)直接用空格键,设定 好后行号会变红;)。另外,在调试C-S程序时推荐使用eclipse。
在本文中,重点介绍ddd的操作,因为这个工具即结合了GDB命令行和图形界面的操作。其余请参阅各个工具的手册。
3.GDB命令行最基本操作
- 程序启动:
A.冷启动
gdb programe.g., gdb ./cs
gdb –p pide.g., gdb –p `pidof cs`
gdb program coree.g., gdb ./cs core.xxx
B.热启动
(gdb) attach pide.g., (gdb) attach 2313
C.传入命令行参数
gdb program --args arglist
(gdb) set args arglist
(gdb) run arglist - 使用shell命令:shell command
- Make:make
make-args(=shell make make-args) - 设置断点:b LineNumber
- 运行程序:r args1 args2 ...
- 彻底终止程序:kill
- 单步执行:n(TIPs1:可以按回车重复上一次操作,在单步调试时这个feature很有用)。
- 单步进入:s
- 继续执行:c
- 设置临时断点:tb LineNumber 可以理解为一次性断点,与断点不同,临时断点只在第一次执行时起作用。
- 查看变量:p
- 设置观察点:
- w Expression,当Expression是一个变量名时,这个变量变化时会停止执行;你也可以使用条件来限定,比如w (z>28),当z大于28时,程序停止。注意观察点一般使用在更大范围上的变量,而不是本地变量,因为在局部变量上设置的观察点在局部结束时(比 如该变量所在的函数执行结束时)就被取消了。
- 当然这并不包含main的情况,因为main函数执行结束后程序就结束了。
- 查看栈帧:
- 栈帧指的是在一个函数调用时,该函数调用的运行信息(包含本地变量、参数以及函数被调用的位置)存储的地方。每当一个函数被调用时,一个新的帧就被系统压入一个由系统维护的帧,在这个栈的顶端是现在正在运行的函数信息,当该函数调用结束时被弹出并析构。
- 在GDB中,frame 0为当前帧,frame 1为当前帧的父帧,frame 2为父帧的父帧,等等,用down命令则是反向的。这是一个很有用的信息,因为在早期的一些帧中的信息可能会给你一些提示。
- backtrace(bt/ where)查看整个帧栈
- 注意:在帧中来回并不影响程序的执行。
实例:插入排序算法调试
用伪代码描述这个过程如下:
拟调试代码如下:
//
// insertion sort,
//
// usage: insert_sort num1 num2 num3 ..., where the numi are the numbers to
// be sorted
intx[10], // input array
y[10],// workspace array
num_inputs,// length of input array
num_y = 0;// current number of elements in y
voidget_args(int ac, char **av)
{int i;
num_inputs = ac - 1;
for(i = 0; i < num_inputs; i++)
x[i] = atoi(av[i+1]);
}
voidscoot_over(int jj)
{int k;
for(k = num_y-1; k > jj; k++)
y[k] = y[k-1];
}
voidinsert(int new_y)
{int j;
if(num_y = 0) { // y empty so far, easy case
y[0] = new_y;
return;
}
// need to insert just before the first y
// element that new_y is less than
for(j = 0; j < num_y; j++) {
if(new_y < y[j]) {
// shift y[j], y[j+1],... rightward
// before inserting new_y
scoot_over(j);
y[j] = new_y;
return;
}
}
}
voidprocess_data()
{
for(num_y = 0; num_y < num_inputs; num_y++)
// insert new y in the proper place
// among y[0],...,y[num_y-1]
insert(x[num_y]);
}
voidprint_results()
{int i;
for(i = 0; i < num_inputs; i++)
printf("%d/n",y[i]);
}
intmain(int argc, char ** argv)
{ get_args(argc,argv);
process_data();
print_results();
}
我们编译一下:
gcc -g -Wall -o insert_sort ins.c
注意我们要使用-g选项告诉编译器在可执行文件中保存符号表——我们程序中变量和代码对应的内存地址。
现在我们开始运行一下,我们使用“从小处开始准则”,首先使用两个数进行测试:
./insert_sort 12 5
我们发现该程序没有退出,貌似进入了一个死循环。我们开始使用ddd调试这个程序:
ddd insert_sort
运行程序,传入两个参数:
r 12 5
此时程序一直运行不退出,按Ctrl+C暂停程序的执行
(GDB) r 12 5
^C
Program received signal SIGINT, Interrupt.
0x080484ff in insert (new_y=3) at insert_sort.c:45
/home/gnuhpc/MyCode/Debug/Chapter_01/insert_sort/pg_019/insert_sort.c:45:939:beg:0x80484ff
(GDB)
我们可以看到程序停止在第49行。我们看一下num_y现在的值:
(GDB) p num_y
$1 = 1
这里的$1指的是你要GDB告诉你的第一个变量。找到了这个地方后,我们看看在num_y=1时都发生了什么,我们在insert函数(第27行)设置断 点(你也可以直接使用break insert在这个函数的入口设置断点),并且设置GDB在断点1处(你可以通过info break命令查看断点)只当num_y==1时才停止:
(GDB) b 27
Breakpoint 1 at 0x80484a1: file insert_sort.c, line 27.
(GDB) condition 1 num_y==1
(GDB)
上述命令也可以使用break if合一:
(GDB) break 27 if num_y==1
然后再运行程序,随后用n单步调试发现我们跳到了该函数的出口处:
此时我们看看num_y的值,以便查看到底这个for循环执行的情况。
(GDB) p num_y
$2 = 0
此时的情况是我们进入这个函数时num_y为1,但是现在num_y为0,在中间这个变量被改变了。现在你知道Bug就在30-36行间。同时,通过单步调试你发现31-33行被跳过了,34、35行为注释,那么Bug就在第30或第36行间了。
我们现在仔细看这两行就能得出结论了:30行有个典型的if判断条件写成赋值的错误,致命的是这个变量是全局变量,直接导致49行的for循环变量一直被重置。我们修改后重新编译(可以另开一个编辑器,不用退出ddd),然后再运行
(GDB) r 12 5
5
0
虽然没有了死循环,但是结果还是不对的。
请注意,初始的时候数组y是空的,在#49进行第一次循环时,y[0]应该为12,在第二个循环中,程序应该挪动12为5腾出位置插入,但是此时这个结果看上去是5取代了12。
此时单步调试进入for循环,#37看y[0]的值,的确是12。我们执行到scoot_over函数时,根据自顶向下准则我们单步跳过,继续执行到#41,看看结果对错再决定是不是要单步进入scoot_over函数:
我们发现12根本就没有被移动,说明scoot_over函数有问题,我们去掉insert函数入口的断点,在scoot_over入口处设置断点,当 num_y=1的时候终止:b scoot_over if num_y==1。进一步单步调试后发现这个#23的for循环就没有执行。
(GDB) p jj
$12 = 0
(GDB) p k
$13 = 0
我们看到是因为没有满足for循环条件而不能进入循环。在这里12应该从y[0]移动到y[1],那么我们确定是循环的初始化错误,应该为k = num_y,将这个地方修改后编译运行,程序出现段错误。我们清空所有的断点,然后在
(GDB) r
Program received signal SIGSEGV, Segmentation fault.
0x08048483 in scoot_over (jj=0) at insert_sort.c:24
(GDB)
这里指出在24行出现seg fault,那么要么k超过了数组界限,要么k-1为负的。打印一下k的值,我们就发现:
(GDB) p k
$14 = 992
(GDB)
远远超过k应该有的值。查看num_y 为1,说明在处理第二个要排序的数时出错,再打印jj值,发现为0,就发现我们的for循环k++应该改为k—。
编译运行,发现ok。但是运行多个数据就又出错了:
(GDB) r 12 5 19 22 6 1
1
5
6
12
0
0Program exited with code 06.
(GDB)
我们看到结果中从19开始的排序都有问题,我们在for (j = 0; j < num_y; j++)
(GDB) break 36 if new_y==19
Breakpoint 10 at 0x80484b1: file insert_sort.c, line 36.
(GDB)
单步调试就发现我们没有对当要插入的元素大于所有元素时进行处理。在#44后加入y[num_y] = new_y;重新编译,运行程序正确,至此,我们通过一个简单的例子演示了一下如何使用GDB进行调试。
参考文献:
《Art of Debugging》
《Linux® Debugging and Performance Tuning: Tips and Techniques》
Author:gnuhpc
WebSite:blog.youkuaiyun.com/gnuhpc
1.让程序停下来的三种模式
- 断点(breakpoint):让程序在特定的地点停止执行。
- 观察点(watchpoint):让程序在特定的内存地址(或者 是一个涉及多个地址的表达式)的值发生变化时停止执行。注意,你不能给一个尚没有在栈帧中的表达式或变量设定观察点,换句话说,常常在程序停下来后才去设 置观察点。在设定观察点后,栈帧中不存在所监控的变量时,观察点自动删除。
- 捕捉点(catchpoint):让程序在发生特定事件时停止执行。
注:
- GDB文档中统称这三种程序暂停手段为breakpoint,例如在GDB的delete命令的帮助手册中就是这么描述的,它实际上指代的是这三种暂停手段,本文中以breakpoints统称三种模式,以中文进行分别称呼。
- GDB执行程序到断点(成为断点被hit)时,它并没有执行断点指向的那一行,而是将要指向断点指向的那一行。
- GDB是以机器指令为单位进行执行的,并非是以程序代码行来进行的,这个可能会带来一些困惑,下文有例子详述。
2.GDB breakpoints的查看
命令:i b = info breakpoints。返回列表每一列的含义如下:
- Identifier :breakpoints的唯一标识。
- Type :该breakpoints属于上述三种模式中的哪一个(breakpoint, watchpoint, catchpoint)
- Disposition:该breakpoints下次被hit以后的状态(keep,del,dis分别对应保留、删除、不使能)
- Enable Status:该breakpoints是否使能。
- Address:该breakpoints物理地址。
- Location :若属于断点则指的是断点在哪个文件的第几行,若是观察点则指的是被观察的变量
3.GDB 程序控制的设置
- 断点设置:
- 设置普通断点:break function/line_number/filename:line_number/filename:function. 该断点在被删除或不使能前一直有效。
- 设置临时断点:tbreak function/line_number/filename:line_number/filename:function. 该断点在被hit一次后自动删除。
- 设置一次性断点:enable once breakpoint-list,这个与临时断点的不同是该断点会在被hit一次后不使能,而不是删除。
- 设置正则表达式断点:rbreak regexp 注意该正则表达式是grep型的正则,不是perl或shell的正则语法。
- 设置条件断点:break break-args if (condition) ,例如break main if argc > 1。
- 这个与观察点不同的是,观察点只要所观察的表达式或变量的值有变化程序就停下,而条件断点必须满足所指条件。条件断点在调试循环的时候非常有用,例如break if (i == 70000) 。
- 在已经添加的断点上加上条件使用cond id condition,例如:cond 3 i == 3;想去掉条件转化为普通断点则直接使用cond id,例如,cond 3。
- 注 意,这里的条件外的括号有没有都行,条件中可以使用<, <=, ==, !=, >, >=, &&, ||,&, |, ^, >>, <<,+, -, x, /, %等运算符,也可以使用方法,例如:break test.c:myfunc if ! check_variable_sanity(i),这里的方法返回值一定要是int,否则该条件就会被误读。
- 删除断点:
- delete breakpoint_list
列表中为断点的ID,以空格隔开 - delete
删除全部断点 - clear
删除下一个GDB将要执行的指令处的断点 - clear function/filename:function/line_number/filename:line_number 删除特定地点的断点
- delete breakpoint_list
- 使能断点:enable breakpoint-list
- 不使能断点:disable breakpoint-list
- 跳过断点:ignore id numbers 表示跳过id表示的断点numbers次。
- 注意:
- 若设置断点是以函数名进行的话,C++中函数的重载会带来麻烦,该同名函数会都被设置上断点。请使用如下格式在C++中进行函数断点的设置:TestClass::testFunc(int)
- 设置的断点可能并非是你想放置的那一行。例如:
1:
int main(void) 2: {
3:
int i; 4: i=3;
5:
return 0; 6: }
我们不使用编译器优化进行编译,然后加载到GDB中,如下:
$ gcc -g3 -Wall -Wextra -o test1 test1.c
$ gdb test1
(gdb) break main
Breakpoint 1 at 0x6: file test1.c, line 4.
我们发现显然#4并非是main函数的入口,这是因为这一行是该函数第一行虽然产生了机器码,但是GDB并不认为这对调试有帮助,于是它就将断点设置在第一行对调试有帮助的代码上。我们使用编译器优化再进行编译,情况会更加令人困惑,如下:
$ gcc -O9 -g3 -Wall -Wextra -o test1 test1.c
$ gdb test1
(gdb) break main
Breakpoint 1 at 0x3: file test1.c, line 6.GCC发现i变量一直就没有使用,所以通过优化直接忽略了,于是程序第一行产生机器码的代码恰好是main函数的最后一行。
因此,建议在不影响的情况下,程序调试时将编译器优化选项关闭。
- 同一行有多个断点时,程序只会停下一次,实际上GDB使用的是其中ID最小的那个。
- 在多文件调试中,常常希望GDB在并非当前文件的部分设置断点,而GDB默认关注的是含有main函数的文件,此时你可以使用list functionname、或者单步调试等方式进入另一个文件的源代码中进行设置,此时你设置的行号就是针对这个文件的源代码了。
- 当 你利用代码行进行断点设置时,重新编译程序并在GDB中reload后,断点可能因为你代码行数的变化而发生相对位置变化(GDB指向的行数),这样的情 况下使用DDD直接对原断点进行拖动是最方便的方法,它不会影响该断点的状态和条件,只会改变它所指的位置,从而省去了del一个断点后再在新位置添加设 置新断点的麻烦。
- 在DDD中还可以Redo和Undo对断点的操作。
- 观察点设置:
- 设置写观察点:watch i ; watch (i | j > 12) && i > 24 && strlen(name) > 6,这是两种观察点设置的方式(变量或表达式)。写观察点在该变量的值被程序修改后立刻中止。注意,很多平台都有硬件支持的观察点,默认GDB是优先使用 的就是这些,若暂时不可用,GDB会使用VM技术实现观察点,这样的好处是硬件的速度较快。
- 设置读观察点:rwatch。
- 设置读写观察点:awatch
- 举例:下列简单程序,可以首先在main函数入口设置断点,然后在该断点被hit时设置观察点。
1: #include <stdio.h>
2:
3:
int main(int argc, char **argv) 4: {
5:
int x = 30; 6:
int y = 10; 7:
8: x = y;
9:
10:
return 0; 11: }
这是个非常简单的程序,在main函数入口处断点被hit后我们可以设置rwatch x进行变量监视。
- 程序恢复:
- 单步执行:
- 单步跳过:n = next跳过调用方法的细节,将该行视为一行代码进行执行。next 3表示连续执行三次next。
- 单步进入:s = step 进入调用方法的细节。
- 执行到下一断点:c = continue,程序继续执行直到hit下一个断点。
- 执行到下一栈帧:fin = finish,程序继续执行直到当前栈帧完成。这个常常被用来完成所谓step out的工作,在你不小心按到了step时(你本意其实是想单步跳过),你就可以使用finish跳出该方法。当然,如果你进入了一个迭代函数中的多层以 内,可能一个临时断点+continue或者until会更加有用,后者见下文。
- 执行到具有更高内存地址的机器指令:u = until (后边可以跟上funtionname/linenumber),应用的场景见下边的代码,在我们进入了这个循环后我们想跳出来执行循环后的代码,此时我 们当然可以在循环后的第一行代码设置临时断点,然后continue到那,但这个操作会比较麻烦,最好的方式是使用until,该命令使程序继续运行知道 遇到一个具有更高内存地址的机器指令时停止,而在循环被编译成机器指令时,会将循环条件放在循环体的最底部,所以利用until正好跳出循环进入下一机器 指令(P.S. 你可以使用GCC的-s查看生成的机器指令以便更好的理解这个命令的运行方式):
1: ...previous code...
2:
int i = 9999; 3:
while (i--) { 4: printf("i is %d/n", i);
5: ... lots of code ...
6: }
7: ...future code...
- 单步执行:
- 程序反向调试:
- 这是GDB7以后新加入的功能,如果你在调试的时候发现自己已经错过了想调试的地方,这个操作可以使你不必重新开始调试而直接返回已经执行过的代码进行调试。我们使用下边一个非常简单的程序对这个新版本的功能加以说明:
1: #include <stdio.h>
2:
void foo() { 3: printf("inside foo()");
4:
int x = 6; 5: x += 2;
6: }
7:
8:
int main() { 9:
int x = 0; 10: x = x+2;
11: foo();
12: printf("x = %d/n", x);
13: x = 4;
14:
return(0); 15: }
16:
我们编译一下然后在main函数处设置断点,然后使用record命令开始记录,这是使用反向调试必须的开始步骤,最后进行两步单步调试,打印x的值:(gdb) b main
Breakpoint 1 at 0x804840d: file test.c, line 9.
(gdb) record
Process record: the program is not being run.
(gdb) r
Starting program: /home/gnuhpc/testBreakpoint 1, main () at test.c:9
9int x = 0;
(gdb) record
(gdb) n
10x = x+2;
(gdb)
11foo();
(gdb) print x
$1 = 2
此时x=2,现在我们反向一步单步调试,打印x的值:(gdb)
reverse-next
10x = x+2;
(gdb) p x
$2 = 0
这正是我们想要的。对于断点,反向调试也支持类似正向调试时的continue语句,我们在15行设置断点,然后continue到这个断点:(gdb) b 15
Breakpoint 2 at 0x8048441: file test.c, line 15.
(gdb) c
Continuing.
inside foo()x = 2Breakpoint 2, main () at test.c:15
15}
此时我们在foo函数处加上断点,然后反向continue:(gdb) b foo
Breakpoint 3 at 0x80483ea: file test.c, line 3.
(gdb)reverse-continue
Continuing.Breakpoint 3, foo () at test.c:3
3printf("inside foo()");
程序回到foo入口处。网上有文献反向调试指出必须使用软件观察点,事实并非如此:
(gdb) watch x
Hardware watchpoint 4: x
(gdb) reverse-continue
Continuing.
Hardware watchpoint 4: xOld value = 6
New value = 134513384
foo () at test.c:4
4int x = 6;
(gdb) n
Hardware watchpoint 4: xOld value = 134513384
New value = 6
foo () at test.c:5
5x += 2;
由于篇幅有限我们在此提供手册上的 反向调试命令解释资料,并且附上一篇 教程,读者可以自行进行使用学习。 - 这是GDB7以后新加入的功能,如果你在调试的时候发现自己已经错过了想调试的地方,这个操作可以使你不必重新开始调试而直接返回已经执行过的代码进行调试。我们使用下边一个非常简单的程序对这个新版本的功能加以说明:
- 停止后的命令列表:
- 在程序中止执行时,用户可能会进行一系列操作,GDB提供了这个操作的自动化,类似于批处理脚本一样将需要操作的命令进行批量执行。语法为:
commands breakpoint-id
...
commands
...
end - 例如我们调试斐波那契数列的程序:
1: #include <stdio.h>
2:
int fibonacci(int n); 3:
int main(void) 4: {
5: printf("Fibonacci(3) is %d./n", fibonacci(3));
6:
return 0; 7: }
8:
int fibonacci(int n) 9: {
10:
if(n<=0||n==1) 11:
return 1; 12:
else 13:
return fibonacci(n-1) + fibonacci(n-2); 14: }
由于这是一个递归函数,我们为了追寻递归,要查看程序以什么顺序调用fibonacci()传入的值,当然你可以使用printf进行查看,只是这个方法 看起来很土。我们如下进行调试:(gdb) break fibonacci然后设置命令列表:(gdb) command 1
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just "end".
>silent
>printf "fibonacci was passed %d./n", n
>continue
>end
(gdb) run
Starting program: fibonacci
fibonacci was passed 3.
fibonacci was passed 2.
fibonacci was passed 1.
fibonacci was passed 0.
fibonacci was passed 1.
Fibonacci(3) is 3.
Program exited normally.
(gdb)
这里就能很清晰地看到函数被调用的情况了。当然,你可以将上述命令列表写成一个宏(最多支持10个传入参数,我们使用了两个):(gdb) define print_and_go
Redefine command "print_and_go"? (y or n) y
Type commands for definition of "print_and_go".
End with a line saying just "end".
>printf $arg0, $arg1
>continue
>end使用的时候非常方便:
(gdb) commands 1
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just "end".
>silent>print_and_go "fibonacci() was passed %d/n" n
你甚至可以将这个宏存放在.gdbinit文件(GDB启动时自加载文件)中,以后方便使用,关于gdbinit的使用下文自有介绍。
>end - 在程序中止执行时,用户可能会进行一系列操作,GDB提供了这个操作的自动化,类似于批处理脚本一样将需要操作的命令进行批量执行。语法为:
附注:后文将介绍catchpoint的用法和实例,在此略过。
参考文献:
《Art of Debugging》
《Linux® Debugging and Performance Tuning: Tips and Techniques》
Author:gnuhpc
WebSite:blog.youkuaiyun.com/gnuhpc
本文首先以一个二叉树插入算法的实现作为例子说明GDB查看程序数据的相关方法,代码如下:
1:// bintree.c: routines to do insert and sorted print of a binary tree
2:
3: #include
4: #include
5:
6:struct node {
7:int val; // stored value
8:struct node *left; // ptr to smaller child
9:struct node *right; // ptr to larger child
10: };
11:
12:typedef struct node *nsp;
13:
14: nsp root;
15:
16: nsp makenode(intx)
17: {
18: nsp tmp;
19:
20: tmp = (nsp) malloc(sizeof(structnode));
21: tmp->val = x;
22: tmp->left = tmp->right = 0;
23:return tmp;
24: }
25:
26:void insert(nsp *btp, int x)
27: {
28: nsp tmp = *btp;
29:
30:if (*btp == 0) {
31: *btp = makenode(x);
32:return;
33: }
34:
35:while (1)
36: {
37:if (x < tmp->val) {
38:
39:if (tmp->left != 0) {
40: tmp = tmp->left;
41: }else {
42: tmp->left = makenode(x);
43:break;
44: }
45:
46: }else {
47:
48:if (tmp->right != 0) {
49: tmp = tmp->right;
50: }else {
51: tmp->right = makenode(x);
52:break;
53: }
54:
55: }
56: }
57: }
58:
59:
60:void printtree(nsp bt)
61: {
62:if (bt == 0) return;
63: printtree(bt->left);
64: printf("%d/n",bt->val);
65: printtree(bt->right);
66: }
67:
68:
69:int main(int argc, char *argv[])
70: {
71: root = 0;
72:for (int i = 1; i < argc; i++)
73: insert(&root, atoi(argv[i]));
74: printtree(root);
75: }
在调试这个二叉树插入程序的时候,我们会非常关心insert方法的执行情况,在进入那个while(1)循环后,我们可能会做以下的操作:
(gdb) p tmp->val
$1=12
(gdb) p tmp->left
$2 = (struct node *) 0x8049698
(gdb) p tmp->right
$3 = (struct node *) 0x0
这个操作显得累赘又麻烦,我们可以有以下的改进措施:
1.直接打印结构体tmp:
(gdb) p *tmp
$4 = {val = 12, left = 0x8049698, right = 0x0}
2.使用display命令:我们在#37设置断点,然后运行程序,待程序运行至该断点停下后使用display = disp 命令对某一个变量进行监视(之所以这样做是因为这个变量必须存在在该栈帧上,也就是说调试的时候这个变量的确被创建并且没有被销毁),程序以后只要一停止 就打印这个变量的值在屏幕上:
(gdb) disp *tmp
1: *tmp = {val = 12, left = 0x8049698, right = 0x0}
(gdb) c
Continuing.
Breakpoint 1, insert (btp=0x804967c, x=5) at bintree.c:37
37 if (x < tmp->val) {
1: *tmp = {val = 8, left = 0x0, right = 0x0}
也可以使用dis disp 1使这个监视动作失效(enable disp 1则恢复),undisp 1为删除。info display为查看当前所有自动打印点相关的信息
3.使用命令列表:在上篇中已经叙述,在此不再赘述。
4.使用call命令:我们在代码中已经有了一个打印整个树的函数printtree,使用call命令我们可以直接利用代码中的方法进行变量监视,在每次insert完成的时候调用printtree对二叉树进行打印:
(gdb) commands 2
Type commands for when breakpoint 2 is hit, one per line.
End with a line saying just "end".
>printf "*********** current tree ***********"
>call printtree(root)
>end
5.使用DDD的Data Window图形化表示:单击右键在root这个变量上然后选择display *root,每次在#37行停下时,在Data Window内对整个树的都有图形化表示,在左右子树上,你可以使用右键单击然后选择Display *()来显示。(Tips:你可以以--separate参数启动DDD,这样每个Window都是独立的,你可以获得更大的视野)。
补充:
1.打印数组:p
1:int *x;
2: main()
3: {
4: x = (int*) malloc(25*sizeof(int));
5: x[3] = 12;
6: }
除了可以使用:
(gdb) p *x@25
$1 = {0, 0, 0, 12, 0 }
我们还可以使用:
(gdb) p (int [25]) *x
$2 = {0, 0, 0, 12, 0 }
2.打印本地变量:info locals,会打印当前栈帧的本地变量。
3.以不同形式打印变量:p/paramenters variable
4.打印历史查看过的变量:使用$number,而只使用$表示上一个变量。
(gdb) p tmp->left
$1 = (struct node *) 0x80496a8
(gdb) p *(tmp->left)
$2 = {val = 5, left = 0x0, right = 0x0}
(gdb) p *$1
$3 = {val = 5, left = 0x0, right = 0x0}(gdb) p tmp->left
$1 = (struct node *) 0x80496a8
(gdb) p *$
$2 = {val = 5, left = 0x0, right = 0x0}
5.修改被调试程序运行时的变量值:set x = 12。
6.利用自定义变量方便调试:例如,
1:int w[4] = {12,5,8,29};
2: main()
3: {
4: w[2] = 88;
5: }
我们设置i,然后利用这个变量对这个数组进行遍历:
(gdb) set $i = 0
(gdb) p w[$i++]
$1=12
(gdb)
$2=5
(gdb)
$3=88
(gdb)
$4=29
7.强制类型打印
p {type}address:把address指定的内存解释为type类型(类似于强制转型,更加强)
8.设置一些常见选项
- 1) set print array:打开数组显示,打开后当数组显示时,每个元素占一行,如果不打开的话,每个元素则以逗号分隔。默认关闭
- 2) set print elements num-of-elements:设置GDB打印数据时显示元素的个数,缺省为200,设为0表示不限制(unlimited)
- 3) set print null-stop:设置GDB打印字符数组的时候,遇到NULL时停止,缺省是关闭的
- 4) set print pretty:设置GDB打印结构的时候,每行一个成员,并且有相应的缩进,缺省是关闭的
- 5) set print object:设置GDB打印多态类型的时候,打印实际的类型,缺省为关闭
- 6) set print static-members:设置GDB打印结构的时候,是否打印static成员,缺省是打开的
- 7) set print vtbl:GDB将用比较规整的格式来显示虚函数表,缺省是关闭的
参考文献:
《Art of Debugging》
《Linux® Debugging and Performance Tuning: Tips and Techniques》
Author:gnuhpc
WebSite:blog.youkuaiyun.com/gnuhpc