GDB调试技巧

0. gdb的gui用法

调试代码的时候,只能看到下一行,每次使用list非常烦,不知道当前代码的context 

简单来说就是在以往的gdb开始的时候添加一个-tui选项.有的版本已经有gdbtui这个程序了

在linux自带的终端里是正常显示的,但是在securecrt里面,可能由于编码的问题,边缘会有些乱码,不过不影响使用(如果你的程序有错误输出,会扰乱整个界面,所以在调试的时候,建议添加2>/dev/null,这样的话基本可用) 

启动gdb之后,上面是src窗口,下面是cmd窗口,默认focus在src窗口的,这样的话上下键以及pagedown,pageup都是在移动显示代码,并不显示上下的调试命令.这个时候要切换focus,具体可简单参见

(gdb) info win  查看当前focus
        SRC     (36 lines)  <has focus>
        CMD     (18 lines)
(gdb) fs next  切换focus
Focus set to CMD window.
(gdb) info win 
        SRC     (36 lines)
        CMD     (18 lines)  <has focus>
(gdb) fs SRC  切换指定focus
Focus set to SRC window.
(gdb)

(Window names are case in-sensitive.)




 1..调试复杂的宏定义


       C语言中的宏定义,有着各种各样的好处和坏处,可谓让人有爱有恨。在大型的工程项目中,为了简洁,为了封装,宏的应用必不可少。但是在调试问题时,因为宏定义是被预定义处理的,所以不会有任何的编译符号和调试信息。这样给调试宏定义时,带来了很大的困难。对于开发人员来说,除了直接肉眼去看宏定义,自己来展开宏定义去确定问题,是否还有其它手段来调试宏定义吗?

本文介绍两种调试宏定义的小技巧:

第一个方法是通过gcc -E 产生预编译后的源代码,即源代码经过预编译后的结果,所有的预编译动作都已完成。如头文件的插入,宏定义的展开。
如下面的代码:
  1. #include <stdlib.h>
  2. #include <stdio.h>

  3. #define MACRO1(x)(++(x))
  4. #define MACRO2(x)(MACRO1(x)+100)
  5. #define MACRO3(x)(MACRO2(x)+200)


  6. int main(void)
  7. {
  8.     int a = 0;
  9.     int b = 0;

  10.     b = MACRO3(a);

  11.     printf("%d\n", b);

  12.     return 0;
  13. }
这里的MACRO3嵌套调用了MACRO2,MACRO1。在真正的代码中,这种用法很常见,不过这处的宏定义很简单,即使是嵌套调用也很容易看出。此处只是一个示意。
Ok,使用gcc -E test.c > test.e,得到预编译后的代码:
  1. /*
  2. 前面是1800+行的头文件代码,此处省略
  3. */

  4. int main(void)
  5. {
  6.     int a = 0;
  7.     int b = 0;

  8.     b = (((++(a))+100)+200);

  9.     printf("%d\n", b);

  10.     return 0;
  11. }
这里可以清晰的看到b = (((++(a))+100)+200);这个就比刚才的宏定义要清楚的多。

但是从这个例子也可以看到这个方法的局限性。
1. 由于预编译处理会执行所有的预处理代码,包括头文件的插入,这导致最后的代码行数太多。
2. 得到的了一个新的代码文件。这样的话,在大型工程中,如果需要调试多个文件中的宏定义,需要我们一个一个的预编译,太麻烦了。


下面看看第二个方法,这个方法要比第一种方法方便得多。
我们都知道为了调试程序,需要使用-g选项,它的作用就是将调试信息加入到最后的二进制可执行文件中。但是你可知道-g 也通-o一样,是分级别的。当不指定级别的时候,其level为2。为了调试宏定义,我们可以使用更高的级别-g3。
下面为我使用-g3编译上面的代码,然后进行调试:
  1. Breakpoint 1, main () at test.c:11
  2. 11 int a = 0;
  3. Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.i686
  4. (gdb) n
  5. 12 int b = 0;
  6. (gdb)
  7. 14 b = MACRO3(a);
  8. (gdb)
  9. 16 printf("%d\n", b);
  10. (gdb) macro expand MACRO3(a)
  11. expands to:(((++(a))+100)+200)
  12. (gdb) macro expand MACRO3(0)
  13. expands to:(((++(0))+100)+200)
  14. (gdb) macroexp MACRO3(0)
  15. expands to:(((++(0))+100)+200)
  16. (gdb)
在调试的过程中,可以使用macro expand/exp 来展开宏定义。从上面的调试过程中,可以直接看到宏定义展开后的结果。并且我们还可以给宏传入任何的一个值,如:
  1. (gdb) macroexp MACRO3(3)
  2. expands to:(((++(3))+100)+200)
  3. (gdb)

第二个方法无疑比第一个方法要方便简单得多。我们只需要在全局的Makefile中添加新的编译参数-g3,就可以支持整个工程代码中所有的宏的调试。当然这个方法也有一个缺点,就是g3的调试信息会比默认的g2的调试信息要大——自然嘛,不然gdb如何知道怎样展开宏定义呢。

2.多线程调试

先介绍一下GDB多线程调试的基本命令。

info threads 显示当前可调试的所有线程,每个线程会有一个GDB为其分配的ID,后面操作线程的时候会用到这个ID。 前面有*的是当前调试的线程。

thread ID 切换当前调试的线程为指定ID的线程。

break thread_test.c:123 thread all 在所有线程中相应的行上设置断点

thread apply ID1 ID2 command 让一个或者多个线程执行GDB命令command。 

thread apply all command 让所有被调试线程执行GDB命令command。

set scheduler-locking off|on|step 估计是实际使用过多线程调试的人都可以发现,在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?通过这个命令就可以实现这个需求。off 不锁定任何线程,也就是所有线程都执行,这是默认值。 on 只有当前被调试程序会执行。 step 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行。

 

gdb对于多线程程序的调试有如下的支持:

  • 线程产生通知:在产生新的线程时, gdb会给出提示信息

(gdb) r
Starting program: /root/thread
[New Thread 1073951360 (LWP 12900)]
[New Thread 1082342592 (LWP 12907)]---以下三个为新产生的线程
[New Thread 1090731072 (LWP 12908)]
[New Thread 1099119552 (LWP 12909)]

  • 查看线程:使用info threads可以查看运行的线程。

(gdb) info threads
  4 Thread 1099119552 (LWP 12940)   0xffffe002 in ?? ()
  3 Thread 1090731072 (LWP 12939)   0xffffe002 in ?? ()
  2 Thread 1082342592 (LWP 12938)   0xffffe002 in ?? ()
* 1 Thread 1073951360 (LWP 12931)   main (argc=1, argv=0xbfffda04) at thread.c:21
(gdb)

注意,行首的蓝色文字为gdb分配的线程号,对线程进行切换时,使用该该号码,而不是上文标出的绿色数字。

另外,行首的红色星号标识了当前活动的线程

  • 切换线程:使用 thread THREADNUMBER 进行切换,THREADNUMBER为上文提到的线程号。下例显示将活动线程从 1 切换至 4。

(gdb) info threads
   4 Thread 1099119552 (LWP 12940)   0xffffe002 in ?? ()
   3 Thread 1090731072 (LWP 12939)   0xffffe002 in ?? ()
   2 Thread 1082342592 (LWP 12938)   0xffffe002 in ?? ()
* 1 Thread 1073951360 (LWP 12931)   main (argc=1, argv=0xbfffda04) at thread.c:21
(gdb) thread 4
[Switching to thread 4 (Thread 1099119552 (LWP 12940))]#0   0xffffe002 in ?? ()
(gdb) info threads
* 4 Thread 1099119552 (LWP 12940)   0xffffe002 in ?? ()
   3 Thread 1090731072 (LWP 12939)   0xffffe002 in ?? ()
   2 Thread 1082342592 (LWP 12938)   0xffffe002 in ?? ()
   1 Thread 1073951360 (LWP 12931)   main (argc=1, argv=0xbfffda04) at thread.c:21
(gdb)

 

后面就是直接在你的线程函数里面设置断点,然后continue到那个断点,一般情况下多线程的时候,由于是同时运行的,最好设置 set scheduler-locking on

这样的话,只调试当前线程


3. GDB技巧:使用checkpoint解决难以复现的Bug



作为程序员,调试是一项很重要的基本功。调试的技巧和水平,直接决定了解决问题的时间。一般情况下,GDB的基本命令已经足以应付大多数问题了。但是,对于有些问题,还是需要更高级一些的命令。今天介绍一下checkpoint。

有一些bug,可能很难复现,当好不容易复现一次,且刚刚进入程序的入口时,我们需要珍惜这个来之不易的机会。如果只使用基本命令的话,对于大部分代码,我们都需要使用step来步进。这样无疑会耗费大量的时间,因为大部分的代码可能都没有问题。可是一旦不小心使用next,结果恰好该语句的函数调用返回出错。那么对于这次来之不易的机会,我们只得到了部分信息,即确定问题出在该函数,但是哪里出错还是不清楚。于是还需要再一次的复现bug,时间就这样浪费了。

所以,对于这种问题,就是checkpoint大显身手的时候。先看一下GDB关于checkpoint的说明:
On certain operating system(Currently, only GNU/Linux), GDB is able to save a snapshot of a program's state, called a checkpoint and come back to it later.
Returning to a checkpoint effectively undoes everything that has happened in the program since the checkpoint was saved. This includes changes in memory, register, and even(within some limits) system state. Effectively, it is like going back in time to the moment when the checkpoint was saved.
也就是说checkpoint是程序在那一刻的快照,当我们发现错过了某个调试机会时,可以再次回到checkpoint保存的那个程序状态。

举例说明一下:
  1. #include <stdlib.h>
  2. #include <stdio.h>

  3. static int func()
  4. {
  5.     static int i = 0;
  6.     ++i;
  7.     if (i== 2){
  8.         return 1;
  9.     }
  10.     return 0;
  11. }

  12. static int func3()
  13. {
  14.     return func();
  15. }

  16. static int func2()
  17. {
  18.     return func();
  19. }

  20. static int func1()
  21. {
  22.     return func();
  23. }

  24. int main()
  25. {
  26.     int ret = 0;

  27.     ret += func1();
  28.     ret += func2();
  29.     ret += func3();

  30.     return ret;
  31. }
当我们执行这个程序时,发现程序返回1,不是期望的成功0。于是开始调试程序,由于函数调用的嵌套过多,我们没法一眼看出是main中的哪个函数调用出错了。于是在ret += func1()前,我们保存一个checkpoint。
  1. (gdb) b main
  2. Breakpoint 1 at 0x80483e0: file test.c, line 31.
  3. (gdb) r
  4. Starting program: /home/fgao/works/test/a.out

  5. Breakpoint 1, main () at test.c:31
  6. 31 int ret = 0;
  7. Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.i686
  8. (gdb) n
  9. 33 ret += func1();
  10. (gdb) checkpoint
  11. checkpoint: fork returned pid 2060.
  12. (gdb)
然后使用next步进,并每次调用完毕,打印ret的值
  1. Breakpoint 1, main() at test.c:31
  2. 31 int ret = 0;
  3. (gdb) n
  4. 33 ret += func1();
  5. (gdb) checkpoint
  6. checkpoint: fork returned pid 2060.
  7. (gdb) n
  8. 34 ret += func2();
  9. (gdb) p ret
  10. $4 = 0
  11. (gdb) n
  12. 35 ret += func3();
  13. (gdb) p ret
  14. $5 = 1
结果发现,在调用func2()调用后,ret的值变为了1。可是此时,我们已经错过了调试func2的机会。如果没有checkpoint,就需要再次从头调试了——对于这个问题从头调试很容易,但是对于很难复现的bug可就不说那么容易的事情了。

ok,使用checkpoint恢复
  1. (gdb) restart 1
  2. Switching to process 2060
  3. #0 main () at test.c:33
  4. 33 ret += func1();
  5. (gdb)
很简单,现在GDB恢复到了保存checkpoint时的状态了。上面“restart 1“中的1为checkpoint的id号,可以使用info查看。
  1. (gdb) info checkpoints
  2. * 1 process 2060 at 0x80483e7, file test.c, line 33
  3.   0 process 2059 (main process) at 0x80483f7, file test.c, line 35

从上面可以看出checkpoint的用法很简单,但是很有用。就是在平时的简单的bug修正中,也可以加快我们的调试速度——毕竟减少了不必要的重现bug的时间。



4.GDB调试技巧:gdb at pid无法调试的问题

当我们使用GDB调试一个daemon的时候,一般有两种方式:
1. 直接在shell命令行键入gdb attach pid (要调试daemon的进程ID)。一般情况下,我都习惯于缩写gdb at pid;
2. 在shell中键入gdb,进入gdb,然后attach pid(同样是要调试daemon的进程ID)。我不常用这种方式,因为这种方式需要2个步骤——尽管键入的字母是一样的。

下面来说说今天遇到的问题。公司的一个实习生来问我问题。他目前正在做一个工作,大致的功能是得到某个daemon的某一时刻的调用栈——就像gdb一样。但是他发现当daemon处于系统调用状态时,从EBP得到的返回地址不正确。然后我让他直接使用gdb查看是否可以看到完整的调用栈,如果GDB可以,那么就没有问题,可以去看GDB如何实现的。如果不能,再想为什么。——这里先插一句,当时我去查看了他的EBP的值,根据其值获得的返回地址确实不正确。今晚查了查,知道在某些函数调用时,为了速度,EBP有可能并没有被压栈。但是这个不是今天的主题,让我们再转回来。他很快的试了试,告诉我gdb虽然可以使用bt(backtrace)得到函数栈,但是显示出来的结果却不正确。

怎么回事呢?不可能啊。我在他那写了一个很简单的测试程序:
  1. #include <stdlib.h>
  2. #include <stdio.h>

  3. static void test(void)
  4. {
  5.     pause();
  6. }


  7. int main(void)
  8. {
  9.     test();

  10.     return 0;
  11. }
编译并运行:
  1. [xxx@xxx-vm-fc13 test]$ gcc-g test.c
  2. [xxx@xxx-vm-fc13 test]$./a.out
然后使用另外一个终端进行调试:
  1. [xxx@xxx-vm-fc13 test]$ ps auwx|grep a.out
  2. xxx 2412 0.0 0.0 1816 288 pts/4 S+ 11:19 0:00./a.out
  3. xxx 2415 0.0 0.0 4308 732 pts/5 S+ 11:20 0:00 grep a.out
  4. [xxx@xxx-vm-fc13 test]$ gdb at 2412
  5. GNU gdb (GDB) Fedora(7.1-18.fc13)
  6. Copyright (C) 2010 Free Software Foundation, Inc.
  7. License GPLv3+: GNU GPL version 3or later <http://gnu.org/licenses/gpl.html>
  8. This is free software: you are freeto change and redistribute it.
  9. There is NO WARRANTY,to the extent permitted by law. Type"show copying"
  10. and "show warranty"for details.
  11. This GDB was configured as "i686-redhat-linux-gnu".
  12. For bug reporting instructions, please see:
  13. <http://www.gnu.org/software/gdb/bugs/>...
  14. Reading symbols from /usr/bin/at...(no debugging symbols found)...done.
  15. Attaching to program:/usr/bin/at, process 2412
  16. Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
  17. Loaded symbols for /lib/ld-linux.so.2
  18. 0x009f4424 in __kernel_vsyscall ()
  19. Missing separate debuginfos, use: debuginfo-install at-3.1.12-5.fc13.i686
  20. (gdb) bt
  21. #0 0x009f4424 in __kernel_vsyscall ()
  22. #1 0x0065dac6 in ?? ()
  23. #2 0x080483dc in ?? ()
  24. #3 0x005d6cc6 in ?? ()
  25. #4 0x08048331 in ?? ()
  26. (gdb)
看到这个结果,我还真有些意外。我使用gdb也有些年头了,从来都是这样调试daemon的啊。怎么会这样呢啊?看这个输出,很像没有调试信息啊。可是前面的编译有-g选项啊。

使用第二种方法试试?
  1. [xxx@xxx-vm-fc13 test]$ gdb
  2. GNU gdb (GDB) Fedora(7.1-18.fc13)
  3. Copyright (C) 2010 Free Software Foundation, Inc.
  4. License GPLv3+: GNU GPL version 3or later <http://gnu.org/licenses/gpl.html>
  5. This is free software: you are freeto change and redistribute it.
  6. There is NO WARRANTY,to the extent permitted by law. Type"show copying"
  7. and "show warranty"for details.
  8. This GDB was configured as "i686-redhat-linux-gnu".
  9. For bug reporting instructions, please see:
  10. <http://www.gnu.org/software/gdb/bugs/>.
  11. (gdb) at 2412
  12. Attaching to process 2412
  13. Reading symbols from /home/fgao/works/test/a.out...done.
  14. Reading symbols from /lib/libc.so.6...(no debugging symbols found)...done.
  15. Loaded symbols for /lib/libc.so.6
  16. Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
  17. Loaded symbols for /lib/ld-linux.so.2
  18. 0x009f4424 in __kernel_vsyscall ()
  19. Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.i686
  20. (gdb) bt
  21. #0 0x009f4424 in __kernel_vsyscall ()
  22. #1 0x0065dac6 in __pause_nocancel () from /lib/libc.so.6
  23. #2 0x080483cf in test () at test.c:6
  24. #3 0x080483dc in main () at test.c:12
  25. (gdb)
啊,这种方式就没有问题啊。怎么回事呢?难道是因为这两种方式的attach还有区别不成?于是我又查了gdb的说明,应该没有区别啊。

想了半天,又试了几次第一种方式,还是没有结果。当我感觉我以前的GDB的世界观都要崩溃的时候,对自己的GDB技能产生了极度的不自信的时候,再次退出gdb的时候,我发现了原因。该原因差点没把我郁闷死。。。

大家想一下哦。。。

  1. (gdb) q
  2. A debugging session is active.

  3. Inferior 1 [process 2412] will be detached.

  4. Quit anyway? (y or n) y
  5. Detaching from program: /usr/bin/at, process 2412
注意看这里的红色代码。进程号没有错,但是program却是/usr/bin/at。My god!原来我使用第一种方式的时候,一直在尝试调试at——这个linux命令。怪不得没有调试信息呢!在我以前的GDB应用中,因为系统中没有at这个命令,所以gdb at pid就等于gdb attach pid,是可以缩写的。但是在当前这个环境中,很并不幸的存在着at这个命令。结果GDB没有尝试去attach,而是直接去调试at命令。

不过这里,我觉得GDB可以做得更友好一些。虽然在这个情况下,GDB去调试at没有问题,但是后面的PID已经被我的测试程序占用了,它应该报个错误或者警告吧。

最后总结一下,GDB的缩写是好,但是小心过分的缩写会与已有命令冲突哦~~总之,缩写需谨慎。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值