链接器在软件开发中扮演一个重要的角色,因为它使得分离编译成为可能。我们不用将一个大型的应用程序组织成为一个巨大的源文件,而是可以把他分解为更小的,更好管理的模块,可以独立的修改和编译这些模块。当我们改变这些模块的一个时,只需简单的重新编译它,并重新链接应用,而不必重新编译其他文件。
要想了解链接的机制,需要知道一个程序从编辑完代码到运行的过程(C语言程序)。
以下方的c程序为例。经过4个阶段生成可执行的二进制文件。
//hello.c
#include<stdio.h>
int main(){
printf("%s","hello.word");
return 0;
}
- 预处理阶段(cpp)
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中的 #include<stdio.h> 命令告诉预处理器读取系统头文件stdio.h的内容(只有stdio.h头文件里的内容,不包括stdio.c中的定义部分),并把它直接插入到程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
- 编译阶段(ccl)
编译器(ccl) 将文本文件hello.i翻译成由汇编语言组成的文本文件hello.s.
- 汇编阶段(as)
汇编器(as)将hello.s翻译成机器语言指令,将这些指令打包成一中叫做可重定位目标程序文件的格式,并将结果保存到目标文件hello.o中。
- 链接阶段(ld)
链接hello.c中调用的函数,比如例子中的printf函数。 printf函数存在一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的heelo.o程序中。链接器就是负责处理这种合并。 结果得到可执行目标文件hello,它可以被加载到内存中,由系统执行。
以上就是背景知识了。下面介绍链接器是如何工作的。O(∩_∩)O
一.静态链接
静态链接器的功能就是以一组可重定位目标文件为输入 生成一个可以加载的可执行目标文件。
目标文件定义:
1.可重定位目标文件---包含二进制代码和数据,可以在编译时与其他可重定位目标文件合并起来。
2.共享目标文件---一种特殊类型的1。
3.可执行目标文件---可直接执行的。
由1+【2】--》3;
为了了解链接器是如何把一组可重定位目标文件链接在一起,我们先了解下它的格式。
可重定位目标文件格式:
ELF头 | |
.text | 已编译程序的机器代码 |
.rodata | 只读数据,比如printf语句中的格式串和开关语句的跳转表 |
.data | 已初始化的全局和静态C变量 |
.bss | 未初始化的全局和静态C变量 |
.symtab | 一个符号表,存放程序中定义和引用的函数和全局变量的信息。 |
.rel.text | 一个.text节中位置的列表,链接时就是修改这个位置 |
.rel.data | 需要重定位的全局变量信息 |
.debug | 调试符号表 |
.line | 原始C程序中行号和.text节中机器指令之间的映射。 |
.startab | 一个字符串表 |
节头部表 |
为了构造可执行文件,链接器必须完成两个主要任务:
1.符号解析---将符号引用和符号定义联系起来。
2.重定位---合并各节,然后修改符号引用指向的地址。
符号解析
符号-----每个符号对应于一个函数,一个全局变量或是一个静态变量。
在建立符号联系时,链接器要确保它们拥有唯一的名字,对于局部变量很简答。但是对于全局变量就有点棘手。为此linux链接器使用下面的规则:
(强符号:已定义的全局变量,弱符号:只声明未定义的全局变量。)
规则1:不允许有多个同名强符号。
规则2:一强多弱 选强
规则3:多弱,任意选
----与静态库链接
重定位
将符号解析完,接下来就是重定位了
步骤1.将所有相同类型的节合并为同一类型的新的聚合节。
步骤2.修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。----》这一步依赖于重定位条目。
重定位条目:汇编时生成的描述如何修改引用的文件,代码重定位条目放在.rel.text中,数据重定位条目放在.rel.data中。
重定位包括 相对引用,和绝对引用。