最近打算写一个链接器系列文章,记录一下最近工作和学习的心得,主要介绍一下链接器的基本知识,包括链接器的工作原理,静态链接以及动态链接等内容。 开篇先介绍一下链接器的基本功能,其他的细节会在后面的文章中继续介绍。

目录

链接器是干什么的

简单的说,链接器的作用就是把一堆目标文件转换成一个可执行文件或者共享库。一个可执行程序或者共享库可以由很多个目标文件构成,每个目标文件可以包含一个或几个模块,链接器就像是一个粘合剂一样,把各个模块“粘合”在一起形成一个完整的程序或库(库本身还是可以被链接到其他可执行文件)。

这个解释又带来几个问题,什么是目标文件,什么是可执行文件和共享库,目标文件和可执行文件以及共享库之间有什么联系呢?后面我会对这些概念做解释,这里我们只要简单的认为,可执行文件就是程序最终想要生成的文件,运行该文件就能实现程序的功能,目标文件就是组成可执行文件的一些拼图碎片,可执行文件就是通过把这些碎片拼起来组成的一个完整的程序。

那么链接器是怎么拼的呢?

编译过程

在通常开发软件的过程中,我们用一些高级语言如C/C++编写程序,然后利用编译器生成可执行文件。例如一个简单的C语言写成的程序helloworld.c:

#include<stdio.h>
int main(int argc, char** argv)
{
    printf("Hello World\n");
}

通过如下的命令就能生成可执行文件helloworld:

$ gcc helloworld.c -o helloworld
$ ./helloworld
Hello World

编译器做了什么,这条命令怎么生成这个“helloworld”可执行文件的呢?实际上可以分成四个步骤:预编译,编译,汇编和链接。

预编译

首先编译器会对helloworld.c源文件和一些头文件执行预编译,在这个例子里就是把stdio.h这个文件在helloworld.c展开来生成helloworld.i文件。 预编译的过程主要包括以下过程:处理由 “#” 开头的预编译指令,如“#ifdef”, “#include”等,将这些预编译指令或展开或删除不需要的分支,同时会删除程序的注释,添加行号和标识。通过预编译,生成的文件就不包含宏定义了,所以我们可以通过查看预编译生成的文件来确定那些宏分支是保留的。

编译

编译的过程就是对预编译生成的文件进行一系列解析输出汇编文件,这一系列过程包括词法分析,语法分析,语义分析,源代码优化,代码生成和目标代码优化等过程。对于编译的过程,这是一个“long long story”了,我不打算在这里展开了。 经过预编译和编译过程,编译器将源文件转换成了汇编文件。汇编代码是机器语言的一种表现形式,就是用人类能看懂的标识(汇编语言)来描述机器码。

汇编

汇编的过程就是将编译生成的汇编代码解析成可执行文件,这个过程没有复杂的语法和语义解析,只是简单的将汇编指令翻译成机器指令,通过汇编就生成了目标文件helloworld.o。

链接

到这读者可能有点疑惑,不是都已经生成机器指令了吗,还有链接器什么事?细心的读者可能发现了,“printf”这个函数在源文件中是没有定义的,程序要怎么跳转到这个函数呢?这正是链接器要做的事情了,事实上上述简单的编译指令已经包含了对glibc中相关库的链接,具体的链接过程比较复杂就不在这里展开了。 从这里就可以引出文章的主题“链接器”了,链接器的作用就是将编译及汇编生成的目标文件和相关的库通过一定的方法结合成一个文件,最终生成的这个文件就是可执行文件了。 为什么需要链接这个过程呢,不能把所有函数都定义好,编译汇编过程直接生成可执行文件吗?事实上一开始人们就是这么干的,早期高级语言还没有被发明出来的时候程序员们用汇编语言直接编写程序,并通过汇编器直接生成机器码在计算机里执行。随着高级语言(如C语言)的发明,人们开始研究模块化和子程序,希望将一些常用的功能包装起来以便于大家共同使用。这就要求汇编器编出来的不是直接可执行的机器码,而是一个特殊的“目标文件”,这个目标文件可以被不同的可执行文件包含,而这就需要一个专门的程序去解决这个组装不同目标文件的问题,这个专门的程序就是链接器。所以链接器就是用来把各个模块组装到一起生成可执行文件的。

共享库

前文还留下了另一个话题,链接器还可以将目标文件组装成一个共享库。那么什么是共享库,它又有什么作用呢?共享库的发明来自于对虚拟内存系统的优化,现代操作系统大多使用了虚拟内存技术。通常虚拟内存系统中总是同时跑很多进程,而人们发现有一些基本的模块几乎在所有的进程中都会用到,所以大量的进程复制了这部分相同的代码,而通过虚拟内存技术可以做到只保留这段代码的一个副本在实际内存中,虚拟内存系统通过映射让所有的进程都可以复用这块代码。 而这块共享代码就是共享库,而关于如何实现所有进程复用这块代码涉及到链接器的另一个功能“动态链接”,我将在后面的文章中继续介绍这方面的内容。

总结

好了,关于链接器的基本功能的介绍基本上就到这了,读者应该了解了链接器的基本功能了。接下来我会继续讲讲链接器是如何实现这些功能的,首先会介绍一下对链接器执行的对象“目标文件”,看看目标文件究竟长什么样,我们下篇文章继续。