计算机系统篇之链接(17):"ldd: Unused direct dependencies" 意味着什么?
Author: stormQ
Created: Wednesday, 13. January 2021 08:37PM
Last Modified: Tuesday, 23. February 2021 08:43AM
本文主要通过 gdb 调试分析了glibc 2.31
版本中ldd -u
命令的源码实现,从而理解其实现原理,并有助于解决“如何避免未使用的且直接依赖的共享库”的问题。除此之外,还介绍了几种查看共享库依赖的方法,包括:如何查看所有的共享库依赖、如何查看直接依赖的共享库。
查看共享库依赖的方法 | 是否显示直接依赖的共享库 | 是否显示间接依赖的共享库 |
---|---|---|
ldd <object-file> |
是 | 是 |
/lib64/ld-linux-x86-64.so.2 --list <object-file> |
是 | 是 |
readelf -d <object-file> | grep NEEDED |
是 | 否 |
objdump -p <object-file> | grep NEEDED |
是 | 否 |
本节通过研究 glibc 的源码,从而理解ldd -u
输出结果中Unused
的含义。
研究过程:
step 1: 准备
1) 准备示例程序
hello.cpp:
int main()
{
return 0;
}
生成可执行目标文件——hello:
$ g++ -Wl,--no-as-needed -g -o hello hello.cpp
注:-Wl,--no-as-needed
,表示将--no-as-needed
选项传递给链接器ld
,这里用于产生不必要的直接共享库依赖,从而便于后面的研究。关于该选项的更多相关内容,见笔者的另一篇文章:《计算机系统篇之链接(18):如何避免未使用的且直接依赖的共享库》。
2) 准备调试文件
这里,直接给出调试文件的基础版本,命名为ldd.gdb
。其内容如下:
file /lib64/ld-linux-x86-64.so.2
set args ./hello
set listsize 20
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 rtld.c:1122
b rtld.c:1968
b rtld.c:2704
b _exit
starti
c
p *modep = trace
c
至于“该调试文件为什么这么写”,见笔者的另一篇文章:《代码调试篇(5):如何调试动态链接器》。
step 2: 梳理用于输出 ldd -u 结果的代码流程
1) 查看用于输出"Unused direct dependencies:"及其附近的代码
1938 /* This loop depends on the dependencies of the executable to
1939 correspond in number and order to the DT_NEEDED entries. */
1940 ElfW(Dyn) *dyn = main_map->l_ld;
1941 bool first = true;
1942 while (dyn->d_tag != DT_NULL)
1943 {
1944 if (dyn->d_tag == DT_NEEDED)
1945 {
1946 l = l->l_next;
1947 #ifdef NEED_DL_SYSINFO_DSO
1948 /* Skip the VDSO since it's not part of the list
1949 of objects we brought in via DT_NEEDED entries. */
1950 if (l == GLRO(dl_sysinfo_map))
1951 l = l->l_next;
1952 #endif
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 }
1963 }
1964
1965 ++dyn;
1966 }
结合ldd -u
命令的输出结果以及上面的代码,我们可以看出:
仅在第一次(即局部变量first
值为false
)发现有未使用的(即l->l_used
值为false
)共享库时,才会输出"Unused direct dependencies:"。
只要发现有未使用的共享库,就打印共享库的名称(即l->l_name
的值)。
这里,我们有如下几个疑问:
main_map
的作用?
main_map->l_ld
的作用?
l_used
字段的更新逻辑是怎样的?
step 3: main_map 的作用?
1) 查看main_map
的定义(在 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 {
// 省略 ...
1093 struct link_map *main_map;
// 省略 ...
2367 }
从上面的代码可以看出,main_map
是一个定义在dl_main
函数中的局部变量。其数据类型为struct link_map *
,即指向结构体link_map
对象的指针。
2) 查看struct link_map
的定义(在 include/link.h 中)
82 /* Structure describing a loaded shared object. The `l_next' and `l_prev'
83 members form a chain of all the shared objects loaded at startup.
84
85 These data structures exist in space used by the run-time dynamic linker;
86 modifying them may have disastrous results.
87
88 This data structure might change in future, if necessary. User-level
89 programs must avoid defining objects of this type. */
90
91 struct link_map
92 {
93 /* These first few members are part of the protocol with the debugger.
94 This is the same format used in SVR4. */
95
96 ElfW(Addr) l_addr; /* Difference between the address in the ELF
97 file and the addresses in memory. */
98 char *l_name; /* Absolute file name object was found in. */
99 ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
100 struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
// 省略 ...
278 /* Nonzero if the DSO is used. */
279 unsigned int l_used;
// 省略 ...
339 };
这里,仅列出了l_name
、l_ld
、l_next
和l_used
等相关字段,省略其余不需要关心的。
3) main_map 是如何被设置的?
查看设置main_map
值的代码:
1282 /* Now the map for the main executable is available. */
1283 main_map = GL(dl_ns)[LM_ID_BASE]._ns_loaded;
从上面的注释可以推测,main_map
表示与可执行目标文件相关的struct link_map
对象的指针。
4) 查看 main_map 的值
a)通过调试文件启动
$ gdb -q -x ldd.gdb
Breakpoint 1 at 0x2710: file rtld.c, line 1122.
Breakpoint 2 at 0x4cb3: file rtld.c, line 1277.
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 4, process_envvars (modep=<synthetic pointer>) at rtld.c:2704
2704 if (__builtin_expect (__libc_enable_secure, 0))
$1 = trace
Breakpoint 1, dl_main (phdr=0x7ffff7fcf040, phnum=<optimized out>, user_entry=<optimized out>, auxv=<optimized out>) at rtld.c:1122
1122 if (*user_entry == (ElfW(Addr)) ENTRY_POINT)
b)设置断点,并继续执行
在main_map
被设置的地方(即 rtld.c:1283 行)设置断点后,继续执行。
(gdb) b 1283
Breakpoint 6 at 0x7ffff7fd3cdd: file rtld.c, line 1283.
(gdb) c
Continuing.
Breakpoint 2, dl_main (phdr=<optimized out>, phnum=<optimized out>, user_entry=<optimized out>, auxv=<optimized out>) at rtld.c:1277
1277 _dl_map_object (NULL, rtld_progname, lt_executable, 0,
(gdb) c
Continuing.
Breakpoint 6, dl_main (phdr=<optimized out>, phnum=<optimized out>, user_entry=<optimized out>, auxv=<optimized out>) at rtld.c:1285
1285 if (__builtin_expect (mode, normal) == normal
c)查看 main_map 的值
(gdb) p/x *main_map
$2 = {l_addr = 0x7ffff7fc4000, l_name = 0x7ffff7ffe740, l_ld = 0x7ffff7fc7dd0, l_next = 0x0, l_prev = 0x0, l_real = 0x7ffff7ffe1a0, l_ns = 0x0,
l_libname = 0x7ffff7ffe728, l_info = {0x0, 0x7ffff7fc7e00, 0x0, 0x7ffff7fc7ed0, 0x0, 0x7ffff7fc7e80, 0x7ffff7fc7e90, 0x7ffff7fc7ee0, 0x7ffff7fc7ef0,
0x7ffff7fc7f00, 0x7ffff7fc7ea0, 0x7ffff7fc7eb0, 0x7ffff7fc7e10, 0x7ffff7fc7e20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff7fc7ec0, 0x0, 0x0, 0x7ffff7fc7f20,
0x7ffff7fc7e30, 0x7ffff7fc7e50, 0x7ffff7fc7e40, 0x7ffff7fc7e60, 0x0, 0x7ffff7fc7f10, 0x0, 0x0, 0x0, 0x0, 0x7ffff7fc7f40, 0x7ffff7fc7f30, 0x0, 0x0,
0x7ffff7fc7f20, 0x0, 0x7ffff7fc7f60, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff7fc7f50, 0x0 <repeats 25 times>, 0x7ffff7fc7e70}, l_phdr = 0x7ffff7fc4040,
l_entry = 0x7ffff7fc5040, l_phnum = 0xd, l_ldnum = 0x1f, l_searchlist = {r_list = 0x0, r_nlist = 0x0}, l_symbolic_searchlist = {r_list = 0x7ffff7ffe720,
r_nlist = 0x0}, l_loader = 0x0, l_versions = 0x0, l_nversions = 0x0, l_nbuckets = 0x2, l_gnu_bitmask_idxbits = 0x0, l_gnu_shift = 0x6,
l_gnu_bitmask = 0x7ffff7fc43b0, {l_gnu_buckets = 0x7ffff7fc43b8, l_chain = 0x7ffff7fc43b8}, {l_gnu_chain_zero = 0x7ffff7fc43ac, l_buckets = 0x7ffff7fc43ac},
l_direct_opencount = 0x0, l_type = 0x0, l_relocated = 0x0, l_init_called = 0x0, l_global = 0x0, l_reserved = 0x0, l_phdr_allocated = 0x0, l_soname_added = 0x0,
l_faked = 0x0, l_need_tls_init = 0x0, l_auditing = 0x0, l_audit_any_plt = 0x0, l_removed = 0x0, l_contiguous = 0x1, l_symbolic_in_local_scope = 0x0,
l_free_initfini = 0x0, l_nodelete_active = 0x0, l_nodelete_pending = 0x0, l_cet = 0x7, l_rpath_dirs = {dirs = 0x0, malloced = 0x0}, l_reloc_result = 0x0,
l_versyms = 0x0, l_origin = 0x7ffff7ffe750, l_map_start = 0x7ffff7fc4000, l_map_end = 0x7ffff7fc8018, l_text_end = 0x7ffff7fc6000, l_scope_mem = {0x7ffff7ffe460,
0x0, 0x0, 0x0}, l_scope_max = 0x4, l_scope = 0x7ffff7ffe508, l_local_scope = {0x7ffff7ffe460, 0x0}, l_file_id = {dev = 0x0, ino = 0x0}, l_runpath_dirs = {
dirs = 0x0, malloced = 0x0}, l_initfini = 0x0, l_reldeps = 0x0, l_reldepsmax = 0x0, l_used = 0x0, l_feature_1 = 0x0, l_flags_1 = 0x8000001, l_flags = 0x8,
l_idx = 0x0, l_mach = {plt = 0x0, gotplt = 0x0, tlsdesc_table = 0x0}, l_lookup_cache = {sym = 0x0, type_class = 0x0, value = 0x0, ret = 0x0},
l_tls_initimage = 0x0, l_tls_initimage_size = 0x0, l_tls_blocksize = 0x0, l_tls_align = 0x0, l_tls_firstbyte_offset = 0x0, l_tls_offset = 0x0, l_tls_modid = 0x0,
l_tls_dtor_count = 0x0, l_relro_addr = 0x3dc0, l_relro_size = 0x240, l_serial = 0x0}
从上面的结果可以看出,main_map->l_map_start
、main_map->l_map_end
的值分别为0x7ffff7fc4000
、0x7ffff7fc8018
。
查看这两个字段的定义,相关代码如下(在 include/link.h 中):
91 struct link_map
92 {
// 省略 ...
242 /* Start and finish of memory map for this object. l_map_start
243 need not be the same as l_addr. */
244 ElfW(Addr) l_map_start, l_map_end;
// 省略 ...
339 };
从上面的注释可以推测,l_map_start
、l_map_end
分别表示内存映射文件的起始地址和结束地址。
查看进程当前的内存映射情况:
(gdb) i proc mappings
process 1212883
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x7ffff7fc4000 0x7ffff7fc5000 0x1000 0x0 /home/test/hello
0x7ffff7fc5000 0x7ffff7fc6000 0x1000 0x1000 /home/test/hello
0x7ffff7fc6000 0x7ffff7fc7000 0x1000 0x2000 /home/test/hello
0x7ffff7fc7000 0x7ffff7fc9000 0x2000 0x2000 /home/test/hello
0x7ffff7fc9000 0x7ffff7fcd000 0x4000 0x0 [vvar]
0x7ffff7fcd000 0x7ffff7fcf000 0x2000 0x0 [vdso]
0x7ffff7fcf000 0x7ffff7fd0000 0x1000 0x0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7fd0000 0x7ffff7ff3000 0x23000 0x1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ff3000 0x7ffff7ffb000 0x8000 0x24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffc000 0x7ffff7ffe000 0x2000 0x2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0 [heap]
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack]
0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]
从上面的结果可以看出,内存地址范围为 0x7ffff7fc4000~0x7ffff7fc8018 对应的内存映射文件为可执行目标文件hello
。
因此,main_map
实际上是指向可执行目标文件hello
的struct link_map
对象的指针。
step 4: main_map->l_ld 的作用?
l_ld
是结构体link_map
中的一个字段。其相关注释为:
/* Dynamic section of the shared object. */
从上面的注释可以推测,l_ld
难道是.dynamic
section 中第一个条目的地址?
1) 查看l_ld
字段的数据类型
l_ld
字段的数据类型为ElfW(Dyn) *
。而ElfW
是一个宏定义(在 elf/link.h 中),如下:
28 /* We use this macro to refer to ELF types independent of the native wordsize.
29 `ElfW(TYPE)' is used in place of `Elf32_TYPE' or `Elf64_TYPE'. */
30 #define ElfW(type) _ElfW (Elf, __ELF_NATIVE_CLASS, type)
31 #define _ElfW(e,w,t) _ElfW_1 (e, w, _##t)
32 #define _ElfW_1(e,w,t) e##w##t
将宏展开为:
#define ElfW(type) Elf##__ELF_NATIVE_CLASS##_##type
其中,__ELF_NATIVE_CLASS
也是一个宏定义。在 32-bit 和 64-bit 机器上,其值分别为 32、64。
所以,在 X86-64 Linux 中,l_ld
字段的数据类型为Elf64_Dyn
。其定义如下(在 elf.h 中):
typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
union
{
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn;
结构体Elf64_Dyn
正是.dynamic
section 中每个条目的定义。
因此,结构体link_map
中的l_ld
字段表示其所指对象的.dynamic
section 中第一个条目的地址。
那么,main_map->l_ld
的作用为:表示可执行目标文件hello
的.dynamic
section 中第一个条目的地址。
step 5: 更新 l_used 字段的调用逻辑是怎样的?
1) l_used 字段在哪些地方可能被设置?
在 glibc 2.31 中,结构体link_map
中的l_used
字段可能被设置的地方共有 3 处(不包括初始化link_map
对象时)。
a) 第一处地方:setup_vdso 函数(在 setup-vdso.h 中)
19 static inline void __attribute__ ((always_inline))
20 setup_vdso (struct link_map *main_map __attribute__ ((unused)),
21 struct link_map ***first_preload __attribute__ ((unused)))
22 {
// 省略 ...
72 /* The vDSO is always used. */
73 l->l_used = 1;
// 省略 ...
121 }
从上面的代码可以看出,vDSO
(伪)共享库的l_used
字段的值总是被设置为 1。该共享库不在我们的研究范围之内。因此,不需要关心这种情况。
b) 第二处地方:_dl_new_object 函数(在 dl-object.c 中)
54 /* Allocate a `struct link_map' for a new object being loaded,
55 and enter it into the _dl_loaded list. */
56 struct link_map *
57 _dl_new_object (char *realname, const char *libname, int type,
58 struct link_map *loader, int mode, Lmid_t nsid)
59 {
// 省略 ...
125 /* If we set the bit now since we know it is never used we avoid
126 dirtying the cache line later. */
127 if ((GLRO(dl_debug_mask) & DL_DEBUG_UNUSED) == 0)
128 new->l_used = 1;
// 省略 ...
260 }
从上面的代码可以看出,当DL_DEBUG_UNUSED
标志位的值为 0 时,l_used
字段的值才会被设置为 1。
由于在ldd -u
命令执行时,DL_DEBUG_UNUSED
标志位的值被设置为 1。所以,这里的语句new->l_used = 1;
不可能被执行。因此,也不需要关心这种情况。
c) 第三处地方:_dl_lookup_symbol_x 函数(在 dl-lookup.c 中)
829 /* Search loaded objects' symbol tables for a definition of the symbol
830 UNDEF_NAME, perhaps with a requested version for the symbol.
831
832 We must never have calls to the audit functions inside this function
833 or in any function which gets called. If this would happen the audit
834 code might create a thread which can throw off all the scope locking. */
835 lookup_t
836 _dl_lookup_symbol_x (const char *undef_name, struct link_map *undef_map,
837 const ElfW(Sym) **ref,
838 struct r_scope_elem *symbol_scope[],
839 const struct r_found_version *version,
840 int type_class, int flags, struct link_map *skip_map)
841 {
// 省略 ...
943 /* The object is used. */
944 if (__glibc_unlikely (current_value.m->l_used == 0))
945 current_value.m->l_used = 1;
// 省略 ...
954 }
到这里,l_used
字段可能被设置的地方我们已经排除了两处。因此,接下来只需要研究_dl_lookup_symbol_x
函数是如何被调用的及其函数实现,我们就可以弄清楚ldd -u
命令的实现原理了。
2) _dl_lookup_symbol_x 是如何被调用的?
在ldd -u
命令执行的过程中,_dl_lookup_symbol_x
函数的调用者共有 2 个。
a) rtld.c 中的第 1546 行(在 dl_main 函数中)
1545 /* With vDSO setup we can initialize the function pointers. */
1546 setup_vdso_pointers ();
从上面的代码可以看出,setup_vdso_pointers
函数是与vDSO
(伪)共享库相关的。该共享库不在我们的研究范围之内。因此,不需要关心这种情况。
b) rtld.c 中的第 1936 行(在 dl_main 函数中)
1936 _dl_receive_error (print_unresolved, relocate_doit, &args);
其中,真正导致_dl_lookup_symbol_x
函数被调用的是relocate_doit
函数。
从执行relocate_doit
函数到_dl_lookup_symbol_x
函数被调用,中间过程涉及多个函数和宏定义,比如:_dl_relocate_object
函数、ELF_DYNAMIC_RELOCATE
宏定义、elf_machine_rela
函数、RESOLVE_MAP
宏定义等。接下来,我们只分析_dl_lookup_symbol_x
函数是如何更新l_used
字段的。
step 6:分析 _dl_lookup_symbol_x 函数的源码实现
1) _dl_lookup_symbol_x 函数的作用?
a) 查看 _dl_lookup_symbol_x 函数的注释
829 /* Search loaded objects' symbol tables for a definition of the symbol
830 UNDEF_NAME, perhaps with a requested version for the symbol.
831
832 We must never have calls to the audit functions inside this function
833 or in any function which gets called. If this would happen the audit
834 code might create a thread which can throw off all the scope locking. */
从上面的注释可以推测,_dl_lookup_symbol_x
函数的作用为:将(带版本号的)未定义符号与一个符号定义关联起来。
b) 查看可执行目标文件 hello 中未定义的符号有哪些
$ readelf --dyn-syms hello | grep UND
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
5: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
从上面的结果可以看出,可执行目标文件hello
中未定义的符号共有 5 个,分别是:_ITM_deregisterTMCloneTab
、__libc_start_main
、__gmon_start__
、_ITM_registerTMCloneTable
和__cxa_finalize
。
2) 分析 _dl_lookup_symbol_x 函数中实际被调用的代码有哪些
在_dl_lookup_symbol_x
函数处设置断点后,连续单步执行。从而,分析在上述 5 个未定义符号的执行过程中,_dl_lookup_symbol_x
函数中一定不会被调用的代码有哪些。排除这些代码后,_dl_lookup_symbol_x
函数中实际可能被调用的代码如下:
835 lookup_t
836 _dl_lookup_symbol_x (const char *undef_name, struct link_map *undef_map,
837 const ElfW(Sym) **ref,
838 struct r_scope_elem *symbol_scope[],
839 const struct r_found_version *version,
840 int type_class, int flags, struct link_map *skip_map)
841 {
842 const uint_fast32_t new_hash = dl_new_hash (undef_name);
843 unsigned long int old_hash = 0xffffffff;
844 struct sym_val current_value = { NULL, NULL };
845 struct r_scope_elem **scope = symbol_scope;
846
// 省略 ...(此处代码会被调用,但无关紧要)
852
853 size_t i = 0;
// 省略 ...
858
859 /* Search the relevant loaded objects for a definition. */
860 for (size_t start = i; *scope != NULL; start = 0, ++scope)
861 if (do_lookup_x (undef_name, new_hash, &old_hash, *ref,
862 ¤t_value, *scope, start, version, flags,
863 skip_map, type_class, undef_map) != 0)
864 break;
865
866 if (__glibc_unlikely (current_value.s == NULL))
867 {
// 省略 ...
885 *ref = NULL;
886 return 0;
887 }
888
// 省略 ...
942
943 /* The object is used. */
944 if (__glibc_unlikely (current_value.m->l_used == 0))
945 current_value.m->l_used = 1;
946
// 省略 ...
951
952 *ref = current_value.s;
953 return LOOKUP_VALUE (current_value.m);
954 }
为了方便起见,也可以分别在rtld.c:856
、rtld.c:872
、rtld.c:895
、rtld.c:938
、rtld.c:949
处设置断点。在解析上述 5 个未定义符号时,这些断点都不会被击中。因此,不必单步执行就可以说明这些断点附近的代码实际上不会被调用。
3) 分析 _dl_lookup_symbol_x 函数中相关参数的意义
a) 参数:const char *undef_name
在_dl_lookup_symbol_x
函数处设置断点后,连续继续执行。每当该断点被击中时,gdb 都会输出参数undef_name
的值,结果如下:
(gdb) c
Continuing.
Breakpoint 6, _dl_lookup_symbol_x (undef_name=0x7ffff7fc44cf "__cxa_finalize", // 省略...
(gdb) c
Continuing.
Breakpoint 6, _dl_lookup_symbol_x (undef_name=0x7ffff7fc4477 "_ITM_deregisterTMCloneTable", // 省略...
(gdb) c
Continuing.
Breakpoint 6, _dl_lookup_symbol_x (undef_name=0x7ffff7fc44de "__libc_start_main", // 省略...
(gdb) c
Continuing.
Breakpoint 6, _dl_lookup_symbol_x (undef_name=0x7ffff7fc4468 "__gmon_start__", // 省略...
(gdb) c
Continuing.
Breakpoint 6, _dl_lookup_symbol_x (undef_name=0x7ffff7fc4493 "_ITM_registerTMCloneTable", // 省略...
从上面的结果可以看出,参数undef_name
的意义为:表示未定义符号的名称,并且不带版本信息。
b) 参数:struct link_map *undef_map
在解析__cxa_finalize
等 5 个未定义符号时,_dl_lookup_symbol_x
函数中参数undef_map
的值都是 0x7ffff7ffe1a0。结果如下(部分):
(gdb) c
Continuing.
Breakpoint 6, _dl_lookup_symbol_x (undef_name=0x7ffff7fc44cf "__cxa_finalize", undef_map=undef_map@entry=0x7ffff7ffe1a0, // 省略...
查看参数undef_map
的值:
(gdb) p/x *undef_map
$2 = { // 省略..., l_map_start = 0x7ffff7fc4000, l_map_end = 0x7ffff7fc8018, // 省略... }
通过上文的分析,内存地址范围为 0x7ffff7fc4000~0x7ffff7fc8018 对应的内存映射文件为可执行目标文件hello
。
因此,参数undef_map
的意义为:指向可执行目标文件hello
的struct link_map
对象的指针。
c) 参数:const ElfW(Sym) **ref
在解析未定义符号__cxa_finalize
时,查看参数ref
的值:
(gdb) c
Continuing.
Breakpoint 6, _dl_lookup_symbol_x (undef_name=0x7ffff7fc44cf "__cxa_finalize", // 省略..., ref=ref@entry=0x7fffffffdab8, // 省略...
(gdb) display/x **ref
1: /x **ref = {st_name = 0x77, st_info = 0x22, st_other = 0x0, st_shndx = 0x0, st_value = 0x0, st_size = 0x0}
在 64-bit 机器中,参数ref
的数据类型为Elf64_Sym **
。所以,参数ref
表示一个符号表条目。
另外,通过该符号表条目中st_name
字段的值,我们可以确定该符号表条目正是__cxa_finalize
的。
查看可执行目标文件hello
中.dynstr
section 的内容:
$ readelf -p .dynstr hello
String dump of section '.dynstr':
[ 1] libstdc++.so.6
[ 10] __gmon_start__
[ 1f] _ITM_deregisterTMCloneTable
[ 3b] _ITM_registerTMCloneTable
[ 55] libm.so.6
[ 5f] libgcc_s.so.1
[ 6d] libc.so.6
[ 77] __cxa_finalize
[ 86] __libc_start_main
[ 98] GLIBC_2.2.5
从上面的结果可以看出,偏移量为 0x77(即st_name
字段的值)处的符号名称正是__cxa_finalize
。
因此,参数ref
的意义为:由参数undef_name
指定的未定义符号所对应的符号表条目。
d) 参数:struct r_scope_elem *symbol_scope[]
查看结构体r_scope_elem
的定义(在 include/link.h 中):
64 /* Structure to describe a single list of scope elements. The lookup
65 functions get passed an array of pointers to such structures. */
66 struct r_scope_elem
67 {
68 /* Array of maps for the scope. */
69 struct link_map **r_list;
70 /* Number of entries in the scope. */
71 unsigned int r_nlist;
72 };
在解析未定义符号__cxa_finalize
时,查看参数symbol_scope
的值:
(gdb) c
Continuing.
Breakpoint 6, _dl_lookup_symbol_x (undef_name=0x7ffff7fc44cf "__cxa_finalize", // 省略 ...,
symbol_scope=symbol_scope@entry=0x7ffff7ffe508, // 省略 ...
(gdb) p/x (*symbol_scope)->r_nlist
$2 = 0x6
(gdb) p/x (*symbol_scope)->r_list[0]
$3 = 0x7ffff7ffe1a0
(gdb) p/x (*symbol_scope)->r_list[1]
$4 = 0x7ffff7fad000
(gdb) p/x (*symbol_scope)->r_list[2]
$5 = 0x7ffff7fad500
(gdb) p/x (*symbol_scope)->r_list[3]
$6 = 0x7ffff7fada00
(gdb) p/x (*symbol_scope)->r_list[4]
$7 = 0x7ffff7fadf00
(gdb) p/x (*symbol_scope)->r_list[5]
$8 = 0x7ffff7ffd9e8
(gdb) p/x (*symbol_scope)->r_list[6]
$9 = 0x0
(gdb) p symbol_scope[1]
$10 = (struct r_scope_elem *) 0x0
从上面的结果可以看出,参数symbol_scope
指向了一个大小为 1、元素数据类型为struct r_scope_elem *
的数组。其中,第一个元素中的r_list
字段指向了一个大小为 6、元素数据类型为struct link_map *
的指针数组,并且该数组的第一个元素的值为 0x7ffff7ffe1a0。通过上文的分析,该地址表示可执行目标文件hello
的struct link_map
对象的指针。
查看该数组中其余元素的l_name
字段的值:
(gdb) p (*symbol_scope)->r_list[1]->l_name
$14 = 0x7ffff7ffeeb0 "/lib/x86_64-linux-gnu/libstdc++.so.6"
(gdb) p (*symbol_scope)->r_list[2]->l_name
$15 = 0x7ffff7fad4e0 "/lib/x86_64-linux-gnu/libm.so.6"
(gdb) p (*symbol_scope)->r_list[3]->l_name
$16 = 0x7ffff7fad9d0 "/lib/x86_64-linux-gnu/libgcc_s.so.1"
(gdb) p (*symbol_scope)->r_list[4]->l_name
$17 = 0x7ffff7fadee0 "/lib/x86_64-linux-gnu/libc.so.6"
(gdb) p (*symbol_scope)->r_list[5]->l_name
$18 = 0x7fffffffe229 "/usr/lib64/ld-linux-x86-64.so.2"
从上面的结果可以看出,symbol_scope[0]->r_list
字段所指向数组中的元素依次为./hello
、libstdc++.so.6
、libm.so.6
、libgcc_s.so.1
、libc.so.6
、ld-linux-x86-64.so.2
的struct link_map
对象的指针。
另外,在解析其余 4 个未定义的符号时,参数symbol_scope
的值都与上面输出结果中的相同,这里不再赘述。
e) 参数:const struct r_found_version *version
查看结构体r_found_version
的定义(在 ldsodefs.h 中):
183 /* For the version handling we need an array with only names and their
184 hash values. */
185 struct r_found_version
186 {
187 const char *name;
188 ElfW(Word) hash;
189
190 int hidden;
191 const char *filename;
192 };
在解析未定义符号__cxa_finalize
时,查看参数version
的值:
(gdb) p *version
$19 = {name = 0x7ffff7fc44f0 "GLIBC_2.2.5", hash = 157882997, hidden = 0, filename = 0x7ffff7fc44c5 "libc.so.6"}
另外,在解析其余 4 个未定义的符号时,只有__libc_start_main
符号带版本信息。并且,__cxa_finalize
和__libc_start_main
共用同一个struct r_found_version
对象。也就是说,解析__cxa_finalize
和__libc_start_main
时,两者的参数version
的值是相同的,如下所示:
(gdb) c
Continuing.
Breakpoint 6, _dl_lookup_symbol_x (undef_name=0x7ffff7fc44cf "__cxa_finalize", // 省略 ..., version=0x7ffff7fae570, // 省略 ...
(gdb) // 省略 ...
(gdb) c
Continuing.
Breakpoint 6, _dl_lookup_symbol_x (undef_name=0x7ffff7fc44de "__libc_start_main", // 省略 ..., version=0x7ffff7fae570, // 省略 ...
f) 其他参数:int type_class、int flags、struct link_map *skip_map
在解析上述 5 个未定义的符号时,参数skip_map
的值都是NULL
。因此,可以不用分析其意义。另外,参数type_class
和flags
的意义,这里我们也不去分析。
step 7: _dl_lookup_symbol_x 函数是如何更新 l_used 字段的?
_dl_lookup_symbol_x
函数中更新l_used
字段的代码如下:
943 /* The object is used. */
944 if (__glibc_unlikely (current_value.m->l_used == 0))
945 current_value.m->l_used = 1;
从上面的代码可以看出,当current_value.m->l_used
的值为 0 时,才会更新l_used
的值为 1。
通过上文的分析,我们知道l_used
的值为 0 时,表示未使用。那么,l_used
的值为 1 时,表示已使用。
接下来,分析在解析上述 5 个未定义的符号时,局部变量struct sym_val current_value
的值。
1) 第一次尝试
在 dl-lookup.c:944 行处设置断点,观察局部变量current_value
的值。
(gdb) b dl-lookup.c:944
Breakpoint 12 at 0x7ffff7fdb22b: file dl-lookup.c, line 944.
(gdb) c
Continuing.
Breakpoint 12, _dl_lookup_symbol_x (undef_name=0x7ffff7fc44cf "__cxa_finalize", undef_map=0x7fffffffd9a0, ref=0x7fffffffdab8, symbol_scope=<optimized out>,
version=0x0, type_class=6, flags=-134552208, skip_map=0x7ffff7ffe1a0) at dl-lookup.c:944
944 if (__glibc_unlikely (current_value.m->l_used == 0))
(gdb) p current_value
$2 = {s = 0x0, m = 0x1c5}
从上面的结果可以看出,局部变量current_value
中,s
字段的值为NULL
,m
字段的值为 0x1c5。
然而,m
字段的数据类型为struct link_map *
,即m
字段的值表示一个内存地址。很显然,0x1c5 不是一个合法的内存地址。
另外,如果s
字段的值真的为NULL
,那么控制流也不会执行到 dl-lookup.c:944 行。这是因为,dl-lookup.c:866 行的代码判断s
字段的值为NULL
时,会在其条件分支结束时退出_dl_lookup_symbol_x
函数。相关代码如下:
866 if (__glibc_unlikely (current_value.s == NULL))
867 {
// 省略 ...
885 *ref = NULL;
886 return 0;
887 }
从这两点来看,我们用 gdb 观察到的current_value
的值与其实际值并不符合。那么,如何观察其实际值呢?继续往下看。
2) 第二次尝试
要观察_dl_lookup_symbol_x
函数中局部变量current_value
的实际值,我们利用了这样一个事实:do_lookup_x
函数中参数result
的值就是_dl_lookup_symbol_x
函数中局部变量current_value
的地址。
a) 设置断点
在解析未定义符号__cxa_finalize
时,分别在dl-lookup.c:861
、dl-lookup.c:547
、dl-lookup.c:554
处设置断点,如下所示:
(gdb) c
Continuing.
Breakpoint 6, _dl_lookup_symbol_x (undef_name=0x7ffff7fc44cf "__cxa_finalize", // 省略 ...
(gdb) b dl-lookup.c:861
Breakpoint 12 at 0x7ffff7fdb1bb: file dl-lookup.c, line 861.
(gdb) b dl-lookup.c:547
Breakpoint 13 at 0x7ffff7fda78b: file dl-lookup.c, line 552.
(gdb) b dl-lookup.c:554
Breakpoint 14 at 0x7ffff7fda7a1: file dl-lookup.c, line 554.
b) 进入 do_lookup_x 函数
(gdb) c
Continuing.
Breakpoint 12, _dl_lookup_symbol_x (undef_name=0x7ffff7fc44cf "__cxa_finalize", // 省略 ...
(gdb) s
do_lookup_x (undef_name=undef_name@entry=0x7ffff7fc44cf "__cxa_finalize", // 省略 ..., result=result@entry=0x7fffffffd9a0, // 省略 ...
从上面的结果可以看出,do_lookup_x
函数中参数result
的值为 0x7fffffffd9a0。该地址也是_dl_lookup_symbol_x
函数中局部变量current_value
的地址。
c) 查看 current_value 的值
继续执行,结果如下所示:
(gdb) c
Continuing.
Breakpoint 13, do_lookup_x (undef_name=0x0, // 省略 ..., result=0x340, // 省略 ...) at dl-lookup.c:553
553 result->m = (struct link_map *) map;
(gdb) c
Continuing.
Breakpoint 14, do_lookup_x (undef_name=<optimized out>, // 省略 ..., result=0x340, // 省略 ...) at dl-lookup.c:554
554 return 1;
在do_lookup_x
函数即将返回时,查看current_value
的值:
(gdb) p *(struct sym_val*)0x7fffffffd9a0
$2 = {s = 0x7ffff7a82e58, m = 0x7ffff7fadf00}
从上面的结果可以看出,current_value
的值:s
字段的值为 0x7ffff7a82e58 ,m
字段的值为 0x7ffff7fadf00。
d) 查看 current_value.m 的值
在do_lookup_x
函数即将返回时,查看current_value.m
的值:
(gdb) p/x *(struct link_map*)0x7ffff7fadf00
$3 = {l_addr = 0x7ffff7a70000, l_name = 0x7ffff7fadee0, l_ld = 0x7ffff7c5ab80, l_next = 0x7ffff7ffd9e8, l_prev = 0x7ffff7fada00, // 省略 ..., l_map_start = 0x7ffff7a70000, l_map_end = 0x7ffff7c614d8, // 省略 ...}
我们可以通过l_name
或者l_map_start、l_map_end
字段,从而确定内存地址为 0x7ffff7fadf00 的struct link_map
对象所对应的共享库正是libc.so.6
,如下所示:
(gdb) p((struct link_map*)0x7ffff7fadf00)->l_name
$4 = 0x7ffff7fadee0 "/lib/x86_64-linux-gnu/libc.so.6"
而__cxa_finalize
函数的定义正是由libc.so.6
提供的,如下所示:
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep __cxa_finalize
1974: 000000000004a090 608 FUNC GLOBAL DEFAULT 16 __cxa_finalize@@GLIBC_2.2.5
e) 查看 current_value.s 的值
在do_lookup_x
函数即将返回时,查看current_value.s
的值:
(gdb) p/x *(Elf64_Sym*)0x7ffff7a82e58
$5 = {st_name = 0x30f1, st_info = 0x12, st_other = 0x0, st_shndx = 0x10, st_value = 0x4a090, st_size = 0x260}
从上面的结果可以看出,该符号表条目中st_name
字段的值为 0x30f1。那么该符号表条目是哪个符号的呢?
查看libc.so.6
的.dynstr
section:
$ readelf -p .dynstr /lib/x86_64-linux-gnu/libc.so.6 | grep 30f1
[ 30f1] __cxa_finalize
从上面的结果可以看出,该符号表条目对应的符号是__cxa_finalize
。
因此,_dl_lookup_symbol_x
函数中局部变量current_value
的意义为:s
字段表示符号定义对应的符号表条目,m
字段表示符号定义的提供者。
我们可以这样理解,_dl_lookup_symbol_x
函数更新l_used
字段的逻辑为:在解析未定义符号时,如果找到了对应的符号定义,那么就将符号定义提供者的l_used
字段设置为 1,从而表示该提供者被使用了。
3) 查看其余 4 个未定义符号的 current_value 的值
类似地(需要额外在 dl-lookup.c:885 行处设置断点),查看其余 4 个未定义符号的current_value
实际值,具体过程不再赘述。这里,直接给出结果如下:
未定义的符号 | current_value 的值 | current_value.s 的值的含义 | current_value.m 的值的含义 |
---|---|---|---|
_ITM_deregisterTMCloneTable |
{s = 0x0, m = 0x0} | 未找到该符号定义的符号表条目 | 无 |
__libc_start_main |
{s = 0x7ffff7a846d0, m = 0x7ffff7fadf00} | 表示 __libc_start_main 符号定义的符号表条目 | 表示 __libc_start_main 符号定义的提供者为共享库 libc.so |
__gmon_start__ |
{s = 0x0, m = 0x0} | 未找到该符号定义的符号表条目 | 无 |
_ITM_registerTMCloneTable |
{s = 0x0, m = 0x0} | 未找到该符号定义的符号表条目 | 无 |
从上面的结果可以看出,只有共享库libc.so
的struct link_map
对象中l_used
字段的值会被设置为 1。也就是说,其它直接依赖的共享库:libstdc++.so.6
、libm.so.6
、ibgcc_s.so.1
都是未使用的。我们可以查看ldd -u
的输出结果加以佐证这一点,如下所示:
(gdb) c
Continuing.
Unused direct dependencies:
/lib/x86_64-linux-gnu/libstdc++.so.6
/lib/x86_64-linux-gnu/libm.so.6
/lib/x86_64-linux-gnu/libgcc_s.so.1
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);
查看进程当前的内存映射情况:
(gdb) i proc mappings
process 197068
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x7ffff7a6e000 0x7ffff7a70000 0x2000 0x0
0x7ffff7a70000 0x7ffff7a95000 0x25000 0x0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7a95000 0x7ffff7c0d000 0x178000 0x25000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7c0d000 0x7ffff7c57000 0x4a000 0x19d000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7c57000 0x7ffff7c58000 0x1000 0x1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7c58000 0x7ffff7c5e000 0x6000 0x1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7c5e000 0x7ffff7c62000 0x4000 0x0
0x7ffff7c62000 0x7ffff7c65000 0x3000 0x0 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
0x7ffff7c65000 0x7ffff7c77000 0x12000 0x3000 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
0x7ffff7c77000 0x7ffff7c7b000 0x4000 0x15000 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
0x7ffff7c7b000 0x7ffff7c7d000 0x2000 0x18000 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
0x7ffff7c7d000 0x7ffff7c8c000 0xf000 0x0 /usr/lib/x86_64-linux-gnu/libm-2.31.so
0x7ffff7c8c000 0x7ffff7d33000 0xa7000 0xf000 /usr/lib/x86_64-linux-gnu/libm-2.31.so
0x7ffff7d33000 0x7ffff7dca000 0x97000 0xb6000 /usr/lib/x86_64-linux-gnu/libm-2.31.so
0x7ffff7dca000 0x7ffff7dcc000 0x2000 0x14c000 /usr/lib/x86_64-linux-gnu/libm-2.31.so
0x7ffff7dcc000 0x7ffff7e62000 0x96000 0x0 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28
0x7ffff7e62000 0x7ffff7f52000 0xf0000 0x96000 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28
0x7ffff7f52000 0x7ffff7f9b000 0x49000 0x186000 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28
0x7ffff7f9b000 0x7ffff7f9c000 0x1000 0x1cf000 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28
0x7ffff7f9c000 0x7ffff7faa000 0xe000 0x1cf000 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28
0x7ffff7faa000 0x7ffff7faf000 0x5000 0x0
0x7ffff7faf000 0x7ffff7fc4000 0x15000 0x0 /etc/ld.so.cache
0x7ffff7fc4000 0x7ffff7fc5000 0x1000 0x0 /home/test/hello
0x7ffff7fc5000 0x7ffff7fc6000 0x1000 0x1000 /home/test/hello
0x7ffff7fc6000 0x7ffff7fc7000 0x1000 0x2000 /home/test/hello
0x7ffff7fc7000 0x7ffff7fc8000 0x1000 0x2000 /home/test/hello
0x7ffff7fc8000 0x7ffff7fc9000 0x1000 0x3000 /home/test/hello
0x7ffff7fc9000 0x7ffff7fcd000 0x4000 0x0 [vvar]
0x7ffff7fcd000 0x7ffff7fcf000 0x2000 0x0 [vdso]
0x7ffff7fcf000 0x7ffff7fd0000 0x1000 0x0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7fd0000 0x7ffff7ff3000 0x23000 0x1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ff3000 0x7ffff7ffb000 0x8000 0x24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffc000 0x7ffff7ffe000 0x2000 0x2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0 [heap]
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack]
0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]
从上面的结果可以看出,虽然直接依赖的共享库:libstdc++.so.6
、libm.so.6
、ibgcc_s.so.1
实际都未使用,但仍被映射到了进程的地址空间中。
研究结论:
如果一个直接依赖的共享库不是任何一个未定义符号的提供者,那么执行ldd -u
命令时,该共享库会被认为是”未使用的“,从而打印出该共享库的名称。
本节通过一个示例研究,ldd -u
输出的是否都一定是未使用的且直接依赖的共享库。
研究过程:
step 1: 准备
1) 准备共享库
a) 共享库的头文件
sum.h:
#ifndef SUM_H
#define SUM_H
int sum(int, int);
#endif // SUM_H
b) 共享库的源文件
sum.cpp:
#include "sum.h"
int sum(int a, int b)
{
return a + b;
}
c) 生成共享库
$ g++ -fPIC -shared -g -o libsum.so sum.cpp
2) 准备示例程序
a) 源文件
main.cpp:
#include "sum.h"
#include <stdio.h>
int g_val_1 = 1;
int g_val_2 = 16;
int main()
{
printf("g_val_1=%x, g_val_2=%x\n", g_val_1, g_val_2);
printf("sum=%x\n", sum(g_val_1, g_val_2));
return 0;
}
b) 生成可执行目标文件 main
$ g++ -g -o main main.cpp -lsum -L.
step 2: 查看可执行目标文件 main 中未使用的且直接依赖的共享库
$ ldd -u main
Unused direct dependencies:
libsum.so
从上面的结果可以看出,libsum.so
被认为是未使用的且直接依赖的共享库(实际上并非如此)。这是为什么呢?
step 3: 查看可执行目标文件 main 所有依赖的共享库
$ ldd main
linux-vdso.so.1 (0x00007ffc83b64000)
libsum.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa0eec24000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa0eee32000)
从上面的结果可以看出,libsum.so
未找到。这正是其被当作未使用的且直接依赖的共享库的原因。
研究结论:
如果找不到一个直接依赖的共享库,那么该共享库也会出现在ldd -u
的输出结果中。
ldd: Unused direct dependencies
意味着:要么找不到这个直接依赖的共享库,要么该直接依赖的共享库不是任何一个未定义符号的提供者。
下一篇:计算机系统篇之链接(18):如何避免未使用的且直接依赖的共享库