标签 GDB 下的文章

用GDB调试多进程程序

有一段时间没有写技术方面的东西了^_^。众所周知,GDB是Unix/Linux下调试程序的龙头老大,GDB功能强大,我们在平时多使用其一些最基本的功能,而且一般调试的都是单进程的程序。最近一个项目中的问题让我接触如何使用GDB调试多进程程序,更确切的是说调试调用fork的多进程程序。

使用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 0×10808: 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

至此,我们调试完毕。

上面仅仅是一个简单的多进程程序,在我们平时开发的多进程程序远远比这个复杂,但是调试基本原理是不变,有一些技巧则需要我们在实践中慢慢摸索。

汇编之路-复习栈操作

不得不承认上次关于栈桢和栈操作写得有些笼统,这里做一次“补充”,美名其曰:“复习”。

下面的这个例子几乎就能覆盖所有的栈操作相关的内容了。
void dummy()
{
        int     i = 12;
        int     j = 13;
        char    c = 'a';
}

int main()
{
        dummy();
        return 0;
}

下面是利用MDB(注[1])反汇编的代码:
> main::dis
main:                           pushl   %ebp
main+1:                         movl    %esp,%ebp
main+3:                         subl    $8,%esp
main+6:                         andl    $0xf0,%esp
main+9:                         movl    $0,%eax
main+0xe:                       subl    %eax,%esp
main+0×10:                      call    -0x2a          
main+0×15:                      movl    $0,%eax
main+0x1a:                      leave
main+0x1b:                      ret

> dummy::dis
dummy:                          pushl   %ebp
dummy+1:                        movl    %esp,%ebp
dummy+3:                        subl    $0xc,%esp
dummy+6:                        movl    $0xc,-4(%ebp)
dummy+0xd:                      movl    $0xd,-8(%ebp)
dummy+0×14:                     movb    $0×61,-9(%ebp)
dummy+0×18:                     leave
dummy+0×19:                     ret

分析上面的汇编代码我们要解决如下几个方面问题:
1、过程调用的标准模式
我们知道发生过程调用的指令是call,那么call做了些什么呢?上面每个过程的最后都有leave指令,它又作了什么呢?我们不妨来跟踪一个栈帧的形成过程,分析后自然会有答案。

(1) 我们从main + 0×10处开始,这里是一个call指令,此时的活动栈帧为main的栈帧,dummy栈帧尚未形成:
+          + 0xffffffff
|          |
+———-+
|          | main的返回地址,属于main的调用者栈帧范畴
+———-+ —————————
|    A     | main栈帧栈底 <– %ebp
+———-+
|    B     |
+———-+
|    C     | main栈帧栈顶 <– %esp
+———-+
|          |
+          + 0×00000000

(2) 调用call指令后,未执行dummy前,此时main的栈帧已经结束,%eip中存放dummy起始指令地址准备执行。
+          + 0xffffffff
|          |
+———-+
|          | main的返回地址,属于main的调用者栈帧范畴
+———-+ —————————
|    A     | main栈帧栈底 <— %ebp
+———-+
|    B     |
+———-+
|    C     |
+———-+
|          | dummy的返回地址, main栈帧栈顶 <– %esp
+———-+ —————————
|          |
+          + 0×00000000
可见call首先将main调用的函数(这里是dummy)的返回地址pushl到栈中,形成main栈帧的最后一个部分,然后跳到dummy的起始处。所以call等价于下面两条指令:
pushl %eip  //将下一条指令地址压入栈中
jmp dummy

(3) 形成dummy栈帧
dummy首先将main的栈底保存起来,然后创建自己的栈底。
+          + 0xffffffff
|          |
+———-+
|          | dummy的返回地址,属于main的栈帧范畴
+———-+ —————————
|    D     | dummy栈帧栈底 <– %ebp,存储着main栈帧栈底
+———-+
|    E     |
+———-+
|    F     | dummy栈帧栈顶 <– %esp
+———-+ —————————
|          |
+          + 0×00000000

(4) dummy返回
dummy返回时调用的第一条指令leave,该指令相当于如下两条指令:
指令1: movl %ebp %esp // 将%esp置到dummy栈桢首部

该指令执行后状态如下:
+          + 0xffffffff
|          |
+———-+
|          | dummy的返回地址,属于main的栈帧范畴
+———-+ —————————
|    D     | dummy栈帧栈底 <– %esp <– %ebp
+———-+
|    E     |
+———-+
|    F     | dummy栈帧栈顶
+———-+ —————————
|          |
+          + 0×00000000

指令2:popl %ebp
该指令执行后状态如下:
+          + 0xffffffff
|          |
+———-+
|          | main的返回地址,属于main的调用者栈帧范畴
+———-+ —————————-
|    A     | main栈帧栈底 <— %ebp
+———-+
|    B     |
+———-+
|    C     |
+———-+
|          | dummy的返回地址,main栈帧栈顶 <– %esp
+———-+ —————————
|    D     | dummy栈帧栈底
+———-+
|    E     |
+———-+
|    F     | dummy栈帧栈顶
+———-+ —————————
|          |
+          + 0×00000000

dummy返回时调用的第二条指令ret,该指令相当于popl %eip,执行完内存栈的情况如下:
+          + 0xffffffff
|          |
+———-+
|          | main的返回地址,属于main的调用者栈帧范畴
+———-+ —————————-
|    A     | main栈帧栈底 <— %ebp
+———-+
|    B     |
+———-+
|    C     | <– %esp main栈帧栈顶
+———-+
|          | dummy的返回地址
+———-+ —————————
|    D     | dummy栈帧栈底
+———-+
|    E     |
+———-+
|    F     | dummy栈帧栈顶
+———-+ —————————
|          |
+          + 0×00000000

至此,main的栈桢又再次被恢复了。

经过上面分析,得出过程调用标准模式如下:
pushl %ebp
movl %esp %ebp

//过程体

leave
ret
其中ret和call对应,而leave则和最开始的那两句对应。

2、访问局部变量
在dummy的汇编码中我们可以清晰的看到对三个局部变量i,j,c的赋值语句:
movl    $0xc,-4(%ebp)
movl    $0xd,-8(%ebp)
movb    $0×61,-9(%ebp)
其三者有一个共同点就是“都是通过对%ebp的偏移来访问局部变量的”。

3、局部变量的分配
两个以上的局部变量的栈上分配涉及到栈内存的对齐问题,dummy的代码足以说明问题。我们在dummy的栈桢中分配了两个整型和一个char型变量,实际需要9个字节。那我们来看看汇编是否给我们只分配了9个字节呢?
movl    %esp,%ebp
subl    $0xc,%esp
movl    $0xc,-4(%ebp)

可以看出subl $0xc,%esp一句在内存栈上为我们留出12个字节的空间,在char c的后面又多分了3个字节,以保证对后面的变量的地址访问是对齐的。

4、对异构类型变量的分配和访问
举例如下:
struct test_t {
        int i;
        int j;
        int a[3];
};

void dummy()
{
        struct test_t t;
        t.i = 11;
        t.j = 12;
        t.a[0] = 'a';
        t.a[1] = 'b';
        t.a[2] = 'c';
}

int main()
{
        dummy();
        return 0;
}

> dummy::dis
dummy:                          pushl   %ebp
dummy+1:                        movl    %esp,%ebp
dummy+3:                        subl    $0×28,%esp
dummy+6:                        movl    $0xb,-0×28(%ebp)
dummy+0xd:                      movl    $0xc,-0×24(%ebp)
dummy+0×14:                     movl    $0×61,-0×20(%ebp)
dummy+0x1b:                     movl    $0×62,-0x1c(%ebp)
dummy+0×22:                     movl    $0×63,-0×18(%ebp)
dummy+0×29:                     leave
dummy+0x2a:                     ret

与上面的例子不同的是这次为了存储一个test_t类型结构,栈居然留出了0×28(40d)大小的空间,在t.a[2]与%ebp之间留了0×14(20)个字节空闲。这里的原因不得而知。如果是为了对齐,那么这个代价着实不小。

[注1]
在X86平台的Solaris9上,GDB反汇编使用的语法与我们的稍有差异,而使用Solaris自带的MDB(The Modular Debugger)则和我们的汇编语法保持一致。顺便说一句MDB是一个强大的调试工具,在Sun公司的网站上有其详细的使用说明。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats