链接;将不同部分的代码和数据收集和组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器执行了。
链接是由叫链接器(linker)的程序自动执行的。

  • 静态链接
  • 加载时的共享库的动态链接
  • 运行时的共享库的动态链接

编译器驱动程序

预处理器(cpp)、编译器(ccl)、汇编器(as)、链接器(ld)

shell调用一个在操作系统中叫加载器(loader)的函数,它拷贝可执行文件p中的代码和数据到存储器,然后将控制器转移到这个程序开头。

静态链接

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

为了创建可执行文件,链接器必须完成两个主要任务:

  • 符号解析:目标文件定义和引用符号,符号解析的目的是将每个符号引用和符号定义联系起来
  • 重定位:编译器和汇编器生成从地址零开始的代码和数据节,链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用,使得他们指向这个存储器位置,从而重定位这些节。

目标文件

目标文件有三种形式:

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

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

可重定位目标文件

os-链接-1.png

  • ELF头(ELF header)以一个16字节的序列开始,这个序列描述了字的大小和生成该文件的系统的字节顺序。其中包括ELF头的大小,目标文件类型(比如可重定位、可执行或者共享的),机器类型(IA32),节头部表的文件偏移,以及节头部表中的表目大小和数量。
  • .text:已编译程序的机器代码
  • .rodata:只读数据,比如switch语句的跳转表
  • .data: 已初始化的全局变量
  • .bss:未初始化的全局变量
  • .symtab:符号表,它存放在程序中被定义和引用的函数和全局变量的信息。
  • .rel.text:当链接器把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改。
  • .rel.data:被模块定义或引用的任何全局变量的信息
  • .debug:一个调试符号表,其有些表目是程序中定义的局部变量和类型定义。
  • .line:原始C源程序中的行号和.text节中机器执行之间的映射
  • .strtab: 一个字符串表,包括.symtab

符号和符号表

C程序员使用static属性在模块内部隐藏变量和函数声明,就像用public 和 private声明一样,使用static属性的全局变量和函数时私有的,反之公共的。

os-链接-2.png
os-链接-3.png

GNU READELF 工具展示重复定位目标文件内容

符号解析

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

编译器还确保静态本地变量,它们也会有本地链接器符号,拥有唯一的名字。

C++/java的方法重载,编译器将每个唯一的方法和参数列表组合编码成一个对链接器来说唯一的名字,这种编码过程叫做毁坏,而相反的过程叫做恢复。

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

在编译时,编译器输出每个全局符号给汇编器,或者是强,或者是弱,而汇编器把这个消息隐含地编码在可重复定位文件的符号表里。

函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。

根据强弱符号的定义,unix链接器使用下面的规则来处理多处定义的符号:

  • 规则1: 不允许有多个强符号
  • 规则2:如果有一个强符号和多个弱符号,那么选择强符号
  • 规则3:如果有多个弱符号,那么从这些弱符号中任意选择一个

os-链接-4.png

与静态库链接

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

AR工具打包成静态库文件

os-链接-5.png

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

如果在命令中,定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接会失败。

重定位

重定位:合并输入模块,并为每个符号分配运行时地址,分两步

  • 重定位节和符号定义:在这一步中,链接器将所有的相同类型的节合并为同一类型的新的聚合节。
  • 重定位节中的符号引用:在这一步中,链接器修改代码节和数据节中对每个符号的引用,使的它们指向正确的运行时地址。依赖于重定位表目的可重定位目标模块中的数据结构。

重定位表目

汇编器遇到对最终位置未知的目标引用时就会生成一个重定位表目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位表目放在.relo.text中,已初始化数据的重定位表目放在.relo.data中。

os-链接-6.png

可执行目标文件

典型的ELF可执行文件的各类信息
os-链接-7.png

加载可执行目标文件

加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条指令,即入口点来运行该程序,这个将程序拷贝到存储器并运行的过程叫做加载。

os-链接-8.png

动态链接共享库

共享库致力于解决静态库缺陷的现代创新产物。共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并在存储器中和一个程序链接起来。这个过程称为动态链接,是由一个叫动态链接器的程序来执行的。

微软的操作系统大量地利用了共享库,它们称为DDL(动态链接库)。

对于一个共享库只有一个.so文件,创建共享库libvector.so:
os-链接-5.png

-fPIC选项指示编译器生成与位置无关的代码
-shared选项指示链接器创建一个共享的目标文件

unix> gcc -o p2 main2.c ./libvertor.so

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

与位置无关的代码(PIC)

编译库代码,使得不需要链接修改库代码,就可以在任何地址加载和执行这些代码,这样的代码叫做与位置无关的代码(PIC)。

处理目标文件的工具

os-链接-5.png

总结

链接可以在编译时静态编译器来完成,也可以在加载时和运行时由动态链接器来完成。链接器处理称为目标文件的二进制文件,它有三种不同的形式:可重定位的、可执行的和共享的。可重定位的目标文件由静态链接器组合成一个可执行的目标文件,它可以加载到存储器中并执行。共享目标文件(共享库)是在运行时由动态链接器链接和加载的,或者隐含地调用程序被加载和开始执行时,或者根据需要在程序调用dlopen库的函数时。

链接器的两个主要任务是符号解析和重定位。符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终存储器地址,并修改时对那些目标的引用。