静态链接是指将目标文件链接在一起形成可执行文件的过程。
静态链接过程是把各目标文件中的各段合并到可执行文件中的相应段中。
链接器为目标文件分配地址和空间。这个空间有两层含义,既包括在可执行文件中占有的空间也包括在虚拟地址中分配的空间。其中虚拟地址空间的分配关系重大。
静态链接的过程一般分两步——1、空间与地址分配。2、符号解析与重定位。
第一步就是获取段信息,合并段将它们映射到可执行文件的段表信息中。整理符号和引用并放入全局符号表中。
第二步,实际上就是链接,把目标文件中的地址呀、符号呀、数据等进行重定位然后链接。
VMA:Virtual Memory Address
LMA:Load Memory Address
链接前的VMA都是0,链接后就有实实在在的地址了。
符号地址在原来的目标文件中的每个段中都有一个偏移量,这个偏移量是固定的,所以在链接的过程中只要在虚拟地址的基础上再加上这个偏移量就是某符号在虚拟地址空间中的地址。
在空间和地址分配完成以后,链接器即将进行符号解析与重定位。本小节举了个例子,用了很多汇编代码,有些晦涩难懂。
目标文件中使用的都是虚拟地址不是物理地址,这一点很重要。目标文件的起始地址都是0。
它存储着与重定位相关的信息。
每个要被重定位的ELF段都对应一个重定位表,重定位表本身也是一个段,所以你也可以叫重定位表为重定位段。
每一个要被重定位的地方叫做重定位入口。
重定位入口的偏移表示入口在要被重定位的段中的位置。
重定位表的实质是一个Elf32_Rel的结构体数组,每个数组元素对应一个重定位入口。
重定位的过程伴随着符号解析的过程。
每个重定位的入口对应一个符号引用,链接器会查找有所有目标文件的符号表所组成的全局符号表,然后根据这个全局符号表进行重定位。
32位x86平台下的ELF文件的重定位入口所修正的指令寻址方式只有2种:绝对近址32位寻址和相对近址32位寻址。
修正的位置长度为4bytes。
经过绝对地址修正方式修正得到的地址是该符号的实际地址,而相对地址寻址方式得到的是符号与被修正位置的距离。
相同的符号定义在多个不同的目标文件中,但是类型各不相同,这说明它们不是同一个变量或者函数,因此不能对它们进行相同的操作。但是链接器只认符号不认类型,它认为它们都一样。
这种情况主要分为3种:
1、至少2个强符号类型不一致。
2、一个强符号和多个弱符号类型不一致。
3、至少2个弱符号类型不一致。
强符号是指定义在目标文件中全局性符号,包括函数和变量,显然它们如果有相同的多个,那就是重定义,这本身就会报错。
现在的编译器和链接器都支持COMMON块机制。它主要针对的对象是弱符号。如果在众多符号之中有一个符号是强符号,那么符号所占空间与强符号相同。如果弱符号大小超过强符号,编译器会发出警告。
编译器为什么不把未初始化的全局变量当做未初始化的局部静态变量处理?为什么不在bss中给它们分配空间,而非要把它们标记为COMMON类型呢?
因为编译时编译器不知道弱符号需要多大空间,所以这时无法为其在BSS中分配空间,只能当做局部静态变量处理。但是在链接的时候可以确定,所以链接以后才在BSS中分配空间。
编译器把所有未初始化的全局变量都当成COMMON类型处理,这样做是为了与强类型分开,凡是非COMMON类型的都是强类型。多个强类型的符号会发生重复定义的错误。
C++在很多时候会产生重复代码,模版是其中最具代表性的一个。模版可以在不同的编译单元被实例化成相同的类型,两个完全一样的类是完全没有必要的,一个足矣。
不解决代码重复问题会导致:
1、空间浪费。这个根本就不用解释。
2、地址容易出错。因为是多个相同的实例嘛,就会有多个指针分别指向这些实例,但是这些实例之间没差别,它们在逻辑上是同一函数,这就容易造成指针的误指。
3、指令运行效率较低。缓存机制会缓存多份重复的代码,但是程序只会用特定的一份,在这么多份相同的代码中找特定的一份不好找,成功率较低,即,缓存命中率低。
解决方案:把每个编译单元中的每个模版的不同实例分别放进不同的段中,并且对不同的单元都这样做,这样在最后链接的时候不同编译单元中的相同实例段就合并从而消除多份相同的实例。
缺点:不同的编译单元可能使用了不同的编译器版本或者优化选项,这会导致实际产生的代码不同,链接器必须选择其中一个副本。
函数级别链接:默认情况下链接器会把所有的目标文件链接在一起,不管有用的代码还是没用的代码,这会导致可执行文件很大。
所谓函数级别链接就是每个编译单元也把函数单独放进一个段中,在链接的时候只链接那些有用的函数段。
这种做法会减慢编译和链接的过程,因为段的数量增加了。
在C++中全局对象的构造在main之前完成,析构在main之后完成。
在ELF文件中有.init和.fini两个段。
init段包含了进程的初始化代码,在main之前执行。
fini段包含了进程的终止代码,在main之后执行。
C++的全局构造和析构由此实现。
把不同编译器产生的目标文件链接在一起需要特定的条件——相同的ABI(Application
Binary Interface)。
ABI:符号修饰标准、变量内存布局、函数调用方式等与二进制兼容性相关的内容。
C语言间的目标文件能否互相兼容具体决定于如下几个方面:
1、内置类型大小和存储方式。
2、组合类型大小和存储方式。
3、外部符号与用户定义的符号之间的命名方式和解析方式。
4、函数调用方式。
5、堆栈分布方式。
6、寄存器使用方式。
C++在这方面的决定因素P141+P142介绍。
C++代码不仅对于由不同编译器编译得到的目标文件不兼容,而且就算是同一编译器的不同版本编译得到的目标文件也不兼容。
这都是ABI闹的。
开发环境往往附带语言库,这些库是对系统API的封装。大部分的C语言库函数都调用了系统API,少数除外。
静态库实际上可以看成是一组目标文件的集合。
C语言中看似简单的库函数和系统中众多的API存在着依赖关系。
静态链接的过程分为三步:
1、调用C语言编译器。把C语言程序变成汇编语言程序。
2、调用汇编器。把汇编程序变成目标文件。
3、调用链接器链接成可执行文件。
WINDOWS内核其实就是一个文件——\WINDOWS\system32\ntoskrnl.exe。
虽然大多数情况下,链接器是对目标文件进行链接,但是对某些系统软件来说却不是这样的。
有几种控制链接过程的方法:
1、使用命令行来指定参数。这就像WINDOWS下的CMD和LINUX下的SHELL。
2、编译器通过把链接指令存储进目标文件中实现自动链接。
3、用链接脚本来控制连接过程。
C语言程序的执行入口其实不是main而是库中的_start。
编译器会把只打印字符串的printf优化为puts。
链接控制脚本是用一种特别的语言写成包含为数不多操作。
输入文件中的段是输入段,输出文件中的段是输出段,链接控制的过程就是从输入段到输出端的过程。
它由命令语句和赋值语句组成。
总体来讲它类似于C语言。
本节就是大概地介绍了一下链接脚本语法规则而已。
BFD(Binary File Descriptor Library)为不同目标文件格式提供统一接口,达到跨平台。
你需要做的只是操作抽象的目标文件。
现在编译器通常不直接处理目标文件,而是通过BFD。