http://blog.chinaunix.net/uid-21712186-id-1818222.html
这篇文章主要是来谈怎么使用GDB来调试一个运行着的程序,或者说怎么调试一个进程,似乎标题有些拗口,其次也会对fork()分离出现的多子进程的调试加以说明。
下面是一段测试代码。
test.c
#include < stdio.h > #include < unistd.h > static void PrintMessage(int i); static void GoToSleep(void); int main(void) { int i = 100000; while ( 1 ) { PrintMessage( i ); GoToSleep(); i -= 1; } return 0; } void PrintMessage(int i) { char buf[1024]; sprintf(buf,"%d bottles of beer on the wall.\n", i); printf("%s",buf); } static void GoToSleep(void) { sleep(3); }
接下来是编译时使用的Makefile文件.
TARGET = test SRC = $(TARGET).c OBJ = $(TARGET).o CC = gcc CFLAGS = -g3 -W -Wall -std=c99 $(TARGET): $(OBJS) .PHONY: clean clean: $(RM) $(TARGET) $(OBJS)
此程序是一个服务程序,程序一旦启动,将作为一个进程永驻内存,可以通过
~@hqlong ps -ef | grep "test"
来查看该进程的信息。
此程序主要实现每3秒钟向墙上打印一瓶啤酒。对于这样的一个启动就作为一个进程进驻内存的程序应该怎么来进行调试呢?接下来的事情就是要来回来这个问题,
通过make来对源文件进行编译。
~@hqlong make
这里会在当前目录下产生一个test的可执行文件。
在对程序进行正式调试之前来回忆一个使用GDB调试一个非服务程序的步骤。假设test这个可执行文件是一个非服务程序,那么一般是通过如下几步方式来进行调试的。
hqlong@ubuntu:/tmp$ gdb test GNU gdb 6.8-debian Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http ://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i486-linux-gnu"... (gdb) b 1 Breakpoint 1 at 0x80483f4: file beer-process.c, line 1. (gdb) r Starting program: /tmp/test Breakpoint 1, main () at beer-process.c:9 warning: Source file is more recent than executable. 9 { (gdb) n main () at beer-process.c:10 10 int i = 100000; (gdb) q The program is running. Exit anyway? (y or n) y </http>
首先是通过gdb test来调试程序。然后使用b(break) 1在第一行设置断点,然后使用r(run) 来运行程序,最后使用n来单步运行程序,如果想要查看运行中某变量的值,可能通过p(print)来打印。如查看i的值,就可以通过p i。最后使用q(quit)来退出程序。
由于服务程序一旦启动,就以进程的方式进驻内存,不退出,所以和非服务程序的调试方式有一些区别。
服务一旦启动后,系统会分配一个pid,然后使用gdb绑定上这个pid,最后就可以通过通用方式进行调试了。
绑定进程的方式有下几种。
hqlong@ubuntu:/tmp$ ./test & 100000 bottles of beer on the wall. [1] 25292
方式一
通过–pid参数来绑定指定的进程程序。
~@hqlong gdb --pid 25552
方式二
通过程序和进程号来绑定。
~@hqlong gdb test 25552
方式二
先启动gdb后,通过attach来绑定pid
~@hqlong gdb gdb) attach 25552
将pid和gdb绑定后,就可以来对每一段代码进行调试。
下面是对上面例子的完整调试过程。
1. 启动进程
hqlong@ubuntu:/tmp$ ./test & [1] 25615 hqlong@ubuntu:/tmp$ 100000 bottles of beer on the wall. 99999 bottles of beer on the wall. 99998 bottles of beer on the wall.
可以看见,启动test后,系统所分配的pid(进程号)为25615,然后每隔3秒钟就打印出一条信息。
2.使用gdb来绑定test进程。
这里需要重新启动一个新的shell来进行调试,也就是新开一个窗口,然后使用
hqlong@ubuntu:/tmp$ gdb --pid 25615 GNU gdb 6.8-debian Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http ://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i486-linux-gnu". Attaching to process 25615 Reading symbols from /tmp/test...done. Reading symbols from /lib/tls/i686/cmov/libc.so.6...done. Loaded symbols for /lib/tls/i686/cmov/libc.so.6 Reading symbols from /lib/ld-linux.so.2...done. Loaded symbols for /lib/ld-linux.so.2 0xb7ffb430 in __kernel_vsyscall () (gdb) </http>
来将gdb与已启动的test进行绑定,从而进入调试状态,这里会发现,当进入gdb调试状态后,先前启动程序的窗口就不再每隔3秒钟打印信息了,而是将控制权将给了gdb.
3.使用bt来显示当前程序的函数调用栈结构(也就是函数的调用顺序)。
(gdb) bt #0 0xb7ffb430 in __kernel_vsyscall () #1 0xb7f23780 in nanosleep () from /lib/tls/i686/cmov/libc.so.6 #2 0xb7f235be in sleep () from /lib/tls/i686/cmov/libc.so.6 #3 0x0804851e in GoToSleep () at beer-process.c:35 #4 0x080484ac in main () at beer-process.c:15 (gdb)
可知函数的调用过程为:main调用GoToSleep,GoToSleep调用sleep等。
使用frame来选择程序的调试起点,也可以使用break来选择指定的行或者函数来作为断点。
(gdb) frame 4 #4 0x080484ac in main () at beer-process.c:15 15 GoToSleep(); (gdb)
这里我们选择最底层的栈,也就是整个程序的入口main函数来作为起点。
4. 调试跟踪过程
使用n(next)来一步一步的跟踪。
(gdb) n Single stepping until exit from function sleep, which has no line number information. GoToSleep () at beer-process.c:36 36 } (gdb) main () at beer-process.c:16 16 i -= 1; (gdb) 14 PrintMessage( i ); (gdb)
这里有个技巧,如果要重复执行上一次执行的操作,可以直接回车。
通过上面的调试,发现程序运行到了14行,也就是PrintMessage函数处,如果这个时候我们继续n(next),程序就直接跳过函数,运行到函数的下一行,但如果想进入函数内部去调试,可以使用s(step),来进入函数体内进行调试。
进入函数体
(gdb) 14 PrintMessage( i ); (gdb) s PrintMessage (i=99897) at beer-process.c:25 25 { (gdb) n 27 sprintf(buf,"%d bottles of beer on the wall.\n", i); (gdb) n 28 printf("%s",buf); (gdb)
以上是进入PrintMessage体内,然后继续使用n(next)来一步一步的运行。进行到了28行,发现有两个变量,一个为i,另一个为buf,可以通过p(print)来打印该变量的值。
(gdb) p i $1 = 99897 (gdb) p buf $2 = "99897 bottles of beer on the wall.\n\000\000R\000�0u������$u���\227\001�\000\000\000\000p���\000\000\000\000\000\000\000\000\001\000\000\000��DB�x��8���\000\000\000\000�\217\001�p\226\001�0u��$u���\226\000�\001\000\000\000\000\000\000\000�v���\222\001�\205���", '\0' <repeats 12 times>, "�\226\000��\217\001�\000\000\000\000\000u���t���\020\001�\b\000\000\000\f\000\000\000\000����v��b\221\000�\000���\020���\f\000\000\000�\217"... (gdb) </repeats>
发现通过p i 来打印i的值,结果是正确的,但通过p buf来打印buf 的值,却显示了很多不可读的乱码。这里因为buf代表这个变量在内存中的首地址,所以p不知道变量在什么时候结束,而i则不同,因为i是整型,在内存中一般占4个字节,所以p直接从首地址向后数4位就行了。所以这里打印字符指针或者数组时,需要指定一个长度。语法为 p *buf@len. 这里的len可以通过strlen(buf)来取得。
(gdb) p *buf@strlen(buf) $3 = "99897 bottles of beer on the wall.\n" (gdb)
这一下子就对了。
接下来继续使用n(next)来单步运行。
(gdb) 28 printf("%s",buf);
发现运行到这一步%