Linux动态链接so初始化执行

Posted tsecer

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux动态链接so初始化执行相关的知识,希望对你有一定的参考价值。

一、so文件和exe文件
这两种文件其实具有很多相似自出,或者说so文件是介于obj文件和exe文件的一种中间过渡形式,它虽然不能直接运行(但是经过特殊编写的so文件内核是支持加载运行的,例如ld.so),但是具有了自己的一些更为高级的内容,例如一些初始化节,got表等内容,虽然孱弱,但是它具有了更加完善的生物形态。但是大家不要用进化论的观点来认为so文件比exe文件出现的早,事实上so是比较新的一个概念。我们看一下这些文件类型的标识类型说明
#define ET_NONE   0
#define ET_REL    1
#define ET_EXEC   2
#define ET_DYN    3
可以看到动态so文件的数值要比exe数值大。
这里说明这么多,就是so可以被通用的动态链接器识别和加载,由于so已经是一个exe的雏形,所以在加载一个so文件的时候可能除了完成动态链接之外还需要进行一些额外的操作,其中我们最为关心的就是个个so中定义的一些全局变量的初始化。这个概念对于通常的C文件生成的so没有直接意义,但是对于包含了全局变量的C++文件可能比较有用,也就是在这个so文件被加载之后、该文件中任何代码执行之前需要执行so中所有的初始化函数。
二、测试工程代码
//总共包含四个源文件,三个dep??.c生成各自的libdep??.so,main.c生成主程序。其中main.exe依赖dep11.so 和 dep12.so,而dep11.so进而依赖dep21.so,主要看这些文件的打开顺序以及初始化函数的执行顺序
[[email protected] soinit]$ ls
dep11.c  dep12.c  dep21.c  main.c  Makefile

[[email protected] soinit]$ cat dep11.c 
#include <stdio.h>
int __attribute__((constructor)) dep11(void)
{
    return printf("In %s ",__FUNCTION__);
}

[[email protected] soinit]$ cat dep12.c 
#include <stdio.h>
int __attribute__((constructor))  dep12(void)
{
     return     printf("In %s ",__FUNCTION__);
}

[[email protected] soinit]$ cat dep21.c 
#include <stdio.h>
int __attribute__((constructor))  dep21(void)
{
    return    printf("In %s ",__FUNCTION__);
}

[[email protected] soinit]$ cat Makefile 
main.exe:main.c libdep11.so libdep12.so
    gcc -fPIC main.c -o [email protected] -L. -ldep11 -ldep12
    LD_LIBRARY_PATH=. ./main.exe
libdep12.so libdep21.so:lib%.so:%.c
    gcc -fPIC -shared -o [email protected] $<
libdep11.so:libdep21.so
    gcc -fPIC -shared -o [email protected] -L. -ldep21 dep11.c 
clean:
    rm -f *.so *.exe *.o

//执行make之后输出的执行顺序,这个顺序我们稍后解释
[[email protected] soinit]$ make
gcc -fPIC -shared -o libdep21.so dep21.c
gcc -fPIC -shared -o libdep11.so -L. -ldep21 dep11.c 
gcc -fPIC -shared -o libdep12.so dep12.c
gcc -fPIC main.c -o main.exe -L. -ldep11 -ldep12
/usr/bin/ld: warning: libdep21.so, needed by ./libdep11.so, not found (try using -rpath or -rpath-link)
LD_LIBRARY_PATH=. ./main.exe
In dep21
In dep12
In dep11
In dep00
三、动态库入口位置由来及意义
1、入口的形式
[[email protected] soinit]$ readelf -a libdep11.so 
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 03 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2‘s complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - Linux
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x370
  Start of program headers:          52 (bytes into file)
  Start of section headers:          1956 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         5
  Size of section headers:           40 (bytes)
  Number of section headers:         27
  Section header string table index: 24

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .note.gnu.build-i NOTE            000000d4 0000d4 000024 00   A  0   0  4
  [ 2] .gnu.hash         GNU_HASH        000000f8 0000f8 00003c 04   A  3   0  4
  [ 3] .dynsym           DYNSYM          00000134 000134 0000b0 10   A  4   1  4
  [ 4] .dynstr           STRTAB          000001e4 0001e4 000090 00   A  0   0  1
  [ 5] .gnu.version      VERSYM          00000274 000274 000016 02   A  3   0  2
  [ 6] .gnu.version_r    VERNEED         0000028c 00028c 000030 00   A  4   1  4
  [ 7] .rel.dyn          REL             000002bc 0002bc 000028 08   A  3   0  4
  [ 8] .rel.plt          REL             000002e4 0002e4 000018 08   A  3  10  4
  [ 9] .init             PROGBITS        000002fc 0002fc 000030 00  AX  0   0  4
  [10] .plt              PROGBITS        0000032c 00032c 000040 04  AX  0   0  4
  [11] .text             PROGBITS        00000370 000370 000138 00  AX  0   0 16
  [12] .fini             PROGBITS        000004a8 0004a8 00001c 00  AX  0   0  4
  这里一个比较有意思的现象就是so文件也有自己的入口地址,这个地址位于.text节的开始。但是明显的我们没有指定其实地址,对于通常的可执行程序,我们使用的是内置连接脚本,通过ld --verbose可以看到脚本内容。我们使用libdep11.so生成的命令添加 -v显示连接器使用的脚本:
[[email protected] soinit]$ ld -shared -verbose
GNU ld version 2.19.51.0.14-34.fc12 20090722
  Supported emulations:
   elf_i386
   i386linux
   elf_x86_64
using internal linker script:
==================================================
/* Script for --shared -z combreloc: shared library, combine & sort relocs */
OUTPUT_FORMAT("elf32-i386", "elf32-i386",
          "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)
SEARCH_DIR("/usr/i686-redhat-linux/lib"); SEARCH_DIR("/usr/local/lib"); SEARCH_DIR("/lib"); SEARCH_DIR("/usr/lib");
内置链接脚本同样使用的是_start符号,但是我们并没有定义这个符号,因为这个符号是在/usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crt1.o中定义的,而生成动态库的时候并没有链接这个文件,下面是通过gcc -v显示的动态链接命令
 /usr/libexec/gcc/i686-redhat-linux/4.4.2/collect2 --eh-frame-hdr --build-id -m elf_i386 --hash-style=gnu -shared -o libdep11.so /usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crti.o /usr/lib/gcc/i686-redhat-linux/4.4.2/crtbeginS.o -L. -L/usr/lib/gcc/i686-redhat-linux/4.4.2 -L/usr/lib/gcc/i686-redhat-linux/4.4.2 -L/usr/lib/gcc/i686-redhat-linux/4.4.2/../../.. -ldep21 /tmp/cc3PSy9f.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i686-redhat-linux/4.4.2/crtendS.o /usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crtn.o
2、入口的由来
在链接器代码中,其中对于这个内容的处理比较特殊,链接器内部特地为so文件的生成做了兼容,所以此时不会出现链接错误:
binutils-2.21.1ldldlang.c
static void
lang_end (void)
{
……
  if ((link_info.relocatable && !link_info.gc_sections)
      || (link_info.shared && !link_info.executable
))
    warn = entry_from_cmdline对于动态链接文件来说,是否警告根据entry是否是命令行确定,由于我们例子中位于内置脚本中,所以不告警
  else
    warn = TRUE;
……
{
      bfd_vma val;
      const char *send;

      /* We couldn‘t find the entry symbol.  Try parsing it as a
     number.  */
      val = bfd_scan_vma (entry_symbol.name, &send, 0);
      if (*send == ‘‘)
    {
      if (! bfd_set_start_address (link_info.output_bfd, val))
        einfo (_("%P%F: can‘t set start address "));
    }
      else
    {
      asection *ts;

      /* Can‘t find the entry symbol, and it‘s not a number.  Use
         the first address in the text section.  */
      ts = bfd_get_section_by_name (link_info.output_bfd, entry_section);如果找不到入口符号(即_start符号),则用entry_section地址代替。其定义为const char *entry_section = ".text";,所以就是text节作为共享库文件的入口,也就是我们看到和.text起始地址一致的原因

      if (ts != NULL)
        {
          if (warn)//这里的warn为false,所以没有告警
        einfo (_("%P: warning: cannot find entry symbol %s;"
             " defaulting to %V "),
               entry_symbol.name,
               bfd_get_section_vma (link_info.output_bfd, ts));
          if (!(bfd_set_start_address
            (link_info.output_bfd,
             bfd_get_section_vma (link_info.output_bfd, ts))))
        einfo (_("%P%F: can‘t set start address "));
        }
      else
        {
          if (warn)
        einfo (_("%P: warning: cannot find entry symbol %s;"
             " not setting start address "),
               entry_symbol.name);
        }
    }
    }
}
3、入口的意义
对于大部分so文件没有意义,只有so真正独立运行时有意义,例如ld.so文件,这个让内核把任务的入口设置到该位置。
四、动态文件初始化顺序
1、init的由来
binutils-2.21.1ldldmain.c
main (int argc, char **argv)

  link_info.init_function = "_init";
  link_info.fini_function = "_fini";

binutils-2.21.1fdelflink.c
      /* Add some entries to the .dynamic section.  We fill in some of the
     values later, in bfd_elf_final_link, but we must add the entries
     now so that we know the final size of the .dynamic section.  */

      /* If there are initialization and/or finalization functions to
     call then add the corresponding DT_INIT/DT_FINI entries.  */
      h = (info->init_function
       ? elf_link_hash_lookup (elf_hash_table (info),
                   info->init_function, FALSE,
                   FALSE, FALSE)
       : NULL);
      if (h != NULL
      && (h->ref_regular
          || h->def_regular))
    {
      if (!_bfd_elf_add_dynamic_entry (info, DT_INIT, 0))
        return FALSE;
    }
对于共享文件链接,通过之前命令可以看到,它的确链接了crti.o,其中也定义了_init函数
[[email protected] soinit]$ nm /usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crt1.o 
00000000 R _IO_stdin_used
00000000 D __data_start
         U __libc_csu_fini
         U __libc_csu_init
         U __libc_start_main
00000000 R _fp_hw
00000000 T _start
00000000 W data_start
         U main
[[email protected] soinit]$ nm /usr/lib/gcc/i686-redhat-linux/4.4.2/../../../crti.o 
         U _GLOBAL_OFFSET_TABLE_
         w __gmon_start__
00000000 T _fini
00000000 T _init
最后定义了_init符号。
2、动态链接器打开共享库顺序
在dlopen--->>>dl_open_worker函数中,它执行大致流程为
_dl_map_object
_dl_map_object_deps
 _dl_init (new, args->argc, args->argv, args->env)
也就是首先打开所有的直接依赖,这是一个递归过程,深度优先,其关键代码为
_dl_map_object_deps
  for (d = l->l_ld; d->d_tag != DT_NULL; ++d)
        if (__builtin_expect (d->d_tag, DT_NEEDED) == DT_NEEDED)
          {
        /* Map in the needed object.  */
        struct link_map *dep;

        /* Recognize DSTs.  */
        name = expand_dst (l, strtab + d->d_un.d_val, 0);
        /* Store the tag in the argument structure.  */
        args.name = name;

        bool malloced;
        int err = _dl_catch_error (&objname, &errstring, &malloced,
                       openaux, &args);
}
在一个so文件的所有依赖被加载完成之后,open函数将会一次调用所有的直接依赖的初始化入口函数,这个函数再负责遍历可能存在的、生成so时使用的各个obj文件中的init节中的函数指针数组。
_dl_init (struct link_map *main_map, int argc, char **argv, char **env)
{
  i = main_map->l_searchlist.r_nlist;
  while (i-- > 0)
    call_init (main_map->l_initfini[i], argc, argv, env);
}
这里可以看到,此处函数执行的时候,它是执行的while(i--)指令,这意味着直接依赖的初始化函数的执行顺序是和在依赖中出现的顺序相反。所以我们看main.exe依次依赖了dep11.so和dep12.so,但是dep12.so的初始化函数的执行要早于dep11.so
[[email protected] soinit]$ readelf -d main.exe 

Dynamic section at offset 0x650 contains 22 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libdep11.so]
 0x00000001 (NEEDED)                     Shared library: [libdep12.so]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
3、动态链接器如何知道各个SO的初始化函数入口
[[email protected] soinit]$ readelf -d main.exe 

Dynamic section at offset 0x650 contains 22 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libdep11.so]
 0x00000001 (NEEDED)                     Shared library: [libdep12.so]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x804836c
 0x0000000d (FINI)                       0x804857c
动过动态节中的DT_INIT和DT_FINI标签确定。
 
 
 
 
 

以上是关于Linux动态链接so初始化执行的主要内容,如果未能解决你的问题,请参考以下文章

Linux下的.so文件是动态链接库

Linux学习——动态链接库和静态链接库

动态链接如何知道在哪里可以找到链接的文件?

怎么查看一个动态库 需要链接 文件

linux下查看动态链接库so文件的依赖的相关组建

面试 Linux 下的动态链接库问题