CSAPP链接

链接是将源代码文件中的函数和全局变量等符号与其他源代码文件或库文件中的定义进行关联的过程。链接的目标是生成一个可执行文件,其中包含了所有必要的代码和数据,以便程序在运行时能够正确地执行。

7.1 静态链接

1.符号解析:目标文定义和引用符号,为了将每个符号引用和一个符号定义关联系起来。

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

(目标文件纯粹是字节块的集合,包含代码,数据,其他包含指导链接器和加载器的数据结构)

7.2 目标文件

1.可重定位目标文件:二进制代码与数据,编译时与其他可重定位文件合并起来创建一个可执行目标文件。

2.可执行目标文件:二进制代码和数据,可以直接拷贝到存储器进行执行。

3.共享目标文件:特殊形式的可重定位文件,可以在加载或者运行时被动态加载到存储器并链接。

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

分清目标模块与目标文件:字节序列—-存在磁盘文件中的目标模块。

7.3 可重定位文件

其中包括 ELF 头的大小、目标文件的类型(比如,可重定位、可执行或者是共享的)、机器类型(比如,IA32)、节头部表 (section header table) 的文件偏移,以及节头部表中的表目大小和数量。

img

.text: 已编译程序的机器代码。

.rodata: 只读数据,printf语句中的格式串…switch判断的符号。

.data: 已初始化的全局C变量,而局部变量存储在栈中。

.bss: 未初始化的全局C变量,不占空间仅仅是一个占位符…

.symtab: 一个符号表 (symbol table),存放程序中被定义和引用的函数和全局变量信息。,symtab符号表不包含局部变量的表目。

.rel.text: 当链接器把这个目标文件和其他文件结合时,.text 节中的许多位置都需要修改。(调用外部函数或者引用全局变量的指令都需要更改,而调用本地函数的指令则不需要),可执行文件不需要重定位信息。

.rel.data: 被模块定义或引用的任何全局变量的信息。

.debug: 一个调试符号表,其有些表目是程序中定义的局部变量和类型定义。

.line: 原始C 源程序中的行号和ext 节中机器指令之间的映射。

.strtab: 一个字符串表,其内容包括symtab 和debug 节中的符号表,以及节头部中的节名字。

7.4 符号和符号表

链接器中有三种符号:(对于可重定位目标模块m)

(一)由m定义并且能够被其他模块引用的为全局符号

(二)由其他模块定义并且被m引用的全局符号为外部符号

(三)只被m定义和引用的为本地符号

符号表不包含对应于本地非静态程序变量的任何符号,这些符号在运行过程中被栈管理。

static属性的本地过程变量是不在栈中管理的,编译器在.data与.bss中为每个定义分配空间,并且在符号表中创建一个有唯一名字的本地连接器符号。

符号表由编译器构造。

7.6 符号解析

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

C++与JAVA支持重载函数,因为编译器将每个唯一的方法和参数列表组合编码成一个对链接器来收唯一的名字。例如Foo:bar(int , long)编码为bar_3Fooil,类似于这种方式给予一个唯一的名字便于链接器进行分辨。

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

编译时期,编译器将输出每个全局符号给汇编器,而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。

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

符号选择规则:

1.不允许多个强符号。

2.一个强符号和多个弱符号,选择强符号。

3.多个弱符号,随机选择。

7.6.2 与静态库链接

将所有线管的目标模块打包为一个单独的文件,称为静态库,它可以用作链接器的输入。

为了避免每次都需要编译整个标准函数集合,我们会对将相关函数被编译成独立的目标模块,然后封装为一个单独的静态库文件。

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

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

在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的相同顺序来扫描可重定位目标文件和存档文件。链接器在扫描中会维持一个可重定位目标文件的集合E,这个集合中的文件会被合并起来形成可秩序文件,和一个为解析的符号(引用了但是尚未定义的符号)集合U,以及一个在前面输入文件中已定义的符号集合D。初始E、U和D全是空的。

1.对于命令行上的每个输入文件f,链接器会判断f是一个目标文件还是一个存档文件(archive)如果f是一个目标文件则将其添加到E,修改E和D来反映f中的符号定义和引用,再继续下一个输入文件。

2.如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D来反映m中的符号定义和引用。对于存档文件中所有的成员目标文件都反复进行这个过程,直到U和D都不再发生变化。此时,任何不包含在E中的成员目标文件都被丢弃,而链接器将继续到下一个输入文件。

3.当链接器完成对命令行上输入文件的扫描之后,U是非空的,那么链接器就会输出一个错误并且终止。否则,他会合并和重定位E中的目标文件,从而构建输出的可执行文件。

!!!如果符号的定义出现在引用这个符号的目标文件之前,那么就无法被解析。(库一般放末尾,若不是相互独立需要考虑先后顺序关系)

7.7 重定位

重定义分为两步:

重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自输入模块的.data 节被全部合并成一个节,这个节成为输出的可执行目标文件的.data 节。然后,链接器将运行时存储器地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每个指令和全局变量都有惟一的运行时存储器地址了。 2.重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。为了执行这一步,链接器依赖于称为重定位表目 (relocation entry)的可重定位目标模块中的数据结构,我们接下来将会描述这种数据结构。

7.7.1重定义表目

未知数据和代码将会存放在存储器的什么位置,无论合适汇编器遇到对最终位置未知的目标引用,他就会生成一个重定位表目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。

ELF 重定位表目的格式offset 是需要被修改的引用的节偏移。symbol 标识被修改引用应该指向的符号。type 告知链接器如何修改新的引用。

img

7.7.2 重定位符号引用

如何引用,偏移,绝对路径等算法进行引用

7.8 可执行目标文件

可执行文件已经完全链接的(已经被重定位),所以他就不需要.relo节。

img

7.9 加载可执行目标文件

通过调用某个驻留在存储器中称为加载器的操作系统代码来为我们运行可执行文件。加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第 1 条指令,即入口点 (entry point),来运行该程序。这个将程序拷贝到存储器并运行的过程叫做加载 (Ioading)。

在可执行文件中段头表的指导下,加载器将可执行文件的相关内容拷贝到代码和数据段。接下来,加载器跳转到程序的入口点,也就是符号start 的地址。在start 地址处的启动代码( startup code)是在目标文件ctrl.o中定义的,对所有的C程序都是一样的。展示了启动代码中特殊的调用序列。在从text 和init 节中调用了初始化例程后,启动代码调用atexit 例程,这个程序附加了一系列在应用调用exit 函数时应该调用的程序exit 函数运行 atexit 注册的函数,然后通过调用exit 将控制返回给操作系统。接着,启动代码调用应用程序的 main 程序,这就开始执行我们的C代码了。在应用程序返回之后,启动代码调用exit程序,它将控制返回给操作系统。

img

7.10 动态链接共享库

共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并在存储器中和一个程序链接起来。这个过程称为动态链接 (dynamic linking),是由一个叫做动态链接器 (dynamic linker)的程序来执行的。

(一)所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样直接被拷贝和嵌入到引用他们的可执行文件中。(二)一个共享库的.text节只有一个副本可以被不同的正在运行的进程共享。(不会拷贝任何数据和代码到可执行文件中,链接器仅仅拷贝了一些重定位和符号表信息).interp节中包含动态链接器的路径名,动态链接器本身就是一个共享目标。

img

7.12 与位置无关的代码(PIC)

一种方法是给每个共享库分配一个事先预备的专用的地址空间组块 (chunk),然后要求加载器总是在这个地址加载共享库。首先,它对地址空间的使用效率不高,因为即使一个进程不使用这个库,那部分空间还是会被分配出来。第二,它也难以管理。我们将不得不保证没有组块会重叠。每次当一个库修改了之后,我们必须确认它的已分配的组块还适合它的大小。如果不适合了,我们必须找一个新的组块。并且,如果我们创建了一个新的库,我们还必须为它寻找空间。

以更好地方法就是编译库代码,使得不需要链接器修改库代码,就可以在任何地址加载和执行这些代码,称为位置无关的代码。

7.12.1 PIC数据引用

img

需要额外的五条指令来引用全局变量。

无论我们在存储器中的何处加载一个目标模块(包括共享目标模块),数据段总是分配为紧随在代码段后面。

编译器在数据段开始的地方创建了一个表,叫做全局偏移量表(global offsettable,GOT)。GOT 包含每个被这个目标模块引用的全局数据目标的表目。编译器还为 GOT 中每个表目生成一个重定位记录。在加载时,动态链接器会重定位 GOT 中的每个表目,使得它包含正确的绝对地址。每个引用全局数据的目标模块都有一张自己的 GOT。

7.12.2 PIC函数调用

img

需要三条指令来引用全局变量。

延迟绑定:将过程地址的绑定推迟到第一次调用该过程时。第一次可能开销较大,但是后续就只需要一个寄存器和一条指令就可以完成引用。GOT(.data)和PLT(.text)