链接
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被夹在到内存中运行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
链接时由链接器程序自动执行的。链接器在软件开发中扮演这一个关键的角色,因为它们使得分离编译称为可能。
传统静态链接,加载时共享库的动态链接,以及运行时的共享库的动态链接。
编译器驱动程序
大多数编译系统提供编译器驱动程序,它代码用户在需要时调用语言预处理器,编译器,汇编器和链接器。
//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; }
静态链接
- 像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.c
和ar 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.o
和libvector.a
:gcc -c main2.c
和gcc -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
函数引用了两个全局符号,array
和sum
。为每个引用,汇编器都会产生一个重定位条目。- 函数
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.so
和libvector.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]
开始的条目调用用户代码调用的函数。
- 过程链接表: PLT是一个数组,其中每个条目是16字节。
库打桩机制
- 库打桩机制允许你截获对共享库函数的调用。取而代之执行自己的代码。
- 基本思想: 给定一个需要打桩的目标函数,创建一个包装函数,它的原型和目标函数完全一样。使用某种特殊的打桩机制,你就可以欺骗系统调用包装函数而不是目标函数了。包装函数会执行它自己的逻辑,然后调用目标函数,再将目标函数的返回值传递给调用者。
处理目标文件的工具
ar
: 创建静态库,插入,删除,列出和提取成员。strings
: 列出一个目标文件中的所有可打印的字符串。strip
: 从目标文件中删除符号表信息。nm
: 列出一个目标文件的符号表中定义的符号。size
:列出一个目标文件中节的名字和大小。readelf
:显示一个目标文件的完整结构,包括ELF头中编码的所有信息。包含size
和nm
的功能。objdump
:所有二进制工具之母。能够显示一个目标文件中所有的信息。它的最大作用就是反汇编.text
节中的二进制指令。ldd
:列出一个可执行文件在运行时所需要的共享库。
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/csapp/%E9%93%BE%E6%8E%A5/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。