首先说说我为什么要去读这一章。这个学期开OS的课,在Morden Operating System上读到和Process有关的内容时看到这样一句话:“Process is fundamentally a container that holds all the information needed to run a program.”当时瞬间就想到了之前在csapp上看的模棱两可的“目标可执行文件”这个概念,于是重新又把它的第7章给读了一遍。
要理解linker的作用,首先要搞明白他在整个计算机系统中处于一个什么样的位置。
关于一个程序是怎样从码农们手撕的代码变成内存中能跑起来的程序这个过程就不再过多的叙述,这篇文章只是着重的去讲一下有关linker的这一部分。我们可以看到,linker的接受的输入是若干个.o文件,简单的说就是经过汇编器编译后生成的机器码,学名叫“relocatable object file(可重定位的目标文件)”,概念相近的称呼也有“module(模块)”。而汇编器的输出,是一个名叫“executable object program(目标可执行文件)”的二进制文件,这个文件的特征就是可以直接拷贝到内存中不需做任何的更改便可以运行。那么我们研究linker的作用是什么就可以从这里入手——为了构造最终的目标可执行文件,他需要对输入若干可重定位的目标文件做哪些事情?
linker的作用主要有两个:
(1)符号解析(symbol resolution):将每个符号的定义和每个符号的引用联系起来。(就是让系统明白,当这个程序run的时候,遇到的具体的变量或函数名,他们到底来自哪个文件的定义?是自己这个?还是其他一起输入linker的文件?)
(2)重定位(relocation):把每个符号定义与存储器中的一个具体位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器的位置,从而重定位这些节。(在取得了每个符号的引用和定义的连接之后,要把符号的定义在存储器中绑定一个具体的地址)
书中对符号的解释不是太清楚,至少我一开始的时候没太理解这个概念,在这儿结合书本的内容我用自己的话来概括下我对这个概念的理解。“符号”可以分为3类:
1、由该模块定义的并且能被其他模块引用的“全局符号”。这里的“全局符号”对应于C语言中的非静态的函数和全局变量。
2、由其他模块定义的由该模块引用的“全局符号”。解释同上
3、由该模块定义的并且不能被其他模块引用的“全局符号”。对应于C语言中的静态变量,即static变量。static关键字相当于C语言中的“private”,即只能被自己这个文件(模块)使用的全局变量。
应当注意的是这里的变量全是全局变量而不是函数内部的私有变量,私有变量由运行时stack存储管理,linker对她并不感兴趣:)
那么在了解了符号的概念之后,要想具体的了解linker对可重定位的目标可执行文件做的一些事情,就要了解relocatable object file的一些结构(他是怎么记录自身的各种符号信息的?)对不对?
大家第一次看到这个图不要害怕,其实这就是汇编器(Assembler)将编译器处理的源代码文件进行进一步的编译或者说汇编之后形成的可重定位的目标可执行文件。这个文件的一个个小格子就是一个个的“节(section)”,他们存放该program的各种信息,在这里我只会解释几个我认为对理解linker作用很有必要的section。
.text:已编译程序的机器代码。
.data:已初始化的全局C变量。
.bss:未初始化的全局C变量。在这里符号只是一个占位符,它不占用任何的内存空间。
.symtab:一个符号表,存放在程序中定义和引用的函数和全部全局变量的信息。
.rel.test:存放代码的重定位条目(relocation entry)。
.rel.data:存放数据的重定位条目。
以上都属于本章的基础知识铺垫部分,理解了上述内容,就可以很容易的理解linker对可重定位的目标可执行文件所做的操作了。
1、符号解析
linker解析符号的方法是将每个符号的引用与所有输入的relocatable object file中的.symtab节中所有的符号定义中确定的一个联系起来。
1.1链接器如何解释多重定义的全局符号?
对于定义和引用都在一个module中的符号,linker的操作很简单,不需要指来指去改来改去;而真正要深入探讨的操作是对定义和引用不在同一个文件中的符号,尤其是当寻找到的符号定义有重名时。对此linker的做法是:
(1)定义强符号和弱符号的概念。函数和已初始化的变量是强符号,为初始化的变量是弱符号。
(2)设定规则。当有多重定义冲突的时候,linker遵循的规则是:
one:不允许有多个强符号定义
two:如果有一个强符号和多个弱符号定义,那么选择强符号定义
three:如果有多个弱符号定义,那么随便选择一个
1.2与静态库链接
为什么会有“静态库”(static libraries)这个概念?
首先在C语言编程中,我们需要实现丰富的功能,就要使用各种各样的函数接口。以ANSI C为例,它定义了一组广泛的标准I/O、字符串操作和整数数学函数,例如atoi、printf、scanf、strcpy、rand。他们在libc.a库中,对每个C程序来说都是可用的。如果不使用静态库,我们看看编程开发人员可以用什么其他的办法来向用户提供这些函数。
一种实现的方法是让编译器直接辨认出对函数的调用,并直接生成相应的代码——这显然是不可行的,C语言中有大量的函数,这样做显然会使得编译器的设计变得相当复杂,每次添加、修改、删除一个函数时,都需要一个新的编译器版本。虽然对于编程人员而言这样是十分方便的,因为所有的标准函数都是直接可用的。
另一种实现的方法是将所有的这些函数放到一个单独的可重定位的目标可执行文件中,它的优点是将编译器的设计与标准函数的实现分离开来,在一定程度上仍然便利编程人员。但是这样做的缺点却是每次运行程序的时候都要将该装载函数的rof文件copy到内存中去,而这样是很浪费内存空间的。而且同样将这么一大批函数赛到一个文件中,每次的维护都要重现编译整个源文件,这又是相当大的一个工作量。
何为静态库?
在Unix中,静态库以archive这种特殊的文件格式存在于磁盘中,是一组连接起来的relocatable object file的集合。
1.3链接器如何使用静态库来解析引用
维护一个基于(U,E,D)三个集合的算法
2、重定位
在这个过程中,将合并模块并为每个符号分配运行时的地址。重定位由两个步骤组成:
在这里有一个需要理解的概念是重定位条目(relocation entry)。在汇编器生成一个可重定位的目标模块时,当遇到UNDEFINED的符号,即不知道该数据或代码最终该存放到存储器的什么位置时,它就会为该符号生成一个重定位条目,即之前介绍的可重定位目标文件中的.rel.text和.rel.data两个表所记录的内容。