概述
0、链接的基本概念
链接是将各种代码和数据部分收集起来组合成为一个单一文件的过程,这个文件可被加载(拷贝)到存储器并执行。链接可以执行于编译时(传统静态链接),也就是在源代码被翻译成机器二进制代码时,也可以执行与加载时(加载时的共享库动态链接),也就是程序被加载器加载到存储器并执行时,甚至执行于运行时(运行时的共享库动态链接),由应用程序来执行。早起的计算机系统中,链接是手动执行的,在现代系统中链接是由叫做连接器的程序自动执行的。
1、编译器驱动程序
1.0、编译器驱动程序:编译系统提供的调用预处理器、编译器、汇编器和链接器来构造目标文件的程序。
过程如下图:
调用GCC驱动程序的命令行:
>gcc -O2 -g -o p <*.c filename>
分步调用翻译器(cpp、ccl、as)的命令行:
>cpp [options] <*.c filename> <*.i filename>
>ccl <*.i filename> <*.c filename> -O2 [options] -o <*.s filename>
>as [options] -o <*.o filename> <*.s filename>
调用链接器的命令行:
>ld -o p [system object files and args] <*.o filename>
运行可执行目标文件:
>./p
2、可重定位目标文件
2.0、ELF格式可重定位目标文件
ELF头:描述了生成改文件的系统的字的大小和字节顺序、帮助链接器语法分析和解释目标文件的指导信息(ELF头大小、目标文件类型、机器类型、节头部表的文件偏移以及节头部表的条目大小和数量)。所谓“条目”可以理解为某个格式的数据结构,可以比喻为通常表格的表头格式下的一条信息。
节头部表:描述各个节的位置和大小
节:一些包含特定信息的条目的集合
2.1、节
2.1.0.symtab:符号表
1)记录内容:关于符号的信息(是信息而不是变量本身)
A、由m定义并能被其他模块引用的全局符号(程序中的定义)
B、由其他模块定义并被模块m引用的全局符号(程序中声明而未定义)
C、只被模块m定义和引用的本地符号(static函数和变量)
B类符号在符号表中会有UND标记(ABS:不该被重定位的符号;COM:.bss符号,通过value值给出对齐要求),以期在链接时找到唯一定义;C类符号包括static的局部变量,编译器会给它们唯一的名字。
2).格式:
3)示例:
2.1.1、.rel:重定位表
1)记录内容:关于链接时如何修改重定位项的信息
当汇编器生成一个目标模块时,它不知道数据和代码最终运行时会被放在存储器的什么地方,也不知道它引用的外部定义的函数和数据的位置,所以无论何时当汇编器遇到对这样的位置未知对象的地址引用(直接或间接),就会产生重定位条目。
2)格式
3)示例
2.1.2、关于其他节
.text:已编译机器代码
.rodata:只读数据,包括printf中的格式串、const变量、开关语句跳转表等
.bss:不占据实际空间、仅仅是个占位符
.data:已初始化全局变量,已经为它开了所需字节的空间并放入初始值(这里指非重定向项)
.strtab:字符串表,各个符号的名字,每个名字后以NULL结束
3、链接的关键过程-------符号解析和重定位
3.0、符号解析:解析符号引用,就是保证所有被链接的.o文件中每个引用的符号有且仅有一个定义。这个过程由编译器和链接器一起完成,在编译过程中,当编译器遇到一个不在当前模块中定义的符号时,它会产生一个链接器符号表条目,交由链接器完成解析。
1) 在某个目标模块内,编译器要保证
A、不能重复定义符号,也就是每个非UND符号只能有一个(同名的static变量会被编译器重命名使其名字唯一);
B、每个引用都必须指向符号表中的一个符号(可以是UND的),也就是说使用函数或变量前必须出现定义或者声明;
2)在被链接的各个目标模块间,链接器要保证
A、每个非UND符号的名字必须是唯一的(发现重定义时要按照强弱关系取舍,若无法取舍则会报错 “重复定义……”)
B、每个UND符号都必须在其他模块找到定义,也就是在其他模块的符号表中找到相同名字而非UND的项(如果找不到定义,那么会出现正常编译而链接出错“无法解析的……”的现象)
3.1、重定位:由下面两步完成
A、重定位节和符号定义。在这一步中,链接器将所有同类型的节合并为同意类型的新的聚合节,将运行时存储器地址付给新的聚合节(赋给输入模块定义的每个节,赋给输入模块定义的每个符号),这一步完成时,程序中每个指令和全局变量就都有唯一的运行时存储器地址了。
B、重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用(依赖于重定位表中条目的指导),使它们指向正确的运行时地址。
ELF定义了十一种重定位方式,其中最基本的有两种:R_386_PC32(相对)和R_386_32(绝对)
B过程的算法如下(假定A过程已经完成):
注解:第1、2行表示在每个节s以及与每个节相关的重定位条目r上迭代地执行
第8行可以改写为(unsigned)ADDR(r.symbol)-(refaddr-*refptr) 在32位机器上*refptr一般为-4,(refaddr-*refptr) 就表示要修改处的下一条指令位置
第13行的*refptr一般存着要访问的数据结构内部的成员的偏移量
经过链接,就将可重定位目标文件合成了可执行目标文件可以在存储器中运行了,按照链接进行的阶段不同,大方向上有两种具体的实现方式——静态链接和动态链接,接下来讨论。
4、静态链接
4.0 一般可重定位目标文件的静态链接:
>gcc <*.o filename>,对编译器驱动程序命令行上出现的文件从左至右进行符号解析和重定位。
4.1、与静态库的链接
4.1.1、静态库概念的必要性:
将相关目标模块打包成为一个单独的文件,成为静态库文件,Unix中静态库是以.a作为后缀的存档文件。静态库可以用作链接器的输入,链接器构造一个可执行文件时,它只拷贝静态库里被应用程序引用的目标模块,再将目标模块合成可执行目标文件。
静态库链接,是取完全独立使用各目标模块和将所有目标模块打包成一个单独文件与主调程序链接的折中方法(分类合并),也是完全由编译器识别所需目标模块和完全人工识别所需目标模块的折中方法(人工决定使用哪个库,链接器找到使用的具体模块再拷贝出来)。完全独立使用各目标模块,若由程序员负责识别和链接,他将不得不记住各函数所在模块的名字,当这种函数很多时(比如说libc.a中提供的常用函数),将造成极大的不便;若由编译器识别,编译器的实现将会非常复杂,并且受到库更新的影响。所有目标模块打包成一个单独文件,太大,将对内存资源造成不必要的浪费,而且因为某个无关模块的更新也不得不重新编译整个文件。
使用静态库是在程序员使用库函数的便捷性和链接效率、资源利用率上尽量取平衡。
4.1.2、静态库的链接
创建静态库:ar -rcs <*.o filename>
静态库链接的过程:
4.1.3、静态库符号解析过程
在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的相同顺序来扫描可重定位目标文件与存档文件(驱动程序自动将命令行中所有.c文件翻译为.o文件。)在这次扫描中,链接器维持着一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),一个未解析的符号(即引用了尚未定义的符号)集合U,以及一个在前面输入文件中已经定义的符号集合D。初始时,E、D都是空的。
A、对于命令行上的每个输入文件f,链接器会判断f是一个目标文件还是一个存档文件。如果是前者,那么链接器把f添加到E中,修改U和D,然后继续下一个输入文件。
B、如果f是个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D。对存档文件中所有的成员目标文件反复进行这个过程,知道U和D都不在发生变化。此事,任何E之外的成员目标文件都简单地被丢弃,链接器接着处理下一个输入文件。
按从左到右的顺序处理输入文件,也就是说,每个符号引用必须保证在它的右边存在一个定义。一般讲库放在命令行靠右处,必要时可以重复库或合并库来满足依赖要求。
4.2、静态链接输出的可执行目标文件
4.2.0、ELF格式可执行目标文件
静态链接(>gcc <*.c filename> <*.o filename>)在编译过程中完成,输出可执行目标文件。
与可重定位目标文件结构相比,可执行目标文件多出了段头部表和.init节,少了.rel.data和.rel.text节,ELF头部还指出了程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.init节定义了一个小函数叫做_init,程序的初始化代码会调用它,因为可执行文件已经被重定位,所以它不再需要.rel节。
段头部表描述了可执行文件连续的片与运行时存储器空间的映射关系,也就是“可执行文件中偏移量为……的……字节片拷贝到存储器中自地址……起的……字节的空间中)
段头部表的结构如下:
4.2.1、加载可执行目标文件
>./p
通过调用某个主流在存储器中称为加载器的操作系统代码来运行这个可执行文件。加载器会按照段头部表的描述将代码和数据从磁盘拷贝到存储器中,若是静态链接,之后将跳转到程序的第一条指令或入口点来运行该程序,动态链接不一样。
5、动态链接共享库
5.0、共享库概念的必要性:
静态库存在着缺陷:程序员必须实时地了解静态库的更新情况,然后将他们的程序重新与新的静态库链接;多个程序的可执行目标文件中可能含有相同库文件的拷贝,这样不必要的重复浪费了存储器资源。共享库解决了静态库的这些缺陷。
共享库是一个目标模块,在unix中一般以.so作为后缀。在运行时,共享库可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程称为动态链接,是由一个叫动态链接器的程序来执行的。需要注意的是,由于库是“共享”的,共享库和程序的目标模块载入存储器的时间可能相差相当长,可能程序载入之时共享库已经在存储器中(被其他程序使用),也可能程序先载入,再由动态链接器载入共享库,因而链接是在运行时(而非存储器外的编译时)进行的,是在存储器中进行的。5.1、动态链接共享库的过程:
符号解析在ld处完成,输出部分链接的可执行文件p2,剩余的链接工作由动态链接器完成。当p2被加载到存储器中时,加载器不再将控制传递给应用,而是加载和运行动态链接器,动态链接器将完成下面的重定位任务:
A、将共享库的文本和数据载入一个存储器段(如果共享库本不再存储器中的话)。
B、重定位p2对共享库符号的引用(这是问题的关键,记住现在p2已经在存储器中了,.text是可读可执行不可写的,那么怎样重定位呢?见5.3)
5.2、与位置无关的代码(PIC)
为了解决共享库重定位的问题,一个简单的想法是给每个共享库专用的地址空间片。但这种方法的缺陷是对地址的使用效率不高,即使一个进程不适用这个库,那部分空间还是会被分配出来;其次难于管理,当一个库修改时,必须确认它的已分配片还适合它,否则就要重新安排专用地址空间片。
一种更好的情况是通过某种机制使不需要链接器修改代码就可以在任何地址加载和执行库代码,即位置无关代码(PIC),模块内的调用是PC相对的,本身即是PIC,难点在处理引用模块外定义符号的代码。
可以使用GOT(全局偏移量表)来实现PIC,GOT是在.data部分的,在存储器中可以被动态链接器修改,从而实现重定位。
书上具体介绍了使用GOT来实现PIC的数据引用和符号调用的方式,在原书(第二版)P471,这里就不拷上来了:)。
最后
以上就是纯情蜡烛为你收集整理的《深入理解计算机系统》第七章链接 读书笔记的全部内容,希望文章能够帮你解决《深入理解计算机系统》第七章链接 读书笔记所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复