分享

Linux环境下的C/C+基础调试技术2——程序控制

 拨开云雾见天日 2010-12-10
1.让程序停下来的三种模式
•断点(breakpoint):让程序在特定的地点停止执行。
•观察点(watchpoint):让程序在特定的内存地址(或者是一个涉及多个地址的表达式)的值发生变化时停止执行。注意,你不能给一个尚没有在栈帧中的表达式或变量设定观察点,换句话说,常常在程序停下来后才去设置观察点。在设定观察点后,栈帧中不存在所监控的变量时,观察点自动删除。
•捕捉点(catchpoint):让程序在发生特定事件时停止执行。
注:
•GDB文档中统称这三种程序暂停手段为breakpoint,例如在GDB的delete命令的帮助手册中就是这么描述的,它实际上指代的是这三种暂停手段,本文中以breakpoints统称三种模式,以中文进行分别称呼。
•GDB执行程序到断点(成为断点被hit)时,它并没有执行断点指向的那一行,而是将要指向断点指向的那一行。
•GDB是以机器指令为单位进行执行的,并非是以程序代码行来进行的,这个可能会带来一些困惑,下文有例子详述。
2.GDB breakpoints的查看
命令:i b = info breakpoints。返回列表每一列的含义如下:
•Identi?er :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 删除特定地点的断点
•使能断点: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/test
Breakpoint 1, main () at test.c:9
9               int x = 0;
(gdb) record
(gdb) n
10              x = x+2;
(gdb)
11              foo();
(gdb) print x
$1 = 2

此时x=2,现在我们反向一步单步调试,打印x的值:
(gdb) reverse-next
10              x = 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 = 2
Breakpoint 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
3               printf("inside foo()");

程序回到foo入口处。网上有文献反向调试指出必须使用软件观察点,事实并非如此:
(gdb) watch x
Hardware watchpoint 4: x
(gdb) reverse-continue
Continuing.
Hardware watchpoint 4: x
Old value = 6
New value = 134513384
foo () at test.c:4
4               int x = 6;
(gdb) n
Hardware watchpoint 4: x
Old value = 134513384
New value = 6
foo () at test.c:5
5               x += 2;
 

由于篇幅有限我们在此提供手册上的反向调试命令解释资料,并且附上一篇教程,读者可以自行进行使用学习。
•停止后的命令列表:
•在程序中止执行时,用户可能会进行一系列操作,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
>end
你甚至可以将这个宏存放在.gdbinit文件(GDB启动时自加载文件)中,以后方便使用,关于gdbinit的使用下文自有介绍。
附注:后文将介绍watchpoint的用法和实例,在此略过。
参考文献:
《Art of Debugging》
《Linux® Debugging and Performance Tuning: Tips and Techniques》

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多