CSAPP 链接

《深入理解计算机系统》第7章笔记

链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载到存储器并执行。链接可以执行于编译时(compile time),也可以执行于加载时(load time),甚至执行于运行时(run time)。

链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译(separate compilation)成为可能。

编译器驱动程序

大多数编译系统提供编译驱动程序(compiler driver),为用户根据需求调用语言预处理器、编译器、汇编器和链接器

静态链接

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

enter description here

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

  • 符号解析(symbol resolution),将一个符号引用和一个符号定义结合起来
  • 重定位(relocation),编译器和汇编器生成从地址 0 开始的代码和数据节。链接器通过把每个符号定义域一个存储器位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器位置,从而重定位这些节。

目标文件纯粹是字节块的集合。这些块中,有些包含程序代码,有些则包含程序数据,而其他的则包含指导链接器和加载器的数据结构。链接器将这些块连接起来,确定被连接块的运行时位置,并且修改代码和数据块中的各种位置。链接器对目标机器了解甚少。产生目标文件的编译器和汇编器已经完成了大部分工作。

目标文件

目标文件有三种形式:

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

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

可重定位目标文件

典型的 ELF 可重定位目标文件的格式。ELF 头(ELF header)以一个 16 字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。

enter description here

符号和符号表

每个重定位的目标模块m都有一个符号表,包含m所定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:

  1. 有m定义并能够被其他模块引用的全局符号
  2. 由其他模块定义并被m所引用的全局符号
  3. 只被m定义和引用的本地符号

C 程序员使用 static 属性在模块内部隐藏变量和函数声明。

符号解析

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

当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时,它会假设符号是在其他模块中定义的,生成一个链接器符号表表目,并把它交给链接器处理。

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

编译器输出每个全局符号给汇编器,或者是强(stong),或者是弱(week),而汇编器把这些信息隐含得编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。

根据强弱符号的定义,Unix使用一下规则来处理多处定义的符号:

  1. 不允许有多个强符号
  2. 如果有一个强符号和多个弱符号,则选择强符号
  3. 如果有多个弱符号,则从中随机选择一个

与静态库链接

所有编译系统都提供了一种机制,将所有相关的目标模块打包成一个单独的文件,成为静态库(static library),它也可以作为链接器的输入。

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

enter description here

重定位

一旦链接器完成符号解析这一步,就把代码中每一个符号的引用和确定的一个符号定义结合起来。链接器就知道他输入目标模块中的代码节和数据节的确定大小。后面就是重定位的步骤了,

重定位由两个部分组成:

  1. 重定位节和符号定义
  2. 重定义节中的符号引用,链接器修改代码节和数据节中的每一个符号引用,使得他们指向正确的地址

重定向表目

无论何时汇编器遇到对最终位置未知的目标引用,他就会生成一个重定位表目(relocation entry),告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。

有两种最基本的重定位类型:

  • R_386_PC32 : 重定位一个使用32位PC相关的地址引用
  • R_386_32 : 重定位一个使用32位绝对地址的引用

重定位符号表引用

可执行目标文件

一个典型的ELF可执行文件中的各类信息:

enter description here

加载可执行文件

每个Unix程序都有一个运行时的存储器映像:

enter description here

动态链接共享库

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

共享库也称为共享目标(shared object),在 Unix 系统中通常用 .so 后缀来表示。微软的操作系统大量地利用了共享库,它们称为 DLL。

使用共享库来动态链接

小结

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

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

静态链接器是由像 GCC 这样的编译驱动器调用的。它们将多个可重定位目标文件合并成一个单独的可执行目标文件。多个目标文件可以定义相同的符号,而链接器用来悄悄地解析这些多重定义的规则可能在用户程序中引入的微妙错误。

多个目标文件可以被连接到一个单独的静态库中。链接器用库来解析其他目标模块中的符号引用。许多链接器通过从左到右的顺序扫描来解析符号引用,这是另一个引起迷惑的链接时错误来源。

加载器将可执行文件的内容映射到存储器,并运行这个程序。链接器还可能生成部分链接的可执行目标文件,这样的文件中有对定义在共享库中的程序和数据的未解析的引用。在加载时,加载器将部分链接的可执行文件映射到存储器,然后调用动态链接器,它通过加载共享库和重定位程序中的引用来完成链接任务。

被编译为位置无关代码的共享库可以加载到任何地方,也可以在运行时被多个进程共享。为了加载、链接和访问共享库的函数和数据,应用程序还可以在运行时使用动态链接器。