gdb动态库调试分析文档

来源:互联网 发布:奶酪陷阱知乎 编辑:程序博客网 时间:2024/09/21 11:23

1.概述

    本文通过分析linux本地动态库程序调试中,在动态库设置断点的过程,理清调试器在动态库调试的机制。

2. 原理分析

    需要指出,调试器在动态断点的设定时,与普通程序断点设定的唯一区别就是,该断点所在的文件、断点对应的进程虚拟地址都是不确定的,所以,在设定动态库断点的时候,如果在调试器内部加载的符号中没有该函数名,那么调试器会给出如下提示,用户选择y,则设定动态断点成功,见图。

    注意,此时断点的属性是pending,而不是对应的文件名、行号、地址;接下来调试器执行run命令,由此开始,直到程序遇到child_fun断点的过程中涉及到图4所述的模块。调试器进程与动态程序进程之间借助于内核的信号机制进行通信(这里使用SIGTRAP信号),动态库程序在内核创建好进程之后,整个程序的入口地址是动态链接器的起始地址_start()函数处。

    下面介绍一下gdb动态库调试的框架图。

2.1 动态链接器部分分析

为了更好的理解这个过程,这里需要对动态链接器的动态库装载过程做简要流程分析。

在动态链接器中,有一个关于动态调试的重要数据结构,定义在uClibc-0.9.33.2/include/link.h中,该结构是动态链接器与调试器之间关于动态库加载信息的通信标准。

struct r_debug {

              intr_version;

              structlink_map *r_map;

              ElfW(Addr) r_brk;

              enum{

                             RT_CONSISTENT,

                             RT_ADD,

                             RT_DELETE

}r_state;

Efl(Addr) r_ldbase;

};

r_version:通信协议的版本号;

r_map:指向描述加载模块的链表;

struct link_map结构

struct {

              Elf(Addr)  l_addr;

              char*l_name;

              Elf(Dyn)*l_ld;

              strructlink_map *l_next, *l_prev;

              …… // tls相关定义,这里不关注。

};

l_addr:动态库加载基址。

l_name:动态库的全名(包含绝对路径)。

l_ld:动态库的dynamic段。

l_next,l_prev:链表连接件。

r_brk:该地址是动态链接器内部函数_dl_debug_state()所在的地址。

该函数作为调试器与动态链接器进行通信的手段而存在。该函数会在动态链接器开始模块装载、开始模块卸载、完成模块装载、完成模块卸载的时候都会被调用到。调试器可以在该地址设置断点,然后在根据通信协议的语义进行相关操作。

r_state:表征当r_brk地址被调用到的时候,动态链接器所处的加载模块的状态:

RT_CONSISTENT表示完成状态(与RT_ADD、RT_DELETE部分语义重复)。

RT_ADD表示完成加载模块。

RT_DELETE表示完成卸载模块。

r_ldbase:动态链接器本身的加载基址。

struct r_debug数据结构在动态链接器装载动态库程序的时候malloc出来,然后根据实际加载情况去由动态链接器本身填充,之后会将该结构的起始地址赋值给应用程序中数据段中相应的域。

在动态链接的应用程序elf文件中,编译时生成的dynamic段中都会有个特殊的tag:DT_DEBUG.该tag在编译的时候初始值为零,在内核装载该程序的时候,会将该域装载至应用程序镜像中,然后该值最终由动态链接器在完成通信协议要求的全部内容后,将指向struct r_debug结构的指针填在该处。下文会详细介绍调试器对该地址的使用过程。

在了解以上和数据结构之后,下面可以简要分析动态链接器加载流程中对该数据结构的填充过程了。动态链接器的加载过程非常复杂,这里只介绍与本文相关的流程,其它流程这里不关注。

有个重要函数,void *_dl_get_ready_to_run ()函数。该函数在动态链接器的启动代码函数中调用,在动态链接器完成自举后,便调用该函数,在调用完该函数之后,动态链接器便着手准备将程序转向应用程序本身。由此可见,_dl_get_ready_to_run()函数需要完成程序所需全部动态模块的加载、struct r_debug结构的填充、应用程序dynamic段中DT_DEBUG的tag的填充。

函数_dl_debug_state()调用时机。在整个动态链接器中,对该函数的调用一共有四处:

1._dl_get_ready_to_run()函数中(1371行)。在完成全部动态模块加载、填写完成struct r_debug结构、赋值完DT_DEBUG的tag之后。

2._dl_get_ready_to_run()函数中(1422行)。紧接1处调用后,完成应用程序的.init段的初始化代码之后。

3.static void *do_dlopen()函数中。该函数被libdl.so模块中的dl_open接口函数调用,在完成指定模块的加载之后,调用该函数。

4.static void *do_dlclose()函数中。该函数被libdl.so模块中的dl_close接口函数调用,在完成指定模块的卸载之后,调用该函数。

_dl_get_ready_to_run()函数首先malloc出struct r_debug结构,然后调用_dl_parse_dynamic_info()函数将该指针赋值给应用程序的DT_DEBUG域中,然后随着模块的加载,更新struct r_debug结构中r_map执行的链表结构,最后调用_dl_debug_state()函数,与调试器通信,此时,应用程序中的DT_DEBUG域中的指针指向的structr_debug结构中记载了本次动态链接器加载的全部动态模块的信息。整个一个动态库加载流程如图所示。


2.2 gdb部分

首先,调试器在_dl_debug_state()函数出设定断点,运行程序,wait后判断是否停止在_dl_debug_state()函数处。若停在该函数处,则根据调试器与动态链接器之间的通信协议,读取应用程序DT_DEBUG域中指向的structr_debug结构,加载r_map中对应的全部模块的符号,此时,调试器中的符号表应该包含了应用程序本身、应用程序所需的全部动态库的符号,接着,调试器更新其内部断点链表,重新确定属性为pending的断点对应的文件名,虚拟地址,设定好该断点,继续运行程序。流程如下图:


    下面关键问题就是,gdb是怎么准确的在_dl_debug_state()函数设置断点呢。该函数是动态链接库中的一个符号。那么gdb得首先找到被调试动态链接程序使用的动态链接器。在elf文件的.interp段中会记录动态链接器。然后通过扫描动态链接器的符号表就能找到_dl_debug_state()符号的地址。但是这个是个相对地址,我们要在正确的地方设置断点,那么就需要找到该符号的绝对地址。动态链接器采用基址重置的方式进行重定位。

    内核在启动进程的时候会在进程的栈上传递参数和环境变量等,其中就有一个auxiliary数据结构,里面有一个AT_BASE数据,指向的就是动态链接器的加载基址,那么gdb通过分析进程的栈上的数据就可以获取动态链接器的加载基址。那么基址+相对地址=绝对地址。gdb实现在_dl_debug_state()函数上设置断点了。

0 0
原创粉丝点击