链接

静态链接

静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可知陷阱目标文件作为输出。

输入的可重定位目标文件有各种不同的代码和数据节组成,每一节都是一个连续的字节序列。

为了构造可执行文件,连接器必须完成两个主要任务:

符号解析,目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用正好和一个符号定义关联起来。

重定位,编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个富豪定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。

 可重定位目标文件

目标文件有三种以下形式:

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

编译器和汇编器生成可重定位目标文件(包括共享目标文件)。而链接器生成可执行目标文件。如图:

现代x86-64 Linux和Unix系统的目标文件采用可执行可链接格式(Executable and Linkable Format,ELF),下图是一个ELF可重定位目标文件的格式:

其中:

  • ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小(即字长)和字节顺序(大端还是小端),ELF头剩下的部分包括节头部表的偏移、大小以及表项的个数。
  • .text:编译后的代码部分
  • .rodata:只读数据,如 printf 格式串、switch 跳转表等
  • .data:已初始化的全局变量
  • .bss:未初始化全局变量,仅是占位符,不占 据任何实际磁盘空间(区分初始化和非 初始化是为了空间效率)
  • .symtab:存放函数和全局变量 (符号表)信息 , 它不包括局部变量
  • .rel,text:.text节的重定位信息,用于重新修改代 码段的指令中的地址信息
  • .rel,data:.data节的重定位信息,用于对被模块使 用或定义的全局变量进行重定位的信息
  • .debug:调试用符号表 (gcc -g)
  • .line:原始C源程序中的行号和.text节中机器指令之间的映射(gcc -g)
  • .strtab:包含symtab和debug节中符号及节名
  • 节头部表:用于存放每个节的节名、偏移以及大小

符号和符号表

在链接器的上下文中有三种不同的符号:

  1. 全局符号:由一个模块定义并能被其他模块引用,全局连接i去符号对应于非静态的C函数和全局变量。
  2. 外部符号:由其他模块定义并被另一个模块引用的全局符号,对应于在其他模块中定义的非静态C函数和全局变量。
  3. 局部符号:只能被一个模块定义和引用,对用于带static属性的C函数和全局变量。

.symtab节中包含ELF符号表。这张符号表包含一个条目的数组,数组中的每个条目都是属于同一个类型的结构体数据,每个结构体中包含这个符号的名称、地址以及大小等。

在可重定位目标文件中还有三个特殊的伪节,它们在节头部表中是没有条目的,ABS代表不该被重定位的符号;UNDEF代表未定义的符号,也就是在本目标模块中引用,但是却在其他地方定义的符号;COMMON表示还未被分配位置的未初始化数据目标。COMMON节和.bss节的区别在于:

  • COMMON节中表示未初始化的全局变量
  • .bss节中表示未初始化的静态变量,以及初始化为0的全局或静态变量

符号解析

链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。

对那些和引用定义在相同模块中的局部符号的引用,编译器只允许每个模块中每个局部符号有一个定义。静态局部变量也会有本地链接器符号,编译器还要确保它们拥有唯一的名字。

对全局符号的引用解析,当编译器遇到一个不是在当前模块中定义的符号时,会假设该符号是在其他某个模块中定义的,生成一个链接器符号表目,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用符号的定义,就输出一条错误信息并终止。

而对于多重定义的全局符号,编译器会汇编器输出每个符号是强(strong)还是弱(weak),而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。编译器处理多重定义的符号的规则如下:

规则1:不允许有多个同名的强符号

规则2:如果有一个强符号和多个弱符号同名,那么选择弱符号

规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个

与静态库链接

实际中,所有编译系统都提供一种机制,将所有相关的目标模块都打包成为一个单独的文件,称为静态库(static library),它可以用作链接器的输入。当链接器构造一个输出的可执行文件时,它只是复制静态库里被引用的目标模块。

将一个模块链接到可执行文件中:gcc main.c  /usr/lib/libc.o

在Linux系统中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中。

链接器维护一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),一个未解析的符号(即引用了但是尚未定义的符号)集合U,以及一个在钱买你输入文件中已定义的符号集合D。初始时,这几个集合都为空。

关于库的一般准则是将它们放在命令行的结尾。比如,假设foo.c调用libx.a和libz.a中的函数,而这两个库又调用liby.a中的函数,那么命令行中libx.a和libz.a必须处在liby.a之前:

gcc foo.c libx.a libz.a liby.a

如果满足依赖需求,而可以在命令行上重复库。比如,假设foo.c调用libx.a的函数该库又调用liby.a中的函数,liby.a又调用libx.a中的函数,那么libx.a必须在命令行上重复出现:

gcc foo.c libx.a liby.a libx.a

 重定位

一旦链接器完成了符号解析这一步,就把代码中的每个符号引用和一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。这样,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。

然后就开始重定位,在这个步骤中,将合并的输入模块,并未每个符号分配运行时地址。

重定位由两步组成:

  1. 重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一个类型的新的聚合节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。这一步完成后,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
  2. 重定位节中的符号引用:在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。

当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,用于告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化的数据的重定位条目放在.rel.data中。

两种基本的重定位类型:

  1. R_X86_64_PC32:重定位一个使用32位PC相对地址的引用。一个PC的相对地址就是距程序计数器的当前运行时值的偏移量。当CPU执行一条使用PC相对寻址的指令时,它就将指令中编码的32位值加上PC的当前运行时值,得到有效地址,PC值通常是下一条指令在内存中的地址。
  2. R_X86_64_32:重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在命令中编码的32位值作为有效地址,不需要进一步修改。
posted @ 2019-07-20 14:53  2hYan9  阅读(254)  评论(0编辑  收藏  举报