S.l.e!ep.¢%

像打了激速一样,以四倍的速度运转,开心的工作
简单、开放、平等的公司文化;尊重个性、自由与个人价值;
posts - 1098, comments - 335, trackbacks - 0, articles - 1
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

GDB怎么调试运行着的程序

Posted on 2010-11-04 17:03 S.l.e!ep.¢% 阅读(2749) 评论(0)  编辑 收藏 引用 所属分类: Unix

GDB怎么调试运行着的程序

Posted by 机器人 on 10th 九月 2009 in c/c++, linux/server

对于GDB这里就不作介绍了,随便找个搜索引擎一搜GDB,介绍就已经是很详细了,这篇文章主要是来谈怎么使用GDB来调试一个运行着的程序,或者说怎么调试一个进程,似乎标题有些拗口,其次也会对fork()分离出现的多子进程的调试加以说明。
下面是一段测试代码。
test.c

										#include < stdio.h >
										#include < unistd.h >
 
staticvoid PrintMessage(int i);staticvoid GoToSleep(void);
 
 
int main(void){int i =100000;
 
	while(1){
		PrintMessage( i );
		GoToSleep();
		i -=1;}
 
	return0;}
 
 
 
void PrintMessage(int i){char buf[1024];
		sprintf(buf,"%d bottles of beer on the wall.\n", i);printf("%s",buf);
 
}
 
 
 
staticvoid 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$ gdbtest
GNU gdb6.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:1010		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--pid25552

方式二
通过程序和进程号来绑定。

~@hqlong gdbtest25552

方式二
先启动gdb后,通过attach来绑定pid

~@hqlong gdbgdb) 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--pid25615
GNU gdb6.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:1515			GoToSleep();
(gdb)

这里我们选择最底层的栈,也就是整个程序的入口main函数来作为起点。
4. 调试跟踪过程
使用n(next)来一步一步的跟踪。

										(
										gdb
										) n
Single stepping untilexit from functionsleep, 
which has no line number information.
GoToSleep () at beer-process.c:3636}(gdb) 
main () at beer-process.c:1616			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:2525{(gdb) n
27	    sprintf(buf,"%d bottles of beer on the wall.\n", i);
(gdb) n
28printf("%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 12times>, "�\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);

发现运行到这一步时,启动程序的那个窗口打印出了一条信息。

										99897 bottles of beer on the wall.

这说明,程序正好运行到了打印字符的地方。
如果这个时候想退出函数,直接返回,可能通过 finish.

										(
										gdb
										) finish 
Run till exit from #0  PrintMessage (i=99895) at beer-process.c:28
main () at beer-process.c:1515			GoToSleep();
(gdb)
退出PrintMessage函数,接着运行下一行代码GoToSleep.
如果调试完毕,或能通过q(quit)来退出GDB。
										(
										gdb
										) q
The program is running.  Quit anyway (and detach it)? (y or n) y
Detaching from program: /tmp/test, process 25615

以上是调试一个进程的过程。

下面附带说明一下怎么调试一个需要通过fork()分离进程的程序。下面是一段一个Web服务器的在接受请求时的代码片断。

										for
										(
										;;
										)
										{
										if
										(
										(connfd = accept(listenfd, (struct sockaddr *)&clientaddr,
					&clientaddrlen)) == -1){perror("http server: accept error");
		continue;
	}if(fork() == 0)
		accept_request(connfd);
	else close(connfd);
 
	int statloc;
	waitpid(-1, &statloc, WNOHANG);
}

当单步运行到if(fork() == 0)这一步时,那怕是分离进程成功,也不会运行accept_request(connfd);而是直接跳到跳过,运行下面的代码。在调试这样的程序里,需要在进入gdb控制台时,需要将follow-fork-mode的值设成child.

										(
										gdb
										)
										set follow-fork-mode child

本文主要是体验一下使用GDB的调试过程,对一些指令和俗语没有作过多的解释,如果有不明白可直接留言,或者参考相关资料。
参数资料
http://www.gnu.org/software/gdb/
http://dirac.org/linux/gdb/06-Debugging_A_Running_Process.php

结束


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理