前言:
这两天抓紧把《CSAPP》上学到的知识做一个总结,完成这个月四篇博客的任务。另外python有时间可以再深入学习一番。链接这一部分我一直期待着,平时编程的时候出现了太多链接错误,重定义错误,在这里我要彻底弄个明白。
我的github:
我实现的代码全部贴在我的github中,欢迎大家去参观。
https://github.com/YinWenAtBIT
第七章:链接
连接器作用:
一、链接执行的时候:
1. 编译时:
也就是源代码被翻译成机器代码时候,链接器用来链接各个二进制可重定位目标文件,最终生成可执行文件。
2. 加载时:
程序被加载器加载到存储的时候,进行链接。
3. 运行时:
由应用程序进行链接。
二、为什么需要链接器:1.链接器可以使得分离编译成为可能:
我们可以把一个大的应用程序分解成许多小的,更好管理的模块,单独编译每一个模块。并且在修改的时候,只用编译被修改的模块,重新链接就行。能节省大量时间。
三、理解链接过程的好处:
1. 帮助更好的构造大型程序:
在遇上缺少模块,缺少库,或者不兼容的库引起的链接错误时,能更好的解决这个问题。
2. 避免一些危险的编程错误:
链接器在解析符号时所作的决定可能会导致程序出错。在默认情况下,定义多个全局变量在满足一定条件下可以通过编译,而不产生任何警告信息。会让程序产生令人迷惑的行为。需要避免这种情况出现。
3. 帮助你理解语言的作用域规则是如何实现的:
例如,全局变量和局部变量之间的区别是什么,定义一个具有static属性的变量或者函数时,发生了什么。
4. 帮组理解其他重要的系统概念:
比如加载和运行程序,虚拟存储器,分页和存储器映射
5. 更好的利用共享库:
能够利用前任已经造好的轮子来简化自己的编程过程。
连接器简述:
一、静态链接:
1.静态链接器的目标:
以一组可重定位的目标文件和命令行参数作为输入。生成一个完全链接的可以加载和运行的可执行目标文件。
二、链接器的任务:
1.符号解析:
目标文件的定义和引用符号,符号解析的目的是将每个符号的引用刚好和一个符号定义联系起来。
2. 重定位:
编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义域一个存储器位置联系起来,然后修改所有对这些符号的引用,使得他们指向这个存储器位置,从而重定位这些节。
目链接器工作过程:
一、目标文件的种类:
1. 可重定位的目标文件:
包含二进制代码和数据,可以再编译时与其他可重定位目标文件合并起来,创建一个可执行的目标文件。
2. 可执行的目标文件:
包含二进制代码和数据,可以直接拷贝到存储器中运行。
3. 共享目标文件:
特殊的可重定位目标文件,可以在加载或者运行时被动态的加载到存储器并链接。
二、可重定位的目标文件:
由ELF文件格式为例
ELF文件头部表中含有我们需要的信息:
1. .text:
已经编译程序的机器代码
2. .rodata:
只读数据,比如printf语句中的格式串
3. .data:
已经初始化的全局C变量,局部变量保存在栈中,既不出现在.data中,也不出现在.bss节中。
4. .bss:
为初始化的全局C变量。在目标文件中这个节不占据实际的空间,仅仅是一个占位符。
5. .symtab:
一个符号表,存放在程序中定义和引用的函数和全局变量的信息
三、符号和符号表:
1. 由该模块定义,并且能被其他的模块引用的全局符号,非静态的C函数和不带static的全局变量。
2. 由其他模块定义,并被本模块引用的全局符号,这些符号称为外部符号
3. 只在本模块中定义和引用的本地符号,为带static的C函数和带static的全局变量。
符号表中不包含由栈管理的局部变量。
四、符号解析:
链接器解析符号引用的方法是,将每一个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来。
解析多重定义的全局符号的规则:
1. 函数和已经初始化的全局变量是强符号,未初始化的全局变量是弱符号
2.规则:
a. 不允许有多个强符号
b. 如果有一个强符号和多个弱符号,选择强符号
c. 如果有多个弱符号,在其中任选一个
由于规则b的存在,就有可能导致意向不到的结果,因此在编译时,可以加上GCC -fno-common选项调用链接器,会在遇到多重定义的全局符号时,输出一条警告。
五、与静态库链接:
1.我们可以把所有相关的目标模块打爆成为一个单独的文件,成为静态库:
它可以用作链接器的输入。当链接器构造一个可执行的文件时,它只拷贝静态库里被应用程序引用的模块。
2. 链接器如何使用静态库来解析引用:
在符号解析的阶段,链接器从左到右按照命令行中文件出现的顺序来扫描可重定位目标文件和存档文件。链接器维持一个可重定位目标文件集合E,一个未解析的符号结合U,一个再前面的输入文件中已定义的符号集合D。初始时,E,D,U都是空的。
对于输入的每一个文件,链接器判断其是一个目标文件还是存档文件:
a.目标文件f,将f添加到E中,并修改U和D来反应文件f中的符号定义和引用。
b. 存档文件f,尝试匹配U中未解析的符号和存档文件f中定义的符号,如果成功匹配了,那么就将f添加到E中,并修改U和D中的符号引用与定义。对存档文件反复进行这个过程,知道U和D不再变化。此时,任何不包含在E中的成员目标文件都被丢弃。
完成了链接之后,如果U是非空的,那么就会输出一条错误并终止。如果U是空的,那么就合并重定位E中的目标文件,输出可执行的文件。
需要注意的是,由于这个算法对于文件的输入顺序有要求,因此.a文件一般需要放在最后,并且在必要时需要重复在命令行上出现。
六、重定位:
1.重定位节和符号定义:
这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。比如,来自输入模块的.data节被合并成可执行目标文件的.data节。然后,链接器将运行时存储器地址赋给新的聚合节,赋给输入模块中定义的每个节,以及输入模块定义的每个符号。当这一步完成时,程序中每个指令和全局变量都有了唯一的运行时的存储器地址了。
2. 重定位节中的符号引用:
在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。为了执行这一步,链接器依赖与称为重定位条目的可重定位目标模块中的数据结构。
七、可执行目标文件:
1.ELF文件头部:
类似与可重定位的目标文件头部,不过多了.init节,程序的初始化代码会调用该节中的_init函数。
2.加载可执行目标文件:通过加载器将可执行文件的代码和数据拷贝到存储器中,然后通过跳转到程序的第一条指令,或者入口点来执行该程序。这个过程叫做加载。
3.程序运行时存储器映像:
八、动态链接共享库:
1.共享库用来解决存储器浪费:
共享库可以在运行时,加载到任意的存储器地址,并和一个再存储器运行的程序链接起来,这个过程称为动态链接。由一个动态链接器完成的。链接的结果如上图所示。
2. 链接的过程:
在创建可执行文件时,静态执行一些链接(拷贝了一些重定位和符号信息),然后在程序加载时,动态完成链接。
总结: