展示自己、证明自己
分类: C/C++
2013-10-18 10:54:57
GDB 是 linux 系统上常用的 c/c++ 调试工具,功能十分强大。对于较为复杂的系统,比如多进程系统,如何使用 GDB 调试呢?考虑下面这个三进程系统:
Proc2 是 Proc1 的子进程,Proc3 又是 Proc2 的子进程。如何使用 GDB 调试 proc2 或者 proc3 呢?
实际上,GDB 没有对多进程程序调试提供直接支持。例如,使用GDB调试某个进程,如果该进程fork了子进程,GDB会继续调试该进程,子进程会不受干扰地运行下去。 如果你事先在子进程代码里设定了断点,子进程会收到SIGTRAP信号并终止。那么该如何调试子进程呢?其实我们可以利用GDB的特点或者其他一些辅助手 段来达到目的。此外,GDB 也在较新内核上加入一些多进程调试支持。
接下来我们详细介绍几种方法,分别是 follow-fork-mode 方法,attach 子进程方法和 GDB wrapper 方法。
follow-fork-mode
在2.5.60版Linux内核及以后,GDB对使用fork/vfork创建子进程的程序提供了follow-fork-mode选项来支持多进程调试。
follow-fork-mode的用法为:
set follow-fork-mode [parent|child]
因此如果需要调试子进程,在启动gdb后:
(gdb) set follow-fork-mode child |
并在子进程代码设置断点。
此外还有detach-on-fork参数,指示GDB在fork之后是否断开(detach)某个进程的调试,或者都交由GDB控制:
set detach-on-fork [on|off]
注意,最好使用GDB 6.6或以上版本,如果你使用的是GDB6.4,就只有follow-fork-mode模式。
follow-fork-mode/detach-on-fork的使用还是比较简单的,但由于其系统内核/gdb版本限制,我们只能在符合要求的 系统上才能使用。而且,由于follow-fork-mode的调试必然是从父进程开始的,对于fork多次,以至于出现孙进程或曾孙进程的系统,例如上 图3进程系统,调试起来并不方便。
Attach子进程
众所周知,GDB有附着(attach)到正在运行的进程的功能,即attach
例如我们要调试某个进程RIM_Oracle_Agent.9i,首先得到该进程的pid
[root@tivf09 tianq]# ps -ef|grep RIM_Oracle_Agent.9i nobody 6722 6721 0 05:57 ? 00:00:00 RIM_Oracle_Agent.9i root 7541 27816 0 06:10 pts/3 00:00:00 grep -i rim_oracle_agent.9i |
通过pstree可以看到,这是一个三进程系统,oserv是RIM_Oracle_prog的父进程,RIM_Oracle_prog又是RIM_Oracle_Agent.9i的父进程。
[root@tivf09 root]# pstree -H 6722 |
启动GDB,attach到该进程
现在就可以调试了。一个新的问题是,子进程一直在运行,attach上去后都不知道运行到哪里了。有没有办法解决呢?
一个办法是,在要调试的子进程初始代码中,比如main函数开始处,加入一段特殊代码,使子进程在某个条件成立时便循环睡眠等待,attach到进程后在该代码段后设上断点,再把成立的条件取消,使代码可以继续执行下去。
至于这段代码所采用的条件,看你的偏好了。比如我们可以检查一个指定的环境变量的值,或者检查一个特定的文件存不存在。以文件为例,其形式可以如下:
void debug_wait(char *tag_file) { while(1) { if (tag_file存在) 睡眠一段时间; else break; } } |
当attach到进程后,在该段代码之后设上断点,再把该文件删除就OK了。当然你也可以采用其他的条件或形式,只要这个条件可以设置/检测即可。
Attach进程方法还是很方便的,它能够应付各种各样复杂的进程系统,比如孙子/曾孙进程,比如守护进程(daemon process),唯一需要的就是加入一小段代码。
GDB wrapper
很多时候,父进程 fork 出子进程,子进程会紧接着调用 exec族函数来执行新的代码。对于这种情况,我们也可以使用gdb wrapper 方法。它的优点是不用添加额外代码。
其基本原理是以gdb调用待执行代码作为一个新的整体来被exec函数执行,使得待执行代码始终处于gdb的控制中,这样我们自然能够调试该子进程代码。
还是上面那个例子,RIM_Oracle_prog fork出子进程后将紧接着执行RIM_Oracle_Agent.9i的二进制代码文件。我们将该文件重命名为 RIM_Oracle_Agent.9i.binary,并新建一个名为RIM_Oracle_Agent.9i的shell脚本文件,其内容如下:
[root@tivf09 bin]# mv RIM_Oracle_Agent.9i RIM_Oracle_Agent.9i.binary [root@tivf09 bin]# cat RIM_Oracle_Agent.9i #!/bin/sh gdb RIM_Oracle_Agent.binary |
当fork的子进程执行名为RIM_Oracle_Agent.9i的文件时,gdb会被首先启动,使得要调试的代码处于gdb控制之下。
新的问题来了。子进程是在gdb的控制下了,但还是不能调试:如何与gdb交互呢?我们必须以某种方式启动gdb,以便能在某个窗口/终端与gdb交互。具体来说,可以使用xterm生成这个窗口。
xterm是X window系统下的模拟终端程序。比如我们在Linux桌面环境GNOME中敲入xterm命令:
就会跳出一个终端窗口:
如果你是在一台远程linux服务器上调试,那么可以使用VNC(Virtual Network Computing) viewer从本地机器连接到服务器上使用xterm。在此之前,需要在你的本地机器上安装VNC viewer,在服务器上安装并启动VNC server。大多数linux发行版都预装了vnc-server软件包,所以我们可以直接运行vncserver命令。注意,第一次运行 vncserver时会提示输入密码,用作VNC viewer从客户端连接时的密码。可以在VNC server机器上使用vncpasswd命令修改密码。
[root@tivf09 root]# vncserver New 'tivf09:1 (root)' desktop is tivf09:1 Starting applications specified in /root/.vnc/xstartup Log file is /root/.vnc/tivf09:1.log [root@tivf09 root]# [root@tivf09 root]# ps -ef|grep -i vnc root 19609 1 0 Jun05 ? 00:08:46 Xvnc :1 -desktop tivf09:1 (root) -httpd /usr/share/vnc/classes -auth /root/.Xauthority -geometry 1024x768 -depth 16 -rfbwait 30000 -rfbauth /root/.vnc/passwd -rfbport 5901 -pn root 19627 1 0 Jun05 ? 00:00:00 vncconfig -iconic root 12714 10599 0 01:23 pts/0 00:00:00 grep -i vnc [root@tivf09 root]# |
Vncserver是一个Perl脚本,用来启动Xvnc(X VNC server)。X client应用,比如xterm,VNC viewer都是和它通信的。如上所示,我们可以使用的DISPLAY值为tivf09:1。现在就可以从本地机器使用VNC viewer连接过去:
输入密码:
登录成功,界面和服务器本地桌面上一样:
下面我们来修改RIM_Oracle_Agent.9i脚本,使它看起来像下面这样:
#!/bin/sh export DISPLAY=tivf09:1.0; xterm -e gdb RIM_Oracle_Agent.binary |
如果你的程序在exec的时候还传入了参数,可以改成:
#!/bin/sh export DISPLAY=tivf09:1.0; xterm -e gdb --args RIM_Oracle_Agent.binary $@ |
最后加上执行权限
[root@tivf09 bin]# chmod 755 RIM_Oracle_Agent.9i |
现在就可以调试了。运行启动子进程的程序:
[root@tivf09 root]# wrimtest -l 9i_linux Resource Type : RIM Resource Label : 9i_linux Host Name : tivf09 User Name : mdstatus Vendor : Oracle Database : rim Database Home : /data/oracle9i/920 Server ID : rim Instance Home : Instance Name : Opening Regular Session... |
程序停住了。从VNC viewer中可以看到,一个新的gdb xterm窗口在服务器端打开了
[root@tivf09 root]# ps -ef|grep gdb nobody 24312 24311 0 04:30 ? 00:00:00 xterm -e gdb RIM_Oracle_Agent.binary nobody 24314 24312 0 04:30 pts/2 00:00:00 gdb RIM_Oracle_Agent.binary root 24326 10599 0 04:30 pts/0 00:00:00 grep gdb |
运行的正是要调试的程序。设置好断点,开始调试吧!
注意,下面的错误一般是权限的问题,使用 xhost 命令来修改权限:
[root@tivf09 bin]# export DISPLAY=tivf09:1.0 [root@tivf09 bin]# xhost + access control disabled, clients can connect from any host |
xhost + 禁止了访问控制,从任何机器都可以连接过来。考虑到安全问题,你也可以使用xhost + <你的机器名>。
小结
上述三种方法各有特点和优劣,因此适应于不同的场合和环境:
参考资料
关于作者
田强,中国软件开发中心 Tivoli 部门软件工程师,负责 IBM 产品TMF(Tivoli Management Framework)的维护和客户支持工作,热爱 Linux。
来自:http://hi.baidu.com/thinke365/blog/item/c9469f250b9aeb398644f948.html
Breakpoint 2 at 0x804b6f3: file collect.c, line 1172.
(gdb) n
[New process 28538]
[Switching to process 28538]
1174 if (!child)
// 现在gdb已经进入fork调试了,已经跳到子进程了。。。
需要对gdb进行设置:
(gdb) set follow-fork-mode
Requires an argument. Valid arguments are child, parent.
(gdb) set follow-fork-mode
child parent
(gdb) set follow-fork-mode child
子进程进入1174了,也就是要连接ftp了。。。
1174 if (!child)
(gdb) l
1169 fgets (Line, MAX, hostListRes);
1170 if (feof (hostListRes))
1171 break;
1172 while ((child = fork ()) == -1)
1173 sleep (1);
1174 if (!child)
1175 {
1176 // strcpy(machine, Line);
1177 scanline (Line);
1178 Probe (LZODir, TypeOfFetch);
(gdb) bt
#0 main (n=1, p=0xbf8072d4) at collect.c:1174
(gdb) n
1177 scanline (Line);
开始调试到ftp连接函数内部了。。。。
(gdb) br scanline
Breakpoint 3 at 0x804b1e5: file collect.c, line 1062.
(gdb) n
Breakpoint 3, scanline (line=0x804ca80 "") at collect.c:1062
1062 site.ftp_name[0]='/0';
查看一个结构体内部的值:
(gdb) p site
$1 = {ftp_user = '/0'
ftp_port = "/000/000/000/000/000/000/000/000/000"}
查看line的值,是从文件中读出来的。
(gdb) n
1068 if ((scan = strchr (line, LF)) != NULL)
(gdb) p line
$4 = 0x804ca80 ""
scan = line + 6; // 把ftp://前缀去掉了。。。
拷贝字段,是匿名字符。。。。
1115 strcpy (site.ftp_user, ANONYMOUS);
1116 strcpy (site.ftp_pass, ANONYPASS);
scanline出来后,要连接ftp了?
Probe (LZODir, TypeOfFetch);
进入Probe函数。
(gdb) n
Breakpoint 4, Probe (outputdir=0xbf806e2b "/var/parker/LZOData", TypeOfFetch=32 ' ') at collect.c:961
961 FILE *res = NULL;
(gdb) l
956 /* else call DolslR or DoLookup*/
957 int
958 Probe (char *outputdir, char TypeOfFetch)
959 {
960 int rc; /* return code */
961 FILE *res = NULL;
962 char tempfile[MAX];
963 char resfile[MAX];
964 char command[MAX];
965 char TopDir[MAX]; /* the ftp top dir */
从这句话开始连接FTP服务器了。。。。
973 if (Connect (machine) != S_OK)
974 {
975 DEB ("Couldn't connect");
976 return (S_ERROR);
977 }
220-
220-
220
331 Please specify the password.
230 Login successful.
--- Logged in
978 strcpy (TopDir, "/");
连接ftp成功后,开始执行下面的代码了。。。。(gdb) l
973 if (Connect (machine) != S_OK)
974 {
975 DEB ("Couldn't connect");
976 return (S_ERROR);
977 }
978 strcpy (TopDir, "/");
979 if (TypeOfFetch == 'd')
980 {
981 rc = DoDownload (resfile, TopDir);
982 }
(gdb) p TopDir
$15 = '/0'
目录切换成功。。。。
994 if ((rc = DoLISTlR (resfile)) != S_OK
(gdb) l
989 res = fopen (resfile, "w");
990 rc = DoRecursive (TopDir, res);
991 fclose (res);
992 }
993 else
994 if ((rc = DoLISTlR (resfile)) != S_OK
995 && ((rc = DoDownload (resfile, TopDir)) != S_OK))
996 {
997 DEB ("use Recursive Look up");
998 res = fopen (resfile, "w");
(gdb) n
250 Directory successfully changed.
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
--- Done with fetching the directory
226 Directory send OK.
--- Done with List-lR
1003 if (rc != S_OK)
1024命令执行出错???
1024 sprintf (command, "%s/%s/%s %s %s/%s -s", PARKER_HOME, BINDIR,
(gdb)
1027 system (command);
(gdb)
[New process 28551]
creat output file error!
Program exited with code 03.
(gdb) --- Done with Probe
这里执行system命令,出错
(gdb) n
1027 system (command);
(gdb) p command
$18 = "/var/parker/bin/lzo_comp /var/parker/tmp/10.0.0.1.fmt /var/parker/LZOData/10.0.0.1 -s", '/0'
缺少一个文件夹,是因为这个原因导致的错误吗?
这个命令的具体拼接过程?
调试到connect代码了,这个代码在系统库中,而不是用户代码?
973 if (Connect (machine) != S_OK)
(gdb)
Breakpoint 4, 0x00cd3ad0 in connect () from /lib/libc.so.6
(gdb) l
968 struct filestatist files;
969 GetHostipStr (machine, HostIP);
970 sprintf (resfile, "%s/%s.org", WorkDir, HostIP);
971 DEB ("Probe");
972 alarm (TIMEOUT);
973 if (Connect (machine) != S_OK)
974 {
975 DEB ("Couldn't connect");
976 return (S_ERROR);
977 }
(gdb)
调试进connect代码了。。。Single stepping until exit from function connect,
which has no line number information.
WaitCon (m=0x804d3c0 "10.0.0.1") at collect.c:256
256 if ((a = getsockname (h, (struct sockaddr *) &own_addr, &own_addr_len)))
(gdb) l
251 if (connect (h, (void *) &sa, sizeof (sa)) < 0)
252 {
253 perror ("connect");
254 return (S_ERROR);
255 }
256 if ((a = getsockname (h, (struct sockaddr *) &own_addr, &own_addr_len)))
257 {
258 perror ("getsockname");
259 printf ("error here!return %i/n", a);
260 return (S_ERROR);
显示某个函数的代码:
(gdb) l Probe
954 /* TypeOfFetch=r call DoRec */
955 /* TypeOfFetch=l call DoLookup */
956 /* else call DolslR or DoLookup*/
957 int
958 Probe (char *outputdir, char TypeOfFetch)
959 {
960 int rc; /* return code */
961 FILE *res = NULL;
962 char tempfile[MAX];
963 char resfile[MAX];
显示某代码行附近的代码:
(gdb) l 111
106 printf ("%s", Line);
107
108 #endif /* */
109 if (Line[3] == '-')
110 continue;
111 if (!strncmp (Line, PWD, strlen (PWD)))
112 {
113 ptr = Line;
114 while (ptr++)
115 if (*ptr == '/"')