链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被夹在到内存中运行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。

链接时由链接器程序自动执行的。链接器在软件开发中扮演这一个关键的角色,因为它们使得分离编译称为可能。

传统静态链接,加载时共享库的动态链接,以及运行时的共享库的动态链接。

编译器驱动程序

  • 大多数编译系统提供编译器驱动程序,它代码用户在需要时调用语言预处理器,编译器,汇编器和链接器。

    //sum.c
    int sum(int *a, int n){
        int i,s=0;
        for(i=0; i < n; i++ ){
            s+=a[i];
        }
        return s;
    }
    //main.c
    int sum(int *a, int n);
    
    int array[2]={1,2};
    
    int main(){
        int val=sum(array,2);
        return val;
    }
    
  • $ gcc -Og -o prog main.c sum.c,图示过程如下:

静态链接

  • 像Linux LD程序这样的静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的,可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节组成,每一节都是一个连续的字节序列。
  • 为了构造可执行文件,链接器必须完成两个重要任务:
    • 符号解析: 每个符号对应于一个函数,一个全局变量或一个静态变量。符号解析的目的时将每个符号引用正好和一个符号定义关联起来。
    • 重定位: 编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。

目标文件

  • 可重定位目标文件: 包含二进制代码和数据,其形式可以在编译时与其他重定位目标文件合并起来,创建一个可执行目标文件。
  • 可执行目标文件: 包含二进制代码和数据,其形式可以被直接复制到内存并执行。
  • 共享目标文件: 一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态的加载进内存并链接。
  • 编译器和汇编器生成可重定位目标文件。链接器生成可执行目标文件。

可重定位目标文件

  • 如下图所示,展示了一个典型的ELF可重定位目标文件的格式。
  • ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。使用readelf -h main.o查看可重定位目标文件main.o的ELF头如下所示:
      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:                              REL (Relocatable file)
        Machine:                           Advanced Micro Devices X86-64
        Version:                           0x1
        Entry point address:               0x0
        Start of program headers:          0 (bytes into file)
        Start of section headers:          720 (bytes into file)
        Flags:                             0x0
        Size of this header:               64 (bytes)
        Size of program headers:           0 (bytes)
        Number of program headers:         0
        Size of section headers:           64 (bytes)
        Number of section headers:         12
        Section header string table index: 11
    
  • 夹在ELF头和节头部表之间的都是节:
    • .text: 以编译程序的机器代码。
    • .rodata: 只读数据。
    • .data: 已初始化的全局和静态C变量。
    • .bss: 未初始化的全局和静态C变量,以及所有被初始化 为0的全局或静态变量。在目标文件中这个节不占实际的空间,它仅仅是一个占位府。目标文件格式区分已初始化和未初始化时为了空间效率。
    • .symtab: 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
    • .rel.text: 一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
    • .rel.data: 被模块引用或定义的所有全局变量的重定位信息。
    • .debug: 调试符号表。
    • .line: 原始C源程序中的行号和.text节中机器指令之间的影射。
    • .strtab: 一个字符串表,其内容包括.symtab.debug节中的符号表。

符号和符号表

  • 每个可重定位目标模块m都有一个符号表,它包含了m定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:
    • 由模块m定义并能其他模块引用的全局符号。全局链接器符号对应于非静态的C函数和全局变量。
    • 由其他模块定义并被模块m引用的全局符号。这些符号称为外部符号,对应于在其它模块中定义的非静态C函数和全局变量。
    • 只被模块m定义和引用的局部符号。它们对应于带static属性的C函数和全局变量。这些符号在模块m中任何位置都可见,但是不能被其他模块引用。
  • .symtab节中包含ELF符号表。这张符号表包含一个条目的数组。可以用readelf -s prog来查看重定位目标文件的符号表,如下所示:
      Symbol table '.dynsym' contains 6 entries:
        Num:    Value          Size Type    Bind   Vis      Ndx Name
          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)
    
      Symbol table '.symtab' contains 64 entries:
        Num:    Value          Size Type    Bind   Vis      Ndx Name
          0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
          1: 0000000000000238     0 SECTION LOCAL  DEFAULT    1 
          2: 0000000000000254     0 SECTION LOCAL  DEFAULT    2 
          3: 0000000000000274     0 SECTION LOCAL  DEFAULT    3 
          4: 0000000000000298     0 SECTION LOCAL  DEFAULT    4 
          5: 00000000000002b8     0 SECTION LOCAL  DEFAULT    5 
          6: 0000000000000348     0 SECTION LOCAL  DEFAULT    6 
          7: 00000000000003c6     0 SECTION LOCAL  DEFAULT    7 
          8: 00000000000003d8     0 SECTION LOCAL  DEFAULT    8 
          9: 00000000000003f8     0 SECTION LOCAL  DEFAULT    9 
          10: 00000000000004b8     0 SECTION LOCAL  DEFAULT   10 
          11: 00000000000004d0     0 SECTION LOCAL  DEFAULT   11 
          12: 00000000000004e0     0 SECTION LOCAL  DEFAULT   12 
          13: 00000000000004f0     0 SECTION LOCAL  DEFAULT   13 
          14: 00000000000006a4     0 SECTION LOCAL  DEFAULT   14 
          15: 00000000000006b0     0 SECTION LOCAL  DEFAULT   15 
          16: 00000000000006b4     0 SECTION LOCAL  DEFAULT   16 
          17: 00000000000006f8     0 SECTION LOCAL  DEFAULT   17 
          18: 0000000000200df0     0 SECTION LOCAL  DEFAULT   18 
          19: 0000000000200df8     0 SECTION LOCAL  DEFAULT   19 
          20: 0000000000200e00     0 SECTION LOCAL  DEFAULT   20 
          21: 0000000000200fc0     0 SECTION LOCAL  DEFAULT   21 
          22: 0000000000201000     0 SECTION LOCAL  DEFAULT   22 
          23: 0000000000201018     0 SECTION LOCAL  DEFAULT   23 
          24: 0000000000000000     0 SECTION LOCAL  DEFAULT   24 
          25: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
          26: 0000000000000520     0 FUNC    LOCAL  DEFAULT   13 deregister_tm_clones
          27: 0000000000000560     0 FUNC    LOCAL  DEFAULT   13 register_tm_clones
          28: 00000000000005b0     0 FUNC    LOCAL  DEFAULT   13 __do_global_dtors_aux
          29: 0000000000201018     1 OBJECT  LOCAL  DEFAULT   23 completed.7698
          30: 0000000000200df8     0 OBJECT  LOCAL  DEFAULT   19 __do_global_dtors_aux_fin
          31: 00000000000005f0     0 FUNC    LOCAL  DEFAULT   13 frame_dummy
          32: 0000000000200df0     0 OBJECT  LOCAL  DEFAULT   18 __frame_dummy_init_array_
          33: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
          34: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS sum.c
          35: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
          36: 000000000000080c     0 OBJECT  LOCAL  DEFAULT   17 __FRAME_END__
          37: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
          38: 0000000000200df8     0 NOTYPE  LOCAL  DEFAULT   18 __init_array_end
          39: 0000000000200e00     0 OBJECT  LOCAL  DEFAULT   20 _DYNAMIC
          40: 0000000000200df0     0 NOTYPE  LOCAL  DEFAULT   18 __init_array_start
          41: 00000000000006b4     0 NOTYPE  LOCAL  DEFAULT   16 __GNU_EH_FRAME_HDR
          42: 0000000000200fc0     0 OBJECT  LOCAL  DEFAULT   21 _GLOBAL_OFFSET_TABLE_
          43: 00000000000006a0     2 FUNC    GLOBAL DEFAULT   13 __libc_csu_fini
          44: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
          45: 0000000000201000     0 NOTYPE  WEAK   DEFAULT   22 data_start
          46: 0000000000201010     8 OBJECT  GLOBAL DEFAULT   22 array
          47: 0000000000201018     0 NOTYPE  GLOBAL DEFAULT   22 _edata
          48: 00000000000006a4     0 FUNC    GLOBAL DEFAULT   14 _fini
          49: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
          50: 0000000000201000     0 NOTYPE  GLOBAL DEFAULT   22 __data_start
          51: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
          52: 0000000000201008     0 OBJECT  GLOBAL HIDDEN    22 __dso_handle
          53: 0000000000000614    27 FUNC    GLOBAL DEFAULT   13 sum
          54: 00000000000006b0     4 OBJECT  GLOBAL DEFAULT   15 _IO_stdin_used
          55: 0000000000000630   101 FUNC    GLOBAL DEFAULT   13 __libc_csu_init
          56: 0000000000201020     0 NOTYPE  GLOBAL DEFAULT   23 _end
          57: 00000000000004f0    43 FUNC    GLOBAL DEFAULT   13 _start
          58: 0000000000201018     0 NOTYPE  GLOBAL DEFAULT   23 __bss_start
          59: 00000000000005fa    26 FUNC    GLOBAL DEFAULT   13 main
          60: 0000000000201018     0 OBJECT  GLOBAL HIDDEN    22 __TMC_END__
          61: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
          62: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@@GLIBC_2.2
          63: 00000000000004b8     0 FUNC    GLOBAL DEFAULT   10 _init
    
    • name是符号表中的字节偏移,指向符号的以null结尾的字符串名字。
    • value是符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址。
    • size是目标的大小
    • type通常要么是数据,要么是函数。
    • binding字段表示符号是本地的还是全局的。
    • 每个符号都被分配到目标文件中的某个节,由section字段表示,该字段也是一个到节头部表的索引。由三个特殊的伪节,它们在节头部表中没有条目。ABS代表不该被重定位的符号;UNDEF代表未定义的符号;COMMON表示还未初始化的数据目标。对于COMMON符号,value字段给出对齐要求,size给出最小的大小。只有可重定位目标文件才有这些伪节。
      • COMMON: 未初始化的全局变量
      • .bss: 未初始化的静态变量,以及初始化为0的全局或静态变量。
  • readelf -s main.o结果如下
      Symbol table '.symtab' contains 12 entries:
        Num:    Value          Size Type    Bind   Vis      Ndx Name
          0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
          1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
          2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
          3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
          4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
          5: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
          6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
          7: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
          8: 0000000000000000     8 OBJECT  GLOBAL DEFAULT    3 array
          9: 0000000000000000    33 FUNC    GLOBAL DEFAULT    1 main
          10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
          11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND sum
    
    • 开始的8个条目是链接器内部使用的局部符号。
    • 全局符号main定义的条目,是一个位于.text节中偏移量为0出的33个字节函数
    • 全局符号array的定义,是一个位于.data节中偏移量为0处的8字节目标。
    • 最后一个条目是来自对外部符号sum的引用。
    • readelf用一个整数索引来标识每个节。Ndx=1表示.text节,Ndx=3表示.data节。

符号解析

  • 链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。
  • 定义和引用在相同模块中的局部符号,编译器只允许每个模块中每个局部符号有一个定义。静态局部变量也会有本地链接器符号,编译器还要确保它们拥有唯一的名字。
  • 对于全局符号的引用解析比较麻烦。当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时,会假设该符号是在其他某个模块中定义的,生成一个链接器符号表条目,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用符号的定义,就会输出一条错误信息并终止。
  • 多个目标文件可能会定义相同名字的全局符号。在这种情况下,链接器必须要么标志一个错误,要么以某种方法选出一个定义并抛弃其他定义。

链接器如何解析多重定义的全局符号

  • 链接器的输入是一组可重定位目标模块。如果多个模块定义同名的全局符号,Linux编译系统采用下面的方法:
    • 在编译时,编译器向编译器输出的每个全局符号,或者是强或者时弱,而汇编器把这个信息隐含地编码在可重定位目标文件的符号表中。函数和已初始化的全局变量时强符号,未初始化的全局变量时弱符号。
    • 根据强弱符号的定义,Linux链接器使用下面的规则来处理多重定义的符号名:
      • 规则1: 不允许有多个同名的强符号。
      • 规则2: 如果有一个强符号和多个弱符号同名,那么选择强符号。
      • 规则3: 如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
    • 下面是规则使用范例:
      • 下面也是同名强符号被多次定义(规则1)
        /* foo1.c */
        int main(){return 0;}
        /* bar1.c */
        int main(){return 0;}
        //同名强符号main多次定义(规则1)
      
        /* foo2.c */
        int x=15213;
        int main(){return 0;}
        /* bar2.c */
        int x=15213;
        void f(){}
        //同名强符号x被定义了2次(规则2)
      
      • 下面是规则2的使用,如果一个模块中的x未被初始化,那么链接器将安静选择在另一个模块中定义的强符号。
        /* foo3.c */
        #include <stdio.h>
        void f(void)
        int x=15213;
        int main(){
          f();
          printf("x=%d\n",x);
          return 0;
        }
      
        /* bar3.c */
        int x;
        void f(){
          x=15212;
        }
        //运行时,函数f将x的值由15213改为15212。
      
      • 如果x由两个弱符号,也会发生同样的事情(规则3)
        /* foo4.c */
        #include <stdio.h>
        void f(void);
      
        int x;
        int main(){
          x=15213;
          f();
          printf("x=%d\n",x);
          return 0;
        }
      
        /* bar4.c */
        int x;
        void f(){x=15212;}
      
    • 如何处理这种错误呢?用像GCC -fno-common标志这样的选项地调用链接器,这个选项会告诉链接器,在遇到多重定义的全局符号时,触发一个错误。或者使用-Werror选项,它会把所有的警告都变为错误。

与静态库链接

  • 所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称作静态库(static library),它可以用作链接器的输入。当链接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块。
  • 在Linux系统中,静态库以一种称为存档的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。
      /* addvec.c */
      int addcnt=0;
      void addvec(int *x, int *y, int *z, int n){
          int i;
          addcnt++;
          for(i = 0;i < n; i++){
              z[i]=x[i]+y[i];
          }
      }
    
      /* multvec.c */
      int multcnt=0;
      void multvec(int *x, int *y, int *z, int n){
          int i;
          multcnt++;
          for(i = 0; i < n; i++){
              z[i]=x[i]+y[i];
          }
      }
    
    • 创建这些函数的一个静态库,使用ar命令:$ gcc -c addvec.c multvec.car rcs libvector.a addvec.o multvec.o
    • 接下来编写一个应用,调用addvec函数:
      /* main2.c */
      #include <stdio.h>
      #include "vector.h"
    
      int x[2]={1,2};
      int y[2]={3,4};
      int z[2];
    
      int main(){
          addvec(x,y,z,2);
          printf("z=[%d %d]\n",z[0],z[1]);
          return 0;
      }
    
    • 为了创建这个可执行文件,我们要编译和链接输入文件main.olibvector.agcc -c main2.cgcc -static -o prog2c main2.o -L. -lvector.a
    • -static参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到内存并运行,在加载时无需更进一步的链接。
    • -lvector参数是libvector.a的缩写,-L.参数告诉链接器在当前目录下查找libvector.a。![](7-8.png%}

链接器如何使用静态库来解析引用

  • 在符号解析阶段,链接器从左到右按照它们在编译器驱动器程序命令行上出现的顺序来扫描可重定位目标文件和存档文件(驱动程序自动将命令行中所有的.c文件翻译为.o文件)。在这次扫描中,链接器维护一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),一个未解析的符号(即引用了但是尚未定义的符号)集合U,以及一个在前面输入文件中已定义的符号集合D。初始时,E,U和D均为空。
    • 对于命令行上的每个输入文件f,链接器会判断f是一个目标文件还是一个存档文件。如果f是一个目标文件,那么链接器把f添加到E,修改U和D来反映f中的符号定义和引用,并继续下一个输入文件。
    • 如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加入到E中,并且链接器修改U和D来反映m中的符号定义和引用。对存档文件中的所有成员目标文件都以此执行这个过程,直到U和D都不再发生变化。此时,任何不包含在E中的成员目标文件都简单地被抛弃,而链接器将继续处理下一个输入文件。
    • 如果当链接器完成对命令行上输入文件的扫描后,U是非空的,那么链接器就会输出一个错误并终止。否则,它会合并和重定位E中的目标文件,构建输出的可执行文件。
  • 这种算法会导致一个链接时错误,因为命令行上的库和目标文件的顺序非常重要。在命令行上,如果定义一个符号的库出现在引用这个符号的目标文件的前面,那么引用就不能解析,链接就会失败。
      $ gcc -static ./libvector.a main2.c
      /tmp/ccExyQV0.o: In function `main':
      main2.c:(.text+0x1f): undefined reference to `addvec'
      collect2: error: ld returned 1 exit status
    
    • 在处理libvector.a时,U是空的,所以没有libvector.a中的成员目标文件会添加到E中。因此,对addvec的引用是不会被解析的,所以链接器会产生一条错误信息并终止。

重定位

  • 一旦链接器完成了符号解析这一步,就把代码中的每个符号引用和对应的符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块的代码节和数据节的确切大小。
    • 重定位节和符号定义:链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,所有输入模块中的.data节被合并为成一个节,这个节成为输出的可执行目标文件的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块中的每个节,以及赋给输入模块定义的每个符号。
    • 重定位节中的符号引用: 链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接诶器依赖于可重定位目标模块中称为重定位条目的数据结构。

重定位条目

  • 当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置,也不知道这个模块引用的任何外部定义的函数和全局变量的位置。所以,当汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.ret.text中,已初始化数据的重定位条目放在.rel.data中。
  • 重定位条目的格式:
    • offset定义了需要别修改的引用的节偏移
    • symbol标识被修改引用应该指向的符号
    • type告知链接器如何修改新的引用
    • addend是一个由符号常数,一些类型的重定位需要使用它对被修改引用的值作偏移调整
    • 使用如下命令查看重定位条目readelf -r main.o
      Relocation section '.rela.text' at offset 0x228 contains 2 entries:
        Offset          Info           Type           Sym. Value    Sym. Name + Addend
      000000000010  000800000002 R_X86_64_PC32     0000000000000000 array - 4
      000000000015  000b00000004 R_X86_64_PLT32    0000000000000000 sum - 4
    
      Relocation section '.rela.eh_frame' at offset 0x258 contains 1 entry:
        Offset          Info           Type           Sym. Value    Sym. Name + Addend
      000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0
    
    • 使用objdump -dx main.o获得如下信息:
    
      main.o:     file format elf64-x86-64
      main.o
      architecture: i386:x86-64, flags 0x00000011:
      HAS_RELOC, HAS_SYMS
      start address 0x0000000000000000
    
      Sections:
      Idx Name          Size      VMA               LMA               File off  Algn
        0 .text         00000021  0000000000000000  0000000000000000  00000040  2**0
                        CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
        1 .data         00000008  0000000000000000  0000000000000000  00000068  2**3
                        CONTENTS, ALLOC, LOAD, DATA
        2 .bss          00000000  0000000000000000  0000000000000000  00000070  2**0
                        ALLOC
        3 .comment      0000002a  0000000000000000  0000000000000000  00000070  2**0
                        CONTENTS, READONLY
        4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  0000009a  2**0
                        CONTENTS, READONLY
        5 .eh_frame     00000038  0000000000000000  0000000000000000  000000a0  2**3
                        CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
      SYMBOL TABLE:
      0000000000000000 l    df *ABS*  0000000000000000 main.c
      0000000000000000 l    d  .text  0000000000000000 .text
      0000000000000000 l    d  .data  0000000000000000 .data
      0000000000000000 l    d  .bss   0000000000000000 .bss
      0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
      0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
      0000000000000000 l    d  .comment       0000000000000000 .comment
      0000000000000000 g     O .data  0000000000000008 array
      0000000000000000 g     F .text  0000000000000021 main
      0000000000000000         *UND*  0000000000000000 _GLOBAL_OFFSET_TABLE_
      0000000000000000         *UND*  0000000000000000 sum
    
      Disassembly of section .text:
    
      0000000000000000 <main>:
        0:   55                      push   %rbp
        1:   48 89 e5                mov    %rsp,%rbp
        4:   48 83 ec 10             sub    $0x10,%rsp
        8:   be 02 00 00 00          mov    $0x2,%esi
        d:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 14 <main+0x14>
                              10: R_X86_64_PC32       array-0x4
        14:   e8 00 00 00 00          callq  19 <main+0x19>
                              15: R_X86_64_PLT32      sum-0x4
        19:   89 45 fc                mov    %eax,-0x4(%rbp)
        1c:   8b 45 fc                mov    -0x4(%rbp),%eax
        1f:   c9                      leaveq 
        20:   c3                      retq   
    
  • ELF定义了32中不同的重定位类型:
    • R_X86_64_PC32: 重定位一个使用32位PC相对地址的引用。
    • R_86_64_32: 重定位一个使用32为绝对地址的引用。

重定位符号引用

  • 首先链接器为每个节和每个符号都选择了运行时地址。
  • 其次根据重定位类型替换掉该符号的占位符。
  Disassembly of section .text:

      0000000000000000 <main>:
        0:   55                      push   %rbp
        1:   48 89 e5                mov    %rsp,%rbp
        4:   48 83 ec 10             sub    $0x10,%rsp
        8:   be 02 00 00 00          mov    $0x2,%esi
        d:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 14 <main+0x14>
                              10: R_X86_64_PC32       array-0x4
        14:   e8 00 00 00 00          callq  19 <main+0x19>
                              15: R_X86_64_PLT32      sum-0x4
        19:   89 45 fc                mov    %eax,-0x4(%rbp)
        1c:   8b 45 fc                mov    -0x4(%rbp),%eax
        1f:   c9                      leaveq 
        20:   c3                      retq
  • main函数引用了两个全局符号,arraysum。为每个引用,汇编器都会产生一个重定位条目。
  • 函数main调用sum函数,sum函数是在模块sum.o中定义的。
  • call指令开始于节偏移0xe的地方,包含1字节的操作码0xe8,后面跟着的是对目标sum的32为PC相对地址的占位符。
  • 链接器首先修改节的运行时地址,然后根据重定位类型替换掉sum的32位占位符(相对地址或者绝对地址)。

可执行目标文件

  • 下图概括了一个典型的ELF可执行文件中的各类信息
  • 可执行目标文件的格式类似于可重定位目标文件的格式。ELF头描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。
  • .text,.rodata.data节与可重定位目标文件中的节是相似的,除了这些节已经被重定位到它们最终运行时的内存地址以外。
  • .init节定义了一个小函数,叫做_init,程序的初始化代码会调用它。
  • 因为可执行文件是完全链接的,所以它不再需要.rel节。

加载可执行目标文件

  • 要运行可执行目标文件,运行shell命令$ ./prog。因为prog不是一个内置的shell命令,所以shell会认为prog是一个可执行目标文件,通过调用某个驻留在存储器中的加载器的操作系统代码来运行它。
  • 任何Linux程序都可以通过调用execve函数来调用加载器。
  • 加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序复制到内存并运行的过程叫做加载。
  • 在Linux x86-64位系统中,代码段总是从地址0x400000处开始,后面是数据段。运行时堆在数据段之后,通过malloc库往上增长。对后面的区域是为共享模块保留的。用户栈总是从最大的合法用户地址开始,向较小内存地址增长。

动态链接共享库

  • 共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程叫做动态链接,是由一个叫做动态链接器的程序来执行的。共享库也成为共享目标,在Linux系统中通常用.so后缀来表示。Windows系统也使用共享库,称为DLL(动态链接库)
  • 共享库是以两种不同的方式来“共享”的。
    • 首先,在任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库中的内容那样被复制和嵌入到引用它们的可执行文件中。
    • 其次,在内存中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。
  • 执行命令gcc -shared -fpic -o libvector.so addvec.c multvec.c构造libvector.so-fpic选项指示编译器生成与位置无关的代码,-shared选项指示链接器创建一个共享的目标文集爱你。
  • 执行命令gcc -o prog21 main2.c ./libvector.so链接这个库,这样就创建了一个可执行目标文件prog21,因此文件的形式使得它可以在运行时可以和libvector.so链接。
  • 当加载器加载和运行可执行文件prog21时,动态链接器通过执行下面的重定位完成链接任务:
    • 重定位libc.so的文本和数据到某个内存段。
    • 重定位libvector.so的文本和数据到另一个内存段。
    • 重定位prog21中所有对由libc.solibvector.so定义的符号的引用。
    • 最后动态链接器将控制传递给应用程序。

从应用程序中加载和链接共享库

  • 已经讨论了应用程序加载后执行前时,动态链接器加载和链接共享库的情景。然而应用程序还可能在它运行时要求动态链接库加载和链接某个共享库,而无需在编译时将那些库链接到应用中。
  • Linux系统位动态链接器提供了一个简单的接口,允许应用程序在运行加载和链接共享库
      #include <dlfcn.h>
      void *dlopen(const char* filename, int flag);
      //返回: 若成功则指向句柄的指针,若出错则为NULL
    
  • dlopen函数加载和链接共享库filename,如果当前可执行文件是带-rdynamic选项编译的,那么对符号解析来说,它的全局符号也是可用的。flag参数必须要么包括RTLD_NOW,该标志告诉链接器立即解析对外部符号的引用,要么包含RTLD_LAZY,该标志指示链接器推迟符号解析直到执行来自库中的代码。
      #include <dlfcn.h>
      void *dlsym(void* handle, char* symbol);
      //返回: 若成功则为指向符号的指针,若出错则为NULL
      int dlclose(void *handle);
      const char *dlerror(void);
      //返回: 若前面对dlopen,dlsym或dlclose的调用失败,则为错误消息;若前面调用成功,则为NULL
    
  • 接口调用示例如下:
      #include <stdio.h>
      #include <stdlib.h>
      #include <dlfcn.h>
    
      int x[2]={1,2};
      int y[2]={3,4};
      int z[2];
    
      int main(){
          void *handle;
          void(*addvec)(int *,int *, int *, int);
          char *error;
    
          handle=dlopen("./libvector.so",RTLD_LAZY);
          if (!handle)
          {
              fprintf(stderr,"%s\n",dlerror());
              exit(1);
          }
    
          addvec=dlsym(handle,"addvec");
          if ((error=dlerror())!=NULL)
          {
              fprintf(stderr,"%s\n",error);
              exit(1);
          }
    
          addvec(x,y,z,2);
          printf("z=[%d %d]\n",z[0],z[1]);
          if (dlclose(handle)<0)
          {
              fprintf(stderr,"%s\n",dlerror());
              exit(1);
          }
          return 0;
      }
    

位置无关代码

  • 共享库的一个主要目的就是允许多个正在运行的进程共享内存中相同的库代码,因而节约宝贵的内存资源。那么,多个进程如何共享共享程序的一个副本呢?一种方法是给每个共享库分配一个事先预备的专用的地址空间,然后要求加载器总是在这个地址加载共享库。但是这样会造成内存的浪费,因为不是每时每刻都在使用这个共享库。
  • 现代系统以这样一种方式编译共享模块的代码段,使得可以把它们加载内存的任何位置而无需链接器修改。可以加载而无需重定位的代码称为位置无关代码(Position-Independent Code,PIC)。共享库的编译必须总是使用该选项。
  • PIC数据引用
    • 无论我们在内存的何处加载一个目标模块,数据段和代码段的距离总是保持不变。
    • 想要生成对全局变量PIC引用的编译器,在数据段开始的地方创建了一个表,叫做全局偏移量表(Global Offset Table,GOT)。在GOT中,每个被这个目标模块引用的全局数据目标(过程或者全局变量)都有一个8字节条目。编译器还为GOT中每个条目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。每个引用全局目标的目标模块都有自己的GOT。
    • addvec通过GOT[3]间接加载全局变量addcnt的地址,然后把addcnt在内存中加1。这里的关键思想是对GOT[3]的PC相对引用中的偏移量是一个运行时常量。
  • PIC函数调用
    • 假设程序调用一个由共享库定义的函数。编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以被加载到任何位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。不过这种方法并不是PIC,因为它需要链接器修改调用模块的代码段,GNU编译系统使用了一种称为延迟绑定的方法,将过程地址的绑定延迟到第一次调用该过程时。
    • 把函数地址的解析推迟到它实际被调用的地方,能避免动态链接器在加载时进行成百上千个其实并不需要的重定位。
    • 延迟绑定是通过两个数据结构之间的交互实现的,这两个数据结构是:GOT和过程链接表(Procedure Linkage Table,PLT)
    • 如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。GOT是数据段的一部分,而PLT是代码段的一部分。
      • 过程链接表: PLT是一个数组,其中每个条目是16字节。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有自己的PLT条目。每个条目都负责一个具体的函数。PLT[1]调用系统启动函数__libc_start_main,它执行初始化环境,调用main函数并处理其返回值。从PLT[2]开始的条目调用用户代码调用的函数。

库打桩机制

  • 库打桩机制允许你截获对共享库函数的调用。取而代之执行自己的代码。
  • 基本思想: 给定一个需要打桩的目标函数,创建一个包装函数,它的原型和目标函数完全一样。使用某种特殊的打桩机制,你就可以欺骗系统调用包装函数而不是目标函数了。包装函数会执行它自己的逻辑,然后调用目标函数,再将目标函数的返回值传递给调用者。

处理目标文件的工具

  • ar: 创建静态库,插入,删除,列出和提取成员。
  • strings: 列出一个目标文件中的所有可打印的字符串。
  • strip: 从目标文件中删除符号表信息。
  • nm: 列出一个目标文件的符号表中定义的符号。
  • size:列出一个目标文件中节的名字和大小。
  • readelf:显示一个目标文件的完整结构,包括ELF头中编码的所有信息。包含sizenm的功能。
  • objdump:所有二进制工具之母。能够显示一个目标文件中所有的信息。它的最大作用就是反汇编.text节中的二进制指令。
  • ldd:列出一个可执行文件在运行时所需要的共享库。