代码调试篇(5):如何调试动态链接器
Author: stormQ
Created: Monday, 08. February 2021 12:54PM
Last Modified: Sunday, 14. February 2021 10:51AM
本文描述了如何调试运行在normal
和trace
两种模式下的动态链接器的调试方法。掌握后,一方面有助于分析 glibc 是如何实现共享库的加载、符合解析、重定位等过程的,另一方面有助于分析诸如ldd -u
命令的实现原理。
本节我们从一个简单的程序——hello
开始,研究如何在可执行目标文件运行前将断点设置到动态链接器中,并说明要调试动态链接器源码所需要做的准备工作。
研究过程:
step 1: 准备
1) 准备示例程序,并生成可执行目标文件
示例程序,hello.cpp:
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
生成可执行目标文件——hello:
$ g++ -o hello hello.cpp
2) 做好调试 glibc 源码所需要的准备
详细步骤见上一篇文章:《代码调试篇(4):如何调试 glibc 源码——准备篇》。
step 2: 启动动态链接器
1) 使用 gdb 启动动态链接器
$ gdb -q /lib64/ld-linux-x86-64.so.2
Reading symbols from /lib64/ld-linux-x86-64.so.2...
(No debugging symbols found in /lib64/ld-linux-x86-64.so.2)
注:/lib64/ld-linux-x86-64.so.2
是一个软链接。在笔者的机器上,实际指向/usr/lib64/ld-linux-x86-64.so.2
。
由于系统中自带的 ld.so 删掉了符号表等调试相关的信息。因此,在用 gdb 调试时,需要我们将其符号表加载进去。否则,无法击中在_dl_start
函数处设置的断点。
2) 加载符号表
(gdb) symbol-file /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.31.so
Reading symbols from /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.31.so...
3) 设置源码搜索路径
(gdb) directory /home/glibc-2.31/elf/
Source directories searched: /home/glibc-2.31/elf:$cdir:$cwd
注:/home/glibc-2.31
目录存放着我们安装解压后的 glibc 源码,而子目录elf
中存放着动态链接器的源码实现。
4) 用动态链接器启动 hello
(gdb) starti ./hello
Starting program: /usr/lib64/ld-linux-x86-64.so.2 ./hello
Program stopped.
0x00007ffff7fd0100 in _start ()
注意: 这里必须用starti
而不是start
命令。如果使用后者,会直接运行并结束本次调试。
上面输出结果中的_start
函数就是动态链接器的入口。
到这里,我们成功地将断点设置在了动态链接器的入口处。
5) 反汇编 _start 函数(optional)
(gdb) disassemble _start
Dump of assembler code for function _start:
=> 0x00007ffff7fd0100 <+0>: mov %rsp,%rdi
0x00007ffff7fd0103 <+3>: callq 0x7ffff7fd0df0 <_dl_start>
End of assembler dump.
从上面的输出结果中可以看出,_start
函数实际调用了_dl_start
函数。从而,开始了动态链接器的运作过程。
6) 查看 _start 函数的源码(optional)
_start
函数定义在RTLD_START
宏中(dl-machine.h),如下:
/* Initial entry point code for the dynamic linker.
The C function `_dl_start' is the real entry point;
its return value is the user program's entry point. */
#define RTLD_START asm ("\n\
.text\n\
.align 16\n\
.globl _start\n\
.globl _dl_start_user\n\
_start:\n\
movq %rsp, %rdi\n\
call _dl_start\n\
_dl_start_user:\n\
# Save the user entry point address in %r12.\n\
movq %rax, %r12\n\
# See if we were run as a command with the executable file\n\
# name as an extra leading argument.\n\
movl _dl_skip_args(%rip), %eax\n\
# Pop the original argument count.\n\
popq %rdx\n\
# Adjust the stack pointer to skip _dl_skip_args words.\n\
leaq (%rsp,%rax,8), %rsp\n\
# Subtract _dl_skip_args from argc.\n\
subl %eax, %edx\n\
# Push argc back on the stack.\n\
pushq %rdx\n\
# Call _dl_init (struct link_map *main_map, int argc, char **argv, char **env)\n\
# argc -> rsi\n\
movq %rdx, %rsi\n\
# Save %rsp value in %r13.\n\
movq %rsp, %r13\n\
# And align stack for the _dl_init call. \n\
andq $-16, %rsp\n\
# _dl_loaded -> rdi\n\
movq _rtld_local(%rip), %rdi\n\
# env -> rcx\n\
leaq 16(%r13,%rdx,8), %rcx\n\
# argv -> rdx\n\
leaq 8(%r13), %rdx\n\
# Clear %rbp to mark outermost frame obviously even for constructors.\n\
xorl %ebp, %ebp\n\
# Call the function to run the initializers.\n\
call _dl_init\n\
# Pass our finalizer function to the user in %rdx, as per ELF ABI.\n\
leaq _dl_fini(%rip), %rdx\n\
# And make sure %rsp points to argc stored on the stack.\n\
movq %r13, %rsp\n\
# Jump to the user's entry point.\n\
jmp *%r12\n\
.previous\n\
");
而上面这段汇编代码正是动态链接器/usr/lib64/ld-linux-x86-64.so.2
中的.text
节的开始部分,如下:
/lib64/ld-linux-x86-64.so.2: file format elf64-x86-64
Disassembly of section .text:
0000000000001100 <_dl_rtld_di_serinfo@@GLIBC_PRIVATE-0x9f90>:
1100: 48 89 e7 mov %rsp,%rdi
1103: e8 e8 0c 00 00 callq 1df0 <_dl_catch_error@plt+0xd00>
1108: 49 89 c4 mov %rax,%r12
110b: 8b 05 e7 c4 02 00 mov 0x2c4e7(%rip),%eax # 2d5f8 <_dl_catch_error@@GLIBC_PRIVATE+0xf8c8>
1111: 5a pop %rdx
1112: 48 8d 24 c4 lea (%rsp,%rax,8),%rsp
1116: 29 c2 sub %eax,%edx
1118: 52 push %rdx
1119: 48 89 d6 mov %rdx,%rsi
111c: 49 89 e5 mov %rsp,%r13
111f: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
1123: 48 8b 3d 36 cf 02 00 mov 0x2cf36(%rip),%rdi # 2e060 <_rtld_global@@GLIBC_PRIVATE>
112a: 49 8d 4c d5 10 lea 0x10(%r13,%rdx,8),%rcx
112f: 49 8d 55 08 lea 0x8(%r13),%rdx
1133: 31 ed xor %ebp,%ebp
1135: e8 d6 0a 01 00 callq 11c10 <_dl_rtld_di_serinfo@@GLIBC_PRIVATE+0x6b80>
113a: 48 8d 15 0f 0c 01 00 lea 0x10c0f(%rip),%rdx # 11d50 <_dl_rtld_di_serinfo@@GLIBC_PRIVATE+0x6cc0>
1141: 4c 89 ec mov %r13,%rsp
1144: 41 ff e4 jmpq *%r12
1147: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
这意味着,/usr/lib64/ld-linux-x86-64.so.2
中.text
节的第一条指令即为动态链接器的入口。我们可以查看动态链接器的头部信息,加以佐证,如下:
$ readelf -h /lib64/ld-linux-x86-64.so.2
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1100
Start of program headers: 64 (bytes into file)
Start of section headers: 189680 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 11
Size of section headers: 64 (bytes)
Number of section headers: 28
Section header string table index: 27
从上面的输出结果可以看出,Entry point address
(动态链接器的入口地址)的值确实是0x1100
(.text
节的第一条指令的地址)。
7) 在 _dl_start 函数开始处设置断点,并继续执行
(gdb) b _dl_start
Breakpoint 1 at 0x7ffff7fd0df0: file rtld.c, line 463.
(gdb) c
Continuing.
Breakpoint 1, _dl_start (arg=0x7fffffffdea0) at rtld.c:463
warning: Source file is more recent than executable.
463 {
(gdb) n
481 rtld_timer_start (&start_time);
到这里,我们成功地进入了_dl_start
函数。从此,我们就可以调试共享库的加载、符号解析、重定位等过程了。
研究结论:
动态链接器的入口为_start
函数(即.text
节的第一条指令),_start
函数通过调用_dl_start
函数,从而真正开始了动态链接器的处理过程。
为了方便,我们把上述调试过程写成一个调试文件——ld.gdb。
ld.gdb:
file /lib64/ld-linux-x86-64.so.2
set args ./hello
symbol-file /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.31.so
directory /home/glibc-2.31/elf/
b _dl_start
starti
c
执行ld.gdb
的过程如下:
$ gdb -q -x ld.gdb
Breakpoint 1 at 0x1df0: file rtld.c, line 463.
Program stopped.
0x00007ffff7fd0100 in _start ()
Breakpoint 1, _dl_start (arg=0x7fffffffde70) at rtld.c:463
warning: Source file is more recent than executable.
463 {
(gdb)
动态链接器支持多种运行模式,比如:normal
、trace
等,其运行模式定义在rtld.c
的enum mode
中。
上一节中,动态链接器是在默认模式normal
下运行的。本节我们以调试ldd -u
命令的执行过程为例,研究如何调试运行在trace
模式下的动态链接器(相比而言,稍复杂些)。
研究过程:
step 1: 准备
1) 了解 ldd -u
ldd -u <object-file>
命令,用于查看未使用的且直接依赖的共享库
有哪些。
通过man ldd
命令,查看 ldd 的 man page。截取部分内容如下:
In the usual case, ldd invokes the standard dynamic linker (see ld.so(8)) with the LD_TRACE_LOADED_OBJECTS environment variable set to 1. This causes the
dynamic linker to inspect the program's dynamic dependencies, and find (according to the rules described in ld.so(8)) and load the objects that satisfy
those dependencies. For each dependency, ldd displays the location of the matching object and the (hexadecimal) address at which it is loaded.
通过上面的描述可以看出,ldd
命令将环境变量LD_TRACE_LOADED_OBJECTS
设置为 1,然后调用动态链接器,从而实现其功能。
也就是说,ldd <object-file>
命令等价于LD_TRACE_LOADED_OBJECTS=1 <object-file>
。
查看ldd.bash.in
(位于 glibc 中)的内容(部分):
if test "$unused" = yes; then
add_env="$add_env LD_DEBUG=\"$LD_DEBUG${LD_DEBUG:+,}unused\""
fi
从上面的代码可以看出,ldd
的参数-u
的作用为:将unused
追加到环境变量LD_DEBUG
中。
因此,可以认为ldd -u <object-file>
命令等价于LD_TRACE_LOADED_OBJECTS=1 LD_DEBUG=unused <object-file>
。
另外,如果存在未使用的且直接依赖的共享库
,那么ldd
会打印如下输出:
Unused direct dependencies:
// 省略 ...
上面输出结果中的Unused direct dependencies:
是在rtld.c
的第 1957 行打印的。相应的代码如下(部分):
1953 if (!l->l_used)
1954 {
1955 if (first)
1956 {
1957 _dl_printf ("Unused direct dependencies:\n");
1958 first = false;
1959 }
1960
1961 _dl_printf ("\t%s\n", l->l_name);
1962 }
因此,我们可以在其稍前面的位置(rtld.c
的第 1929 行)设置断点。如果该断点击中了,那么我们成功地做到了用 gdb 调试运行在tarce
模式下的动态链接器。
rtld.c
的第 1929 行的代码如下:
1929 struct link_map *l = main_map;
2) 如何决定动态链接器的运行模式
动态链接器的运行模式是由环境变量(比如:LD_DEBUG
、LD_TRACE_LOADED_OBJECTS
等)及其参数(比如:--list
、--verify
等)共同决定的。有兴趣的可以分析下rtld.c
中的dl_main
和process_envvars
函数,此处不详细展开。这里,只需要知道环境变量LD_TRACE_LOADED_OBJECTS
对应的动态链接器运行模式为trace
。
step 2: 使用 gdb 启动动态链接器
这里,我们在上一节的调试文件ld.gdb
的基础上,修改其内容以支持调试运行在trace
模式下的动态链接器。
1) 第一次尝试
一种直接的想法是,将环境变量LD_TRACE_LOADED_OBJECTS=1 LD_DEBUG=unused
添加到可执行目标文件的前面,就像直接执行LD_TRACE_LOADED_OBJECTS=1 LD_DEBUG=unused ./hello
命令那样。从而,将这两个环境变量的值传递给动态链接器。
按照该思路修改ld.gdb
,并将修改后的文件命名为ld_t.gdb
。其内容如下:
file /lib64/ld-linux-x86-64.so.2
set args LD_TRACE_LOADED_OBJECTS=1 LD_DEBUG=unused ./hello
symbol-file /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.31.so
directory /home/glibc-2.31/elf/
b _dl_start
b rtld.c:1929
starti
c
执行ld_t.gdb
的过程如下:
$ gdb -q -x ld_t.gdb
Breakpoint 1 at 0x1df0: file rtld.c, line 463.
Breakpoint 2 at 0x5fd5: file rtld.c, line 1929.
Program stopped.
0x00007ffff7fd0100 in _start ()
Breakpoint 1, _dl_start (arg=0x7fffffffde40) at rtld.c:463
463 {
(gdb) c
Continuing.
LD_TRACE_LOADED_OBJECTS=1: error while loading shared libraries: LD_TRACE_LOADED_OBJECTS=1: cannot open shared object file
[Inferior 1 (process 492652) exited with code 0177]
(gdb)
从上面的输出结果可以看出,执行ld_t.gdb
失败了。报错信息为:LD_TRACE_LOADED_OBJECTS=1: error while loading shared libraries: LD_TRACE_LOADED_OBJECTS=1: cannot open shared object file
。从字面上理解,动态链接器将LD_TRACE_LOADED_OBJECTS=1
当成了目标文件。因此,通过这种方式将环境变量传递给动态链接器是行不通的。
另外,如果将环境变量放到starti
的后面,即starti LD_TRACE_LOADED_OBJECTS=1 LD_DEBUG=unused ./hello
。这种方式也是行不通的。
2) 第二次尝试
我们可以通过 gdb 的set env
命令在执行动态链接器前设置环境变量。
按照该思路修改ld_t.gdb
。其内容如下:
file /lib64/ld-linux-x86-64.so.2
set args ./hello
set env LD_TRACE_LOADED_OBJECTS=1
set env LD_DEBUG=unused
symbol-file /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.31.so
directory /home/glibc-2.31/elf/
b _dl_start
b rtld.c:1929
starti
c
注意: 环境变量的设置必须在执行动态链接器前(即在starti
命令之前)。否则,实际不会生效。
执行ld_t.gdb
的过程如下:
$ gdb -q -x ld_t.gdb
Breakpoint 1 at 0x1df0: file rtld.c, line 463.
Breakpoint 2 at 0x5fd5: file rtld.c, line 1929.
ld_t.gdb:12: Error in sourced command file:
During startup program exited normally.
(gdb)
从上面的输出结果可以看出,这次执行直接退出了,甚至_dl_start
函数都未执行到。
要想弄清楚为什么会出现上面的结果。关键之处在于上面我们一直未曾注意的输出Program stopped.
。详细分析过程见下文的 为什么会出现 "Program stopped"。
3) 第三次尝试
这里,我们采用将动态链接器的运行模式从normal
修改为trace
的方式。
按照该思路修改ld_t.gdb
。其内容如下:
file /lib64/ld-linux-x86-64.so.2
set args ./hello
set env LD_DEBUG=unused
symbol-file /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.31.so
directory /home/glibc-2.31/elf/
b _dl_start
b rtld.c:1929
b rtld.c:1968
b rtld.c:2704
b _exit
starti
c
c
p *modep = trace
c
c
c
c
上面的调试文件主要作了两处修改:
去掉了环境变量LD_TRACE_LOADED_OBJECTS=1
的设置。从而,避免执行ld_t.gdb
时直接退出。
增加了断点rtld.c:2704
。从而,可以通过 gdb 命令将动态链接器的运行模式从normal
修改为trace
。
执行ld_t.gdb
的过程如下:
$ gdb -q -x ld_t.gdb
Breakpoint 1 at 0x1df0: file rtld.c, line 463.
Breakpoint 2 at 0x5fd5: file rtld.c, line 1929.
Breakpoint 3 at 0x5403: file rtld.c, line 1968.
Breakpoint 4 at 0x26eb: file rtld.c, line 2704.
Breakpoint 5 at 0x1f280: file ../sysdeps/unix/sysv/linux/_exit.c, line 27.
Program stopped.
0x00007ffff7fd0100 in _start ()
Breakpoint 1, _dl_start (arg=0x7fffffffde60) at rtld.c:463
463 {
Breakpoint 4, process_envvars (modep=<synthetic pointer>) at rtld.c:2704
2704 if (__builtin_expect (__libc_enable_secure, 0))
$1 = trace
Breakpoint 2, dl_main (phdr=<optimized out>, phnum=<optimized out>, user_entry=<optimized out>, auxv=<optimized out>) at rtld.c:1932
1932 struct relocate_args args = { .l = l,
Breakpoint 3, dl_main (phdr=<optimized out>, phnum=<optimized out>, user_entry=<optimized out>, auxv=<optimized out>) at rtld.c:1968
1968 _exit (first != true);
Breakpoint 5, __GI__exit (status=0) at ../sysdeps/unix/sysv/linux/_exit.c:27
27 {
[Inferior 1 (process 504483) exited normally]
(gdb)
从上面的输出结果可以看出:
Breakpoint 2
(即rtld.c:1929
行)处的断点被击中了。意味着,我们进入了动态链接器用于输出ldd -u
命令执行结果的代码。
Breakpoint 3
(即rtld.c:1968
行)处的断点被击中了。意味着,ldd -u
命令不会真正地执行可执行目标文件。
研究结论:
我们可以采用将动态链接器的运行模式从normal
修改为trace
的方式,来调试ldd -u
命令的执行过程。
ldd -u
命令不会真正地执行可执行目标文件。
在执行ld.gdb
时,gdb 的输出结果中有这样一行:Program stopped.
。那么,所谓的Program
指的是哪个程序呢?
研究过程:
step 1: 分析Program
是指哪个程序
1) 修改ld.gdb
修改ld.gdb
,并将修改后的文件命名为ld_1.gdb
。其内容如下:
file /lib64/ld-linux-x86-64.so.2
set args ./hello
set env LD_DEBUG=libs
symbol-file /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.31.so
directory /home/glibc-2.31/elf/
b _dl_start
starti
c
注:设置环境变量LD_DEBUG=libs
是为了跟踪动态链接器加载共享库的过程。
2) 执行ld_1.gdb
执行ld_1.gdb
的过程如下:
$ gdb -q -x ld_1.gdb
Breakpoint 1 at 0x1df0: file rtld.c, line 463.
493684: find library=libtinfo.so.6 [0]; searching
493684: search cache=/etc/ld.so.cache
493684: trying file=/lib/x86_64-linux-gnu/libtinfo.so.6
493684:
493684: find library=libdl.so.2 [0]; searching
493684: search cache=/etc/ld.so.cache
493684: trying file=/lib/x86_64-linux-gnu/libdl.so.2
493684:
493684: find library=libc.so.6 [0]; searching
493684: search cache=/etc/ld.so.cache
493684: trying file=/lib/x86_64-linux-gnu/libc.so.6
493684:
493684:
493684: calling init: /lib/x86_64-linux-gnu/libc.so.6
493684:
493684:
493684: calling init: /lib/x86_64-linux-gnu/libdl.so.2
493684:
493684:
493684: calling init: /lib/x86_64-linux-gnu/libtinfo.so.6
493684:
493684:
493684: initialize program: /bin/bash
493684:
493684:
493684: transferring control: /bin/bash
493684:
Program stopped.
0x00007ffff7fd0100 in _start ()
Breakpoint 1, _dl_start (arg=0x7fffffffde60) at rtld.c:463
463 {
(gdb)
从上面的输出结果可以看出,在打印Program stopped.
之前,gdb 打印了许多信息。我们可以将这些信息可以分为两类:1)搜索共享库;2)将控制权交给/bin/bash
程序。也就是说,所谓的Program
程序实际就是/bin/bash
。 另外,那些共享库正是/bin/bash
所依赖的。
查看/bin/bash
所依赖的共享库:
$ ldd /bin/bash
linux-vdso.so.1 (0x00007fff7a2f4000)
libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f39d84b4000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f39d84ae000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f39d82bc000)
/lib64/ld-linux-x86-64.so.2 (0x00007f39d8627000)
值得注意的是,我们设置的环境变量LD_DEBUG=libs
对/bin/bash
也生效了。这解释了为什么会输出/bin/bash
所依赖共享库的搜索过程。
因此,我们在ld_t.gdb
中设置的环境变量LD_TRACE_LOADED_OBJECTS=1
和LD_DEBUG=unused
也会对/bin/bash
生效。而这正是导致执行ld_t.gdb
会直接退出的原因。接下来详细分析该过程。
step 2: 为什么执行ld_t.gdb
会直接退出
1) 动态链接器是如何使用环境变量LD_TRACE_LOADED_OBJECTS
和LD_DEBUG
的
查看process_envvars
函数(在 rtld.c 中),其相关代码如下:
2530 static void
2531 process_envvars (enum mode *modep)
2532 {
2533 char **runp = _environ;
2534 char *envline;
2535 enum mode mode = normal;
// 省略 ...
2542 while ((envline = _dl_next_ld_env_entry (&runp)) != NULL)
2543 {
2544 size_t len = 0;
2545
2546 while (envline[len] != '\0' && envline[len] != '=')
2547 ++len;
2548
2549 if (envline[len] != '=')
2550 /* This is a "LD_" variable at the end of the string without
2551 a '=' character. Ignore it since otherwise we will access
2552 invalid memory below. */
2553 continue;
2554
2555 switch (len)
2556 {
// 省略 ...
2563 case 5:
2564 /* Debugging of the dynamic linker? */
2565 if (memcmp (envline, "DEBUG", 5) == 0)
2566 {
2567 process_dl_debug (&envline[6]);
2568 break;
2569 }
// 省略 ...
2683 case 20:
2684 /* The mode of the dynamic linker can be set. */
2685 if (memcmp (envline, "TRACE_LOADED_OBJECTS", 20) == 0)
2686 mode = trace;
2687 break;
// 省略 ...
2696 }
2697 }
2698
2699 /* The caller wants this information. */
2700 *modep = mode;
2701
2702 /* Extra security for SUID binaries. Remove all dangerous environment
2703 variables. */
2704 if (__builtin_expect (__libc_enable_secure, 0))
// 省略 ...
}
从上面代码中可以看出,动态链接器解析环境变量时:
默认的运行模式为normal
。
环境变量LD_DEBUG
不会修改默认运行模式。也就是说,LD_DEBUG
对应的运行模式是normal
。
环境变量LD_TRACE_LOADED_OBJECTS
会将运行模式修改为trace
。也就是说,LD_TRACE_LOADED_OBJECTS
对应的运行模式是trace
。
因此,当同时设置环境变量LD_TRACE_LOADED_OBJECTS=1
和LD_DEBUG=unused
时,动态链接器的运行模式为trace
。当然,动态链接器的实际运行模式还受其参数的影响,而ld_t.gdb
中不涉及。所以,执行ld_t.gdb
时,动态链接器的实际运行模式就是trace
。
2) 动态链接器是如何处理trace
模式的
查看dl_main
函数(在 rtld.c 中),其相关代码如下:
1085 static void
1086 dl_main (const ElfW(Phdr) *phdr,
1087 ElfW(Word) phnum,
1088 ElfW(Addr) *user_entry,
1089 ElfW(auxv_t) *auxv)
1090 {
// 省略 ...
1886 if (__builtin_expect (mode, normal) != normal)
1887 {
1888 /* We were run just to list the shared libraries. It is
1889 important that we do this before real relocation, because the
1890 functions we call below for output may no longer work properly
1891 after relocation. */
1892 struct link_map *l;
1893
1894 if (GLRO(dl_debug_mask) & DL_DEBUG_PRELINK)
1895 {
// 省略 ...
1923 }
1924 else if (GLRO(dl_debug_mask) & DL_DEBUG_UNUSED)
1925 {
// 省略 ...
1941 bool first = true;
1942 while (dyn->d_tag != DT_NULL)
1943 {
// 省略 ...
1953 if (!l->l_used)
1954 {
1955 if (first)
1956 {
1957 _dl_printf ("Unused direct dependencies:\n");
1958 first = false;
1959 }
// 省略 ...
1963 }
// 省略 ...
1966 }
1967
1968 _exit (first != true);
1969 }
1970 else if (! main_map->l_info[DT_NEEDED])
1971 _dl_printf ("\tstatically linked\n");
1972 else
1973 {
// 省略 ...
1986 }
// 省略 ...
2113 _exit (0);
2114 }
// 省略 ...
2365 /* Once we return, _dl_sysdep_start will invoke
2366 the DT_INIT functions and then *USER_ENTRY. */
2367 }
从上面的代码可以看出,动态链接器运行在trace
模式下的行为:
当指定了环境变量LD_DEBUG=unused
时,如果存在未使用的且直接依赖的共享库
,那么动态链接器在打印完后会退出(rtld.c 中第 1968 行)。
动态链接器运行在trace
模式下时,不会将控制权交给应用程序。(rtld.c 中第 2113 行)。
也就是说,在执行ld_t.gdb
时,控制权首先会交给/bin/bash
。而加载/bin/bash
依赖的共享库时,受环境变量LD_TRACE_LOADED_OBJECTS=1
的影响,动态链接器的运行模式为trace
,于是在动态链接器加载./hello
之前就退出了。因此,执行ld_t.gdb
会直接退出。
那么,针对这种情况,有两种解决思路:
设法将环境变量LD_TRACE_LOADED_OBJECTS=1
做到不影响/bin/bash
的执行。
不设置环境变量LD_TRACE_LOADED_OBJECTS=1
,那么加载/bin/bash
依赖的共享库时,动态链接器的运行模式就是默认的normal
。所以,不会直接退出。并且,在/bin/bash
完成加载后(即当_dl_start
处的断点击中后),将动态链接器的运行模式从normal
修改为trace
。这样,就可以如期击中rtld.c:1929
行处的断点了。
研究结论:
gdb 的输出信息Program stopped.
中,所谓的Program
程序实际就是/bin/bash
。
执行ld_t.gdb
会直接退出的原因:在执行ld_t.gdb
时,控制权首先会交给/bin/bash
。而加载/bin/bash
依赖的共享库时,受环境变量LD_TRACE_LOADED_OBJECTS=1
的影响,动态链接器的运行模式为trace
,于是在动态链接器加载./hello
之前就退出了。
下一篇:代码调试之目录