关于gdb调试多进程很早之前就看过,但一直没有总结很深入的学习,今天又碰到这个问题了,记录下
记得kk跟我说过,在开发程序的时候尽量少调试,尽量少使用gdb等调试工具,尽量多的使用log等方式来定位错误,这样会大大减少调试所需的时间和精力,同时也能快速的定位问题。当然,利用gdb调试还是有很多的优势的,比如对于我这个菜鸟来说,可以好好学习下强大的gdb~
关于gdb的基本调试,基本已经掌握了,经常会使用的一些命令有:
s----step
n----next
c-----continue
p----print
bt---backtrace
b----break
等等
今天查找多进程调试的时候突然注意到stop命令,之前一直没有留意,发现这个命令可以在多进程调试中起到作用
下面描述下多进程的调试
关于gdb调试多进程,很经典的一篇文章是IBM developerworks上面的一篇:
http://www.ibm.com/developerworks/cn/linux/l-cn-gdbmp/index.html
详细内容见下面描述。
IBM developerworks上的这篇文章介绍了三种gdb调试多进程的方法:
1、follow-fork-mode方法
这是一种比较简单的方式,通过set follow-fork-mode的方式来调试父进程或子进程
2、attach的方式,这种应该是比较常用的方式,也是很多文章中介绍的一种方式。这种方式一般需要增加一些代码来实现对进程的attach
3、GDB wrapper方法:专用于fork+exec模式,不用添加额外代码,但需要X环境支持(xterm/VNC)这种方式没有使用过,一般对于企业开发的话是不是很多都没有x环境支持的吧(猜测:))
follow-fork-mode方式比较简单,通过set follow-fork-mode child | parent来觉得跟踪child还是parent,然后可以设置断点跟踪了
attach方法首先要找到待调试进程的pid,一般是通过ps相关命令,例如pstree、ps –aux | grep 进程名称等等,然后通过attach 关联到相应的进程。
下面分别是一些介绍文章:
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 方法。
在2.5.60版Linux内核及以后,GDB对使用fork/vfork创建子进程的程序提供了follow-fork-mode选项来支持多进程调试。
follow-fork-mode的用法为:
set follow-fork-mode [parent|child]
- parent: fork之后继续调试父进程,子进程不受影响。
- child: fork之后调试子进程,父进程不受影响。
因此如果需要调试子进程,在启动gdb后:
并在子进程代码设置断点。
此外还有detach-on-fork参数,指示GDB在fork之后是否断开(detach)某个进程的调试,或者都交由GDB控制:
set detach-on-fork [on|off]
- on: 断开调试follow-fork-mode指定的进程。
- off: gdb将控制父进程和子进程。follow-fork-mode指定的进程将被调试,另一个进程置于暂停(suspended)状态。
注意,最好使用GDB 6.6或以上版本,如果你使用的是GDB6.4,就只有follow-fork-mode模式。
follow-fork-mode/detach-on-fork的使用还是比较简单的,但由于其系统内核/gdb版本限制,我们只能在符合要求的系统上才能使用。而且,由于follow-fork-mode的调试必然是从父进程开始的,对于fork多次,以至于出现孙进程或曾孙进程的系统,例如上图3进程系统,调试起来并不方便。
众所周知,GDB有附着(attach)到正在运行的进程的功能,即attach <pid>命令。因此我们可以利用该命令attach到子进程然后进行调试。
例如我们要调试某个进程RIM_Oracle_Agent.9i,首先得到该进程的pid
通过pstree可以看到,这是一个三进程系统,oserv是RIM_Oracle_prog的父进程,RIM_Oracle_prog又是RIM_Oracle_Agent.9i的父进程。
启动GDB,attach到该进程
现在就可以调试了。一个新的问题是,子进程一直在运行,attach上去后都不知道运行到哪里了。有没有办法解决呢?
一个办法是,在要调试的子进程初始代码中,比如main函数开始处,加入一段特殊代码,使子进程在某个条件成立时便循环睡眠等待,attach到进程后在该代码段后设上断点,再把成立的条件取消,使代码可以继续执行下去。
至于这段代码所采用的条件,看你的偏好了。比如我们可以检查一个指定的环境变量的值,或者检查一个特定的文件存不存在。以文件为例,其形式可以如下:
当attach到进程后,在该段代码之后设上断点,再把该文件删除就OK了。当然你也可以采用其他的条件或形式,只要这个条件可以设置/检测即可。
Attach进程方法还是很方便的,它能够应付各种各样复杂的进程系统,比如孙子/曾孙进程,比如守护进程(daemon process),唯一需要的就是加入一小段代码。
很多时候,父进程 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脚本文件,其内容如下:
当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命令修改密码。
Vncserver是一个Perl脚本,用来启动Xvnc(X VNC server)。X client应用,比如xterm,VNC viewer都是和它通信的。如上所示,我们可以使用的DISPLAY值为tivf09:1。现在就可以从本地机器使用VNC viewer连接过去:
输入密码:
登录成功,界面和服务器本地桌面上一样:
下面我们来修改RIM_Oracle_Agent.9i脚本,使它看起来像下面这样:
如果你的程序在exec的时候还传入了参数,可以改成:
最后加上执行权限
现在就可以调试了。运行启动子进程的程序:
程序停住了。从VNC viewer中可以看到,一个新的gdb xterm窗口在服务器端打开了
运行的正是要调试的程序。设置好断点,开始调试吧!
注意,下面的错误一般是权限的问题,使用 xhost 命令来修改权限:
xhost + 禁止了访问控制,从任何机器都可以连接过来。考虑到安全问题,你也可以使用xhost + <你的机器名>。
上述三种方法各有特点和优劣,因此适应于不同的场合和环境:
- follow-fork-mode方法:方便易用,对系统内核和GDB版本有限制,适合于较为简单的多进程系统
- attach子进程方法:灵活强大,但需要添加额外代码,适合于各种复杂情况,特别是守护进程
- GDB wrapper方法:专用于fork+exec模式,不用添加额外代码,但需要X环境支持(xterm/VNC)。
- GDB 官方参考资料:http://sourceware.org/gdb/documentation/
- 更多 VNC 信息:http://www.realvnc.com/
======================================================
http://hi.baidu.com/happyge/blog/item/6c69440f1ad5dd3e6059f302.html
碰到多进程的程序如何调试呢?默认情况下,你next下来,跟的路径都是主进程的,而你想跟的子进程路径没跑到?怎么办呢?
有几种方法,今天看了看attach方法,觉得不错,特记录如下:
原理:运行多进程程序,得到要跟的子进程的ID;然后用开gdb,用attach+ID,然后stop,为什么要stop?防止子进程自己跑完,所以要stop,然后可以设断点,观测点,什么的。设完后,可以step,下一步,向下跟。
主要原理是这样。下面举个例子:
#include<stdio.h>
int main()
{
if(fork() == 0)
{
int b = 9;
sleep(60);
int a =1;
int c = 90;
int d =5;
printf("child/n");
}
else
{
wait(NULL);
printf("parant/n");
}
return 0;
}
怎么样跟到子进程里面去 ?
1 后台运行该程序,可以得到进程ID
2 gdb下,attach+id
3 stop,然后设置断点,观察点等等
4 step
(gdb) attach 12606
Attaching to program: /home/purerain/test/f, process 12606
Symbols already loaded for /lib/tls/libc.so.6
Symbols already loaded for /lib/ld-linux.so.2
0xffffe002 in ?? ()
(gdb) stop
(gdb) b 12
Breakpoint 8 at 0x8048402: file fork.c, line 12.
(gdb) c
Continuing.
Breakpoint 7, main () at fork.c:11
11 int d =5;
(gdb) s
12 printf("child/n");
(gdb) s
19 return 0;
(gdb) s
20 }
(gdb) s
0x42015574 in __libc_start_main () from /lib/tls/libc.so.6
(gdb) s
Single stepping until exit from function __libc_start_main,
which has no line number information.
Program exited normally.
======================================================
使用GDB最好的文档就是其名为'Debugging with GDB' 的参考手册。手册中有一小章节提到了如何调试多进程程序。一般情况下,如果被gdb调试的程序中调用fork派生出一个新的子进程,这时gdb调试的仍然 还是父进程,其子进程的执行不被理会。如果之前你在子进程的执行routine上设置了断点,那么当子进程执行到那个断点时,子进程会因为收到一个 SIGTRAP信号而自行终止,除非你在子进程中拦截了该信号。
那么使用GDB该如何调试多进程程序呢?在其参考手册中提供了一种通用方法,这里说说(GDB在某些平台上如HP-UX,还提供了更简便的方法,不过不具备通用性,这里不说):
[测试程序]
我们先看看我们的测试程序:
/* in eg1.c */
int wib(int no1, int no2)
{
int result, diff;
diff = no1 - no2;
result = no1 / diff;
return result;
}
int main()
{
pid_t pid;
pid = fork();
if (pid <0) {
printf("fork err/n");
exit(-1);
} else if (pid == 0) {
/* in child process */
sleep(60); ------------------ (!)
int value = 10;
int div = 6;
int total = 0;
int i = 0;
int result = 0;
for (i = 0; i < 10; i++) {
result = wib(value, div);
total += result;
div++;
value--;
}
printf("%d wibed by %d equals %d/n", value, div, total);
exit(0);
} else {
/* in parent process */
sleep(4);
wait(-1);
exit(0);
}
}
该测试程序中子进程运行过程中会在wib函数中出现一个'除0'异常。现在我们就要调试该子进程。
[调试原理]
不 知道大家发现没有,在(!)处在我们的测试程序在父进程fork后,子进程调用sleep睡了60秒。这就是关键,这个sleep本来是不该存在于子进程 代码中的,而是而了使用GDB调试后加入的,它是我们调试的一个关键点。为什么要让子进程刚刚运行就开始sleep呢?因为我们要在子进程睡眠期间,利用 shell命令获取其process id,然后再利用gdb调试外部进程的方法attach到该process id上,调试该进程。
[调试过程]
我觉上面的调试原理的思路已经很清晰了,剩下的就是如何操作的问题了。我们来实践一次吧!
我所使用的环境是Solaris OS 9.0/GCC 3.2/GDB 6.1。
GDB 调试程序的前提条件就是你编译程序时必须加入调试符号信息,即使用'-g'编译选项。首先编译我们的源程序'gcc -g -o eg1 eg1.c'。编译好之后,我们就有了我们的调试目标eg1。由于我们在调试过程中需要多个工具配合,所以你最好多打开几个终端窗口,另外一点需要注意的 是最好在eg1的working directory下执行gdb程序,否则gdb回提示'No symbol table is loaded'。你还得手工load symbol table。好了,下面我们就'按部就班'的开始调试我们的eg1。
执行eg1:
eg1 & --- 让eg1后台运行吧。
查找进程id:
ps -fu YOUR_USER_NAME
运行gdb:
gdb
(gdb) attach xxxxx --- xxxxx为利用ps命令获得的子进程process id
(gdb) stop --- 这点很重要,你需要先暂停那个子进程,然后设置一些断点和一些Watch
(gdb) break 37 -- 在result = wib(value, div);这行设置一个断点,可以使用list命令察看源代码
Breakpoint 1 at 0x10808: file eg1.c, line 37.
(gdb) continue
Continuing.
Breakpoint 1, main () at eg1.c:37
37 result = wib(value, div);
(gdb) step
wib (no1=10, no2=6) at eg1.c:13
13 diff = no1 - no2;
(gdb) continue
Continuing.
Breakpoint 1, main () at eg1.c:37
37 result = wib(value, div);
(gdb) step
wib (no1=9, no2=7) at eg1.c:13
13 diff = no1 - no2;
(gdb) continue
Continuing.
Breakpoint 1, main () at eg1.c:37
37 result = wib(value, div);
(gdb) step
wib (no1=8, no2=8) at eg1.c:13
13 diff = no1 - no2;
(gdb) next
14 result = no1 / diff;
(gdb) print diff
$6 = 0 ------- 除数为0,我们找到罪魁祸首了。
(gdb) next
Program received signal SIGFPE, Arithmetic exception.
0xff29d830 in .div () from /usr/lib/libc.so.1
至此,我们调试完毕。
======================================================
总结来看,follow-fork-mode是最方面的,但是作用有限;attach要修改下源代码,但也不是很麻烦
有空还得研究下gdb的手册~