7.5. ELF在Linux下的动态链接实现

动态链接下,可执行文件的装载和静态链接情况基本一样,操作系统先读取可执行文件头部,检查文件合法性,然后从头部中的Program Header中读取每个Segment的虚拟地址,文件偏移和属性,并将他们映射到程序虚拟空间相应位置。

在动态链接下,操作系统将控制权交给了动态链接器ld.so,操作系统通过映射的方式将它加载到进程地址空间中.将控制权交给动态链接器的入口地址。
然后动态链接器进行一系列自身初始化操作,然后根据当前环境参数,开始对可执行文件进行动态链接工作,当所有动态链接工作完成后,将控制权交给可执行程序入口,程序开始执行。

.interp段:
指定了动态链接器的位置。
/lib/ld-linux.so.2
动态链接器在linux下是Glibc中的一部分,属于系统库级别的,

readelf -l program1 | grep interpreter      [Requesting program interpreter: /lib/ld-linux.so.2]i

.dynamic段:
动态链接ELF中最重要的段是.dynamic段,这个段里面保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象,动态链接符号表的位置,动态链接重定位表的位置,共享对象初始化代码的地址等。
Elf32_Dyn由一个类型值加上一个附加的数值或指针。
DT_SYMTAB,动态链接符号表的地址,d_ptr表示.dynsym的地址
DT_STRTAB,动态链接字符串表地址,d_ptr表示.dynstr的地址
DT_HASH,动态链接哈希表大小,d_val表示大小
DT_SONAME,本共享对象的SO-NAME
DT_RPATH,动态链接共享对象搜索路径
DT_INIT,初始化代码地址
DT_FINIT,结束代码地址
DT_NEED,依赖的共享对象文件,d_ptr表示依赖的共享对象文件名
DT_REL,动态链接重定位表地址
DT_RELA,
DT_RELENT,动态重读位表入口数量
DT_RELANET

phil~/repos/my_bible $ readelf -d lib.so

Dynamic section at offset 0x644 contains 24 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x3a4
 0x0000000d (FINI)                       0x588
 0x00000019 (INIT_ARRAY)                 0x1638
 0x0000001b (INIT_ARRAYSZ)               4 (bytes)
 0x0000001a (FINI_ARRAY)                 0x163c
 0x0000001c (FINI_ARRAYSZ)               4 (bytes)
 0x6ffffef5 (GNU_HASH)                   0x118
 0x00000005 (STRTAB)                     0x234
 0x00000006 (SYMTAB)                     0x154
 0x0000000a (STRSZ)                      193 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000003 (PLTGOT)                     0x1738
 0x00000002 (PLTRELSZ)                   32 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x384
 0x00000011 (REL)                        0x344
 0x00000012 (RELSZ)                      64 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffe (VERNEED)                    0x314
 0x6fffffff (VERNEEDNUM)                 1
 0x6ffffff0 (VERSYM)                     0x2f6
 0x6ffffffa (RELCOUNT)                   3
 0x00000000 (NULL)                       0x0

 linux还提供了查看程序主模块或一个共享库依赖哪些共享库:

phil~/repos/my_bible $ ldd ./program1
        linux-gate.so.1 =>  (0xb7734000)
        ./lib.so (0xb7731000)
        libc.so.6 => /lib/libc.so.6 (0xb7578000)
        /lib/ld-linux.so.2 (0xb7735000)
其中linux-gate.so.1比较特殊,是一个内核虚拟共享对象

动态符号表:
为了表示动态链接这些模块之间的符号导入导出关系,ELF专门有个动态符号表.dynsym。它只保存与动态链接相关的符号,对于哪些模块内部的符号,比如模块私有变量则不保存。很多动态链接模块同时拥有.symtab和.dynsym。.symtab保存了所有符号,包含.dynsym中的符号。

对应还有动态符号字符串表.dynstr和为了加快符号查找的符号哈希表.
可以使用readelf查看ELF文件的动态符号表和他的哈希表:
phil~/repos/my_bible $ readelf -sD lib.so

Symbol table of `.gnu.hash' for image:
  Num Buc:    Value  Size   Type   Bind Vis      Ndx Name
    8   0: 00001758     0 NOTYPE  GLOBAL DEFAULT ABS _edata
    9   0: 0000175c     0 NOTYPE  GLOBAL DEFAULT ABS _end
   10   1: 00001758     0 NOTYPE  GLOBAL DEFAULT ABS __bss_start
   11   1: 000003a4     0 FUNC    GLOBAL DEFAULT   9 _init
   12   2: 00000588     0 FUNC    GLOBAL DEFAULT  12 _fini
   13   2: 0000054c    57 FUNC    GLOBAL DEFAULT  11 foobar

动态链接重定位表:
共享对象需要重定位主要原因是导入符号的存在。PIC模式下的共享对象一样需要重定位。
对于PIC共享对象,他的代码段不需要重定位,但是数据段包含了绝对地址引用,因为代码段的绝对地址相关部分分离出来放在了GOT,而GOT实际是数据段的一部分。

动态链接器重定位相关结构:
在静态链接时有.rel.text表示代码段重定位段。.rel.data表示数据段重定位段。

在动态链接下.rel.dyn和.rel.plt分别相当于.rel.data和.rel.text。其中.rel.dyn是对数据段应用的修正,它所修正的位置位于.got和数据段。
而.rel.plt是对函数引用的修正,它所修正的位置位于.got.plt。

phil~/repos/my_bible $ readelf -r lib.so

Relocation section '.rel.dyn' at offset 0x344 contains 8 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00001638  00000008 R_386_RELATIVE   
0000163c  00000008 R_386_RELATIVE   
00001754  00000008 R_386_RELATIVE   
00001724  00000106 R_386_GLOB_DAT    00000000   _ITM_deregisterTMClone
00001728  00000406 R_386_GLOB_DAT    00000000   __cxa_finalize
0000172c  00000506 R_386_GLOB_DAT    00000000   __gmon_start__
00001730  00000606 R_386_GLOB_DAT    00000000   _Jv_RegisterClasses
00001734  00000706 R_386_GLOB_DAT    00000000   _ITM_registerTMCloneTa

Relocation section '.rel.plt' at offset 0x384 contains 4 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00001744  00000207 R_386_JUMP_SLOT   00000000   printf
00001748  00000307 R_386_JUMP_SLOT   00000000   sleep
0000174c  00000407 R_386_JUMP_SLOT   00000000   __cxa_finalize
00001750  00000507 R_386_JUMP_SLOT   00000000   __gmon_start__

  [20] .got              PROGBITS        00001724 000724 000014 04  WA  0   0  4
  [21] .got.plt          PROGBITS        00001738 000738 00001c 04  WA  0   0  4
  [22] .data             PROGBITS        00001754 000754 000004 00  WA  0   0  4


在静态链接中遇到了R_386_32和R_386_PC32,这里有了几种新的重定位入口类型:
R_386_RELATIVE, R_386_GLOB_DAT和R_386_JUMP_SLOT。

对于R_386_GLOB_DAT和R_386_JUMP_SLOT,被修正的位置只需要直接填入符号地址即可。比如printf这个重定位入口,类型是R_386_JUMP_SLOT,偏移是0x00001744,它实际上在.got.plt中。.got.plt的前三项被系统占据的,第四项开始才是真正存放导入函数的地址的地方。而第四项刚好是0x00001724 + 4 * 3 = 0x00001750,即__gmon_start__,,第六项是sleep,第七项是sleep
当动态链接器需要进行重定位时,先查找printf的地址,假设是0x08801234,那么链接器将这个地址填入.got.plt中偏移为0x00001744的位置去,宠儿实现了地址重定位。 
R_386_GLOB_DAT对.got的重定位跟R_386_JUMP_SLOT一样。

对于R_386_RELATIVE类型的重定位入口,实际就是基址重置,共享对象的数据段是无法做到地址无关的,它可能包含绝对地址,需要在装载时重定位。
例如
static int a;
static int *p = &a;
在编译时共享对象起始位置是0,假设静态变量a相对于起始地址0的偏移是B,那么p的值是B,一旦共享对象被装载到地址A,那么a的地址是B+A,那么p的值需要加上A。
如果ELF文件以PIC编译,并调用一个外部函数bar(),那么bar会出现在.rel.plt中,如果不是以PIC模式编译的,则bar将出现在.rel.dyn中。

phil~/repos/my_bible $ gcc -shared lib.c -o lib.so 

phil~/repos/my_bible $ readelf -r lib.so

Relocation section '.rel.dyn' at offset 0x344 contains 11 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0000053c  00000008 R_386_RELATIVE   
00001600  00000008 R_386_RELATIVE   
00001604  00000008 R_386_RELATIVE   
0000171c  00000008 R_386_RELATIVE   
00000541  00000202 R_386_PC32        00000000   printf
0000054d  00000302 R_386_PC32        00000000   sleep
000016f4  00000106 R_386_GLOB_DAT    00000000   _ITM_deregisterTMClone
000016f8  00000406 R_386_GLOB_DAT    00000000   __cxa_finalize
000016fc  00000506 R_386_GLOB_DAT    00000000   __gmon_start__
00001700  00000606 R_386_GLOB_DAT    00000000   _Jv_RegisterClasses
00001704  00000706 R_386_GLOB_DAT    00000000   _ITM_registerTMCloneTa

Relocation section '.rel.plt' at offset 0x39c contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00001714  00000407 R_386_JUMP_SLOT   00000000   __cxa_finalize
00001718  00000507 R_386_JUMP_SLOT   00000000   __gmon_start__

可以看到两个导入函数printf, sleep从.rel.plt到了.rel.dyn中,并从类型R_386_JUMP_SLOT变成了R_386_PC32。
而R_386_RELATIVE类型多了一个偏移0x0000171c,它是printf的第一个参数。在PIC时,这个字符串可以看作普通全局变量,地址可以通过PIC中的相对当前指令的位置加上固定偏移计算出来。在非PIC中,代码段采用绝对地址寻址,所以它需要重定位。


动态链接时进程堆栈初始化信息:
进程初始化时,堆栈保存了关于进程执行环境和命令行参数等信息,同时保存了动态链接器需要的一些辅助信息数组(Auxiliary Vector),
先是32位的类型值,后面32位数据部分。
AT_NULL 0 表述辅助数组结束
AT_EXEFD 2 可执行文件的文件句柄
AT_PHDR 3 可执行文件中程序头表在进程中的地址
AT_PHENT 4 可执行文件头中程序头表中每个入口大小
AT_PHNUM 5 可执行文件头中程序头表入口的数量
AT_BASE 7 表示动态链接器本身的装载地址
AT_ENTRY 9 可执行文件入口地址

它位于环境变量指针的后面
int main (int argc, char **argv)
{
    int *p = (int *)argv;
    int i;
    Elf32_auxv_t *aux;

    printf ("Argument count: %d\n", *(p-1));

    for (i = 0; i < *(p-1); ++i) {
        printf ("Argument %d: %s\n", i, *(p+i));
    }

    p += i;

    p ++;

    printf ("Environment:\n");
    while (*p) {
        printf ("%s\n", *p);
        p++;
    }

    p++;

    printf ("Auxiliary Vectors:\n");
    aux = (Elf32_auxv_t *)p;
    while (aux->a_type != AT_NULL) {
        printf ("Type: %02d Value:%x\n", aux->a_type, aux->a_un.a_val);
        aux++;
    }
    return 0;
}