什么是链接器(Linker)
首先是链接器的本质,链接器本质上也是一个程序,本质上和我经常使用的普通程序没什么不同。
最后是链接器的输出,链接器在将目标文件打包处理后,生成或者可执行文件或库等。
链接器的作用有点类似于我们经常使用的压缩软WinRAR(Linux下是tar),压缩软件将一堆文件打包压缩成一个压缩文件,而链接器和压缩软件的区别在于链接器是将多个目标文件打包成一个文件而不进行压缩。
链接器的工作过程
首先,链接器对给定的目标文件或库的集合进行符号决议以确保模块间的依赖是正确的。
其次,链接器将给定的目标文件集合进行拼接打包成需要的库或最终可执行文件。
最后,链接器对链接好的库或可执行文件进行重定位。
符号决议
符号决议有时候也被叫做符号绑定,名称决议;决议更倾向于静态链接,而绑定更倾向于动态链接。在这个过程当中,链接器需要做的工作就是确保所有目标文件中的符号引用都有唯一的定义。
目标文件里有什么
- 代码部分:指的是计算机可以执行的机器指令,也就是源文件中定义的所有函数。
- 数据部分:源文件中定义的全局变量。
那为什么局部变量没有放到目标文件的数据段当中呢?
这是因为局部变量是函数私有的,局部变量只能在该函数内部使用,所以函数私有的局部变量被放在了代码段中,作为机器指令的操作数。
编译器在遇到外部定义的全局变量或者函数时只要能在当前文件找到其声明,编译器就认为编译正确。而寻找使用变量定义的这项任务就被留给了链接器。链接器的其中一项任务就是要确定所使用的变量要有其唯一的定义。但为了让链接器工作的轻松一点编译器还是多做了一点工作的,这部分工作就是 符号表(Symbol table)。
符号表(Symbol table)
编译器在编译过程中每次遇到一个全局变量或者函数名都会在符号表中添加一项,最终编译器会统计一张符号表。
static用法:如果你认为一个变量只应该被当前文件使用而不暴露给外部,那么你就可以使用static关键字修饰一下。
本质上整个符号表只是想表达两件事:
- 我能提供给其它文件使用的符号
- 我需要其它文件提供给我使用的符号
符号表存放在哪里
静态链接下可执行文件的生成
可执行文件区别于目标文件的地方在于,可执行文件有一个入口函数,这个函数也就是我们在C语言当中定义的main函数,main函数在执行过程中会用到所有可执行文件当中的代码和数据。main函数被操作系统调用执行。
你可以把可执行文件生成的过程想象成装订一本书,一本书中通常有好多章节,这些章节是你自己写的,且一本书不可避免的要引用其它著作。静态链接这个过程就好比不但要装订你自己写的文章,而且也把你引用的其它人的著作也直接装订进了你的书里。这些工作完成后,只需要按一下订书器,一本书就制作完成啦。
在这个比喻中,你写的各个章节就好比你写的代码,引用的其它人的著作就好比使用其它人的静态库,装订成一本书就好比可执行文件的生成。
动态库
将静态链接生成可执行文件的过程比作了装订一本书,静态链接将引用的其它人的著作也装订到了书里,而动态链接可以想象成作者仅仅在引用的地方写了一句话,比如引用了“xxx”,那么作者就在引用的地方写上“此处参考“xxx”,那么读者在读到该处就会自行查找相应内容,其该过程就是动态链接的基本思想了。
因此我们就可以知道helloworld程序中的printf函数到底是在哪里定义的,答案就是该函数是在libc.so当中定义的,Linux下编译链接生成可执行文件时会默认动态链接libc.so(Windows同理),使用ldd命令可查看可执行文件的依赖项(libc.so)。因此虽然你从没有看到过printf的定义也可以正确的使用这个函数。
动态链接
动态链接可以在两种情况下被链接使用,分别是load-time dynamic linking(加载时动态链接) 以及 run-time dynamic linking(运行时动态链接)。
把加载理解为程序从磁盘复制到内存的过程,加载时动态链接就出现在这个过程Windows下比较常见的启动错误问题,就是因为没有找到依赖的动态库,如下图:
加载时动态链接
- 阶段一,将动态库信息写入可执行文件。在编译链接生成可执行文件时,需要将使用的动态库加入到链接选项当中;
- 阶段二,加载可执行文件时依据动态库信息进行动态链接。
为加深对加载时动态链接这个过程的理解,类比一下:沿用前几节读书的例子,我们正在读的书中引用了“xxx”,那么加载时动态链接就好比读者开始准备读这本书的时候(还没有真正的读)就把所有该书当中引用的资料著作都找齐放到一旁准备查看。在这个类比当中,开始读书前的准备工作就好比加载时动态链接。
运行时动态链接
不需要在编译链接时提供动态库信息,也就是说,在可执行文件被启动运行之前,可执行文件对所依赖的动态库信息一无所知,只有当程序运行到需要调用动态库所提供的代码时才会启动动态链接过程。
运行时动态链接就好比直接拿起一本书开始看,看到有引用的参考文献时再去找该资料。运行时动态链接更像是我们平时读书时的样子。
PS:在编译链接过程中,可以同时使用动态库以及静态库。这两种库的使用并不冲突,那么在这种情况下生成的可执行文件中,可执行文件中包含了静态库的数据和代码,以及动态库的必要信息。
动态库vs静态库
动态库优点:
方便了程序升级和bug修复。如果我们修改了动态库的代码,只需重新编译动态库即可,因为可执行文件当中仅仅保留了动态库的必要信息,重新编译动态库后这些必要都信息不会改变(只要不修改动态库的名字和动态库导出的供可执行文件使用的函数),编译好新的动态库后只需要简单的替换原有动态库,下一次运行程序时就可以使用新的动态库了。我们平时使用都客户端程序,比如:QQ,输入法,播放器,都利用了动态库的这一优点,原因就在于方便升级以bug修复,只需要更新相应的动态库就可以了。
插件的实现。我们知道动态链接可以出现在运行时(run-time dynamic link),动态链接的这种特性可以用于扩展程序能力,那么如何扩展呢?你肯定听说过一样神器,没错,就是插件。你有没有想过插件是怎么实现的?实现插件时,我们只需要实现几个规定好的几个函数,我们的插件就可以运行了,可这是怎么做到的呢,答案就在于运行时动态链接,可以将插件以动态的都方式实现。我们知道使用运行时动态链接无需在编译链接期间告诉链接器所使用的动态库信息,可执行文件对此一无所知,只有当运行时才知道使用什么动态库,以及使用了动态库中哪些函数,但是在编译链接可执行文件时又怎么知道插件中定义了哪些函数呢,因此所有的插件实现函数必须都有一个统一的格式,程序在运行时需要加载所有插件(动态库),然后调用所有插件的入口函数(统一的格式),这样我们写的插件就可以被执行起来了。
多语言编程。我们知道使用Python可以快速进行开发,但Python的性能无法同C/C++相比(因为Python是解释型语言),有没有办法可以兼具Python的快速开发能力以及C/C++的高性能呢,答案是可以的,我们可以将C/C++代码编译链接成动态库,这样python就可以直接调用动态库中的函数了。不但Python,Perl以及Java等都可以通过动态库的形式调用C/C++代码。动态库的使用使得同一个项目不同语言混合编程成为可能,而且动态库的使用更大限度的实现了代码复用。
动态库缺点:
动态库中的代码是地址无关代码(Position-Idependent Code,PIC,因此在使用动态库中的代码时程序要多做一些工作。
动态链接下的可执行文件不可以被独立运行(这里讨论的是加载时动态链接,load-time dynamic link),换句话说就是,如果没有提供所依赖的动态库或者所提供的动态库版本和可执行文件所依赖的不兼容,程序是无法启动的。动态库的依赖问题会给程序的安装部署带来麻烦。
静态库优点:
静态链接下的可执行文件由于不依赖任何库,因为部署非常方便,仅仅用一个新的可执行文件进行覆盖就可以了,因此极大的简化了系统部署以及升级。
静态库缺点:
会导致可执行文件过大,且多个程序静态链接同一个静态库的话会导致磁盘浪费的问题。