深入理解计算机系统——链接

链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程。当我们构建一个大型程序的时候,不需要将其组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

在学习CSAPP第一章的时候有这么一个图:
深入理解计算机系统——链接
在这里,我们通过了解可重定位目标文件及可执行目标文件的格式及组成,来了解程序是如何链接的。

静态链接

静态链接是链接器在链接时将可重定位目标文件组合起来,形成一个可执行目标文件。为了构造可执行文件,链接器必须完成两个主要任务: 符号解析和重定位。

  • 可重定位目标文件
    深入理解计算机系统——链接
    其中:
    .text:已编译程序的机器代码。
    .data:已初始化的全局C变量。局部C变量在运行时保存在栈中,既不出现在.data节中,也不出现在.bss节中。
    .bss:未初始化的全局C变量。
    .symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
    .rel.text:一个.text节中位置的列表。重定位时需要修改。
    .rel.data:被模块引用或定义的任何全局变量的重定位信息。

  • 符号解析
    这里的符号,即代码中的变量或方法。
    每个可重定位目标模块m都有一个符号表,它包含m所定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:

    • 全局:由m定义并能被其他模块引用的全局符号。
    • 外部:由其他模块定义并被模块m引用的全局符号。
    • 本地:只被模块m定义和引用的本地符号。

    符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。符号表中包含一个条目的数组,每个条目的格式如下:
    深入理解计算机系统——链接
    条目中包含每个符号的各种信息。

    当我们遇到多重定义的全局符号怎么办呢?首先全局符号被分为强或弱,这个信息包含在符号表中,函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。根据强弱符号的定义,Unix链接器使用下面的规则来处理多重定义的符号:

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

    如果链接的程序定义了多个强符号,则会报错。但是规则2和3会导致一些不易察觉的错误:
    深入理解计算机系统——链接
    函数f将x的值由15213改为15212,这会给main函数的作者带来意外。

  • 静态库
    这里引入一个库的概念,编译系统提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库,其文件后缀为.a,里面包含多个.o文件。它可以用做链接器的输人,当链接器构造一个输出的可执行文件时,它只拷贝静态库里被应用程序引用的目标模块。
    深入理解计算机系统——链接

  • 重定位
    重定位将输入的模块合并,并为每个符号分配运行时地址。其实就是在处理.text与.data.bss的联系关系。重定位由两步组成:

    • 重定位节和符号定义。链接器将所有相同类型的节合并(.data.bss)。例如,所有输人模块的.data节被合成为输出可执目标文件的.data节。然后,链接器将运行时存储器地址分配给新的节。这样,程序中的每个指令和全局变量都有唯一的运行时存储器地址了。
    • 重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。即根据.rel.text与.rel.data中的重定位条目来修改.symtab符号表。

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

  • 可执行目标文件
    深入理解计算机系统——链接
    因为可执行文件是完全链接的(已被重定位了),所以它不再需要.rel节。

  • 加载可执行目标文件
    当我们运行可执行目标文件时,Unix外壳通过调用某个驻留在存储器中称为加载器(loader)的操作系统代码来运行它。任何Uhix程序都可以通过调用execve函数来调用加载器。加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序拷贝到存储器并运行的过程叫做加载(loading)。
    深入理解计算机系统——链接

动态链接

静态链接有一些缺点,比如浪费内存和磁盘空间、模块更新困难等,动态链接可以解决这些问题。

深入理解计算机系统——链接

动态链接就是在运行的时候再去链接。为了使得动态库在内存中只有一份,需要做到不管动态库装载到什么位置,都不需要修改动态库中代码段的内容,从而实现动态库中代码段的共享。而数据段中的内容需要做到进程间的隔离,因此必须是私有的,也就是每个进程都有一份。动态库把代码段中变化的部分放到数据段中去,这样代码段中剩下的就是不变的内容,就可以装载到虚拟内存的任何位置。
而我们需要数据段生成的代码与位置无关,就想到了用偏移量来表示。编译器通过运用以下事实来生成对全局变量的位置无关的引用:无论我们在存储器中的何处加载一个目标模块(包括共享目标模块),数据段总是被分配成紧随在代码段后面。

至此,动态库的内容被分为变与不变的两部分,那么对外部函数和变量的引用等地址会变化的部分。类似于静态链接,通过重定位来找到正确的地址。

编译器在数据段开始的地方创建了一个表,叫做全局偏移量表(GOT)。GOT是.data节的一部分。PLT是.text节的一部分。对于外部变量,动态链接器从各个动态库中可以知道每个库都提供什么函数(符号表)和哪些函数引用需要重定位(重定位表),然后修正.got和.got.plt中的符号到正确的地址。为了提高效率,一般采用的是延迟绑定,也就是只有用到某个函数才去修正.got.plt中地址。