计算机系统篇之链接(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

ldd -u 的实现原理

本节通过研究 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命令的输出结果以及上面的代码,我们可以看出:

这里,我们有如下几个疑问:

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_namel_ldl_nextl_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 = {0x00x7ffff7fc7e00, 0x00x7ffff7fc7ed0, 0x00x7ffff7fc7e80, 0x7ffff7fc7e90, 0x7ffff7fc7ee0, 0x7ffff7fc7ef0, 
    0x7ffff7fc7f00, 0x7ffff7fc7ea0, 0x7ffff7fc7eb0, 0x7ffff7fc7e10, 0x7ffff7fc7e20, 0x00x00x00x00x00x00x00x7ffff7fc7ec0, 0x00x00x7ffff7fc7f20, 
    0x7ffff7fc7e30, 0x7ffff7fc7e50, 0x7ffff7fc7e40, 0x7ffff7fc7e60, 0x00x7ffff7fc7f10, 0x00x00x00x00x7ffff7fc7f40, 0x7ffff7fc7f30, 0x00x0
    0x7ffff7fc7f20, 0x00x7ffff7fc7f60, 0x00x00x00x00x00x00x00x00x7ffff7fc7f50, 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, 
    0x00x00x0}, 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_startmain_map->l_map_end的值分别为0x7ffff7fc40000x7ffff7fc8018

查看这两个字段的定义,相关代码如下(在 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_startl_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实际上是指向可执行目标文件hellostruct link_map对象的指针。

step 4: main_map->l_ld 的作用?

l_ld是结构体link_map中的一个字段。其相关注释为:

 /* Dynamic section of the shared object.  */

从上面的注释可以推测,l_ld难道是.dynamicsection 中第一个条目的地址?

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(typeElf##__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正是.dynamicsection 中每个条目的定义。

因此,结构体link_map中的l_ld字段表示其所指对象的.dynamicsection 中第一个条目的地址。

那么,main_map->l_ld的作用为:表示可执行目标文件hello.dynamicsection 中第一个条目的地址。

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
     00000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     10000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     20000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     30000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     40000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     50000000000000000     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 = { NULLNULL };
 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                      &current_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:856rtld.c:872rtld.c:895rtld.c:938rtld.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=0x7ffff7fc44d"__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的意义为:指向可执行目标文件hellostruct 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.dynstrsection 的内容:

$ 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。通过上文的分析,该地址表示可执行目标文件hellostruct 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字段所指向数组中的元素依次为./hellolibstdc++.so.6libm.so.6libgcc_s.so.1libc.so.6ld-linux-x86-64.so.2struct 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=0x7ffff7fc44d"__libc_start_main"// 省略 ..., version=0x7ffff7fae570, // 省略 ...

f) 其他参数:int type_class、int flags、struct link_map *skip_map

在解析上述 5 个未定义的符号时,参数skip_map的值都是NULL。因此,可以不用分析其意义。另外,参数type_classflags的意义,这里我们也不去分析。

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字段的值为NULLm字段的值为 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:861dl-lookup.c:547dl-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
  1974000000000004a090   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.dynstrsection:

$ 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.sostruct link_map对象中l_used字段的值会被设置为 1。也就是说,其它直接依赖的共享库:libstdc++.so.6libm.so.6ibgcc_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.6libm.so.6ibgcc_s.so.1实际都未使用,但仍被映射到了进程的地址空间中。

研究结论:

如果一个直接依赖的共享库不是任何一个未定义符号的提供者,那么执行ldd -u命令时,该共享库会被认为是”未使用的“,从而打印出该共享库的名称。


ldd -u 输出的都一定是未使用的且直接依赖的共享库吗?

本节通过一个示例研究,ldd -u输出的是否都一定是未使用的且直接依赖的共享库。

研究过程:

step 1: 准备

1) 准备共享库

a) 共享库的头文件

sum.h:

#ifndef SUM_H
#define SUM_H

int sum(intint);

#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" 意味着什么?

ldd: Unused direct dependencies意味着:要么找不到这个直接依赖的共享库,要么该直接依赖的共享库不是任何一个未定义符号的提供者。


下一篇:计算机系统篇之链接(18):如何避免未使用的且直接依赖的共享库

上一篇:计算机系统篇之链接(16):真正理解 RTLD_NEXT 的作用

首页