咱们继续来看动态连接。python
咱们知道静态库在编译连接期间就被打包copy到了可执行文件,也就是说静态库实际上是在编译期间(Compile time)连接使用的,那么动态库又是在何时才连接使用的呢,动态连接能够在两种状况下被连接使用,分别是load-time dynamic linking(加载时动态连接) 以及 run-time dynamic linking(运行时动态连接),接下来咱们分别讲解一下。程序员
load-time dynamic linking(加载时动态连接)编程
首先可能有的同窗会问,什么是load-time呢,load_time翻译过来也就是加载时,那么什么又是加载呢?后端
咱们你们都玩过游戏,当咱们打开游戏的时候常常会跳出来一句话:“加载中,请稍后。。。”和这里的加载意思差很少。这里的加载指的是程序的加载,而所谓程序的加载就是把可执行文件从磁盘搬到内存的过程,由于程序最终都是在内存中被执行的。至于这个过程的详解内容我会在接下来的文章《加载器与可执行文件》一文中给你们详细讲解。在这里咱们只须要简单的把加载理解为程序从磁盘复制到内存的过程,加载时动态连接就出如今这个过程。微信
当把可执行文件复制到内存后,且在程序开始运行以前,操做系统会查找可执行文件依赖的动态库信息(主要是动态库的名字以及存放路径),找到该动态库后就将该动态库从磁盘搬到内存,并进行符号决议(关于符号决议,参考符号决议一节),若是这个过程没有问题,那么一切准备工做就绪,程序就能够开始执行了,若是找不到相应的动态库或者符号决议失败,那么会有相应的错误信息报告为用户,程序运行失败。好比Windows下比较常见的启动错误问题,就是由于没有找到依赖的动态库。Linux下一样会有相似信息提示用户程序启动失败。运维
到这里,同窗们应该对加载时动态连接应该有一个比较清晰的了解了。从整体上看,加载时动态连接能够分为两个阶段:阶段一,将动态库信息写入可执行文件;阶段二,加载可执行文件时依据动态库信息进行动态连接。函数
阶段一,将动态库信息写入可执行文件性能
在编译连接生成可执行文件时,须要将使用的动态库加入到连接选项当中,好比在Linux下引用libMath.so,就须要将libMath.so加入到连接选项当中(好比libMath.so放到了/usr/lib下,那么使用命令 gcc ... -lMath -L/user/lib ... 进行编译连接),因此使用这种方式生成的可执行文件中保存了依赖的动态库信息,在Linux可以使用一个简单的命令ldd来查看。spa
阶段二:加载可执行文件时依据动态库信息进行动态连接操作系统
因为在阶段一辈子成的可执行文件中保存了动态库信息,当可执行文件加载完成后,就能够依据此信息进行中动态库的查找以及符号决议了。
经过这个过程也能够清楚的看到静态库和动态库的区别,使用动态库的可执行文件当中仅仅保留相应信息,动态库的连接过程被推迟到了程序启动加载时。
为加深你对加载时动态连接这个过程的理解,咱们用一个类比来结束本小节,沿用前几节读书的例子,咱们正在读的书中引用了《码农的荒岛求生》以及其它著做,那么加载时动态连接就比如,读者开始准备读这本书的时候(尚未真正的读)就把全部该书当中引用的资料著做都找齐放到一旁准备查看,当咱们真正看到引用其它文献的地方时就能够直接在一旁找到该著做啦。在这个类比当中,开始读书前的准备工做就比如加载时动态连接。
接下来咱们讲解第二种动态连接,run-time dynamic linking(运行时动态连接) 。
run-time dynamic linking(运行时动态连接)
上一小节中咱们看到若是咱们想使用加载时动态连接,那么在编译连接生成可执行文件阶段时须要告诉编译器所依赖的动态库信息,而run-time dynamic linking 运行时动态连接则不须要在编译连接时提供动态库信息,也就是说,在可执行文件被启动运行以前,可执行文件对所依赖的动态库信息一无所知,只有当程序运行到须要调用动态库所提供的代码时才会启动动态连接过程。
咱们在上一节中介绍了load-time,也就是程序加载时,那么程序加载完成后就开始程序执行了,那么所谓run-time(运行时)指的就是从程序开始被CPU执行到程序执行完成退出的这段时间。
因此运行时动态连接这种方式对于“动态连接”阐释的更加淋漓尽致,由于可执行文件在启动运行以前都不知道须要依赖哪些动态库,只在运行时根据代码的须要再进行动态连接。同加载时动态连接相比,运行时动态连接将连接这个过程再次推迟日后推迟,推迟到了程序运行时。
因为在编译连接生成可执行文件的过程当中没有提供所依赖的动态库信息,所以这项任务就留给了程序员,在代码当中若是须要使用某个动态库所提供的函数,咱们可使用特定的API来运行时加载动态库,在Windows下经过LoadLibrary或者LoadLibraryEx,在Linux下经过使用dlopen、dlsym、dlclose这样一组函数在运行时连接动态库。当这些API被调用后,一样是首先去找这些动态库,将其从磁盘copy到内存,而后查找程序依赖的函数是否在动态库中定义。这些过程完成后动态库中的代码就能够被正常使用了。
相对于加载时动态连接,运行时动态连接更加灵活,同时将动态连接过程推迟到运行时能够加快程序的启动速度。
为了和加载时动态连接做比对,咱们继续使用上一小节当中读书的例子,加载时动态连接就比如在开始准备读一本书以前,将该书中全部引用到的资料文献找齐全,而运行时动态连接则不须要这个过程,运行时动态连接就比如直接拿起一本书开始看,看到有引用的参考文献时再去找该资料,找到后查看该文献而后继续读咱们的书。从这个例子当中运行时动态连接更像是咱们平时读书时的样子。
至此,两种动态连接的形式咱们就都已经清楚了,接下来咱们看一下动态连接下生成的可执行文件。
动态连接下可执行文件的生成
在静态连接下,连接器经过将各个目标文件的代码段和数据段合并拷贝到可执行文件,所以静态连接下可执行文件当中包含了所依赖的全部代码和数据,而与之对比的动态连接下可执行文件又是什么样的呢?
其实咱们在动态库这一节中已经了解了动态连接下可执行文件的生成,即,在动态连接下,连接器并非将动态库中的代码和数据拷贝到可执行文件中,而是将动态库的必要信息写入了可执行文件,这样当可执行文件在加载时就能够根据此信息进行动态连接了。为方便理解,咱们将该信息仅仅认为是动态库都名字,真实状况固然要更复杂一点,这里咱们以Linux下可执行文件即ELF文件为例(这一系列的文章重点关注最本质的原理思想,因此这里讨论的一样适合Windows下的可执行文件即exe文件)。
在前几节中咱们将可执行文件简单的划分为了两段,数据段和代码段,在这里咱们继续丰富可执行文件中的内容,如图所示,在动态连接下,可执行文件当中会新增两段,即dynamic段以及GOT(Global offset table)段,这两段内容就是是咱们以前所说的必要信息。
dynamic段中保存了可执行文件依赖哪些动态库,动态连接符号表的位置以及重定位表的位置等信息。关于dynamic以及GOT段的做用限于篇幅就不重点阐述了。若是你对GOT段的具体做用很好奇的话,欢迎关注微信公共帐号,码农的荒岛求生。
当加载可执行文件时,操做系统根据dynamic段中的信息便可找到使用的动态库,从而完成动态连接。
这里须要强调一点,在编译连接过程当中,能够同时使用动态库以及静态库。这两种库的使用并不冲突,那么在这种状况下生成的可执行文件中,可执行文件中包含了静态库的数据和代码,以及动态库的必要信息。
至此,关于静态库,静态连接,动态库,动态连接就讲述到这,那么接下来的问题就是静态库和动态库都有什么样的优缺点。
动态库vs静态库
在计算机的历史当中,最开始程序只能静态连接,可是人们很快发现,静态连接生成的可执行文件存在磁盘空间浪费问题,由于对于每一个程序都须要依赖的libc库,在静态连接下每一个可执行文件当中都有一份libc代码和数据的拷贝,为解决该问题才提出动态库。
在前几节咱们知道,动态连接下可执行文件当中仅仅保留动态库的必要信息,所以解决了静态连接下磁盘浪费问题。动态库的强大之处不只仅于此,咱们知道对于现代计算机系统,好比PC,一般会运行成百上千个程序(进程),且程序只有被加载到内存中才可使用,若是使用静态连接那么在内存中就会有成百上千份一样的libc代码,这对于宝贵的内存资源一样是极大的浪费,而使用动态连接,内存中只须要有一份libc代码,全部的程序(进程)共享这一份代码,所以极大的节省了内存资源,这也是为何动态库又叫共享库。
动态库还有另一个强大之处,那就是若是咱们修改了动态库的代码,咱们只须要从新编译动态库就能够了而无需从新新编译咱们本身的程序,由于可执行文件当中仅仅保留了动态库的必要信息,从新编译动态库后这些必要都信息是不会改变的(只要不修改动态库的名字和动态库导出的供可执行文件使用的函数),编译好新的动态库后只须要简单的替换原有动态库,下一次运行程序时就可使用新的动态库了,所以动态库的这种特性极大的方便了程序升级和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),之因此动态库中的代码是地址无关代码是由于动态库又被成为共享库,全部的程序均可以调用动态库中的代码,所以在使用动态库中的代码时程序要多作一些工做,这里咱们再也不具体展开讲解到底程序多作了哪些工做,对此感兴趣当同窗能够参考CSAPP(深刻理解计算机系统)。这里咱们说动态连接的程序性能相比静态连接稍弱,可是这里的性能损失是微乎其微的,同动态库能够带来的好处相比,咱们能够彻底忽略这里的性能损失,同窗们能够放心的使用动态库。
动态库的一个优势其实也是它的缺点,即动态连接下的可执行文件不能够被独立运行(这里讨论的是加载时动态连接,load-time dynamic link),换句话说就是,若是没有提供所依赖的动态库或者所提供的动态库版本和可执行文件所依赖的不兼容,程序是没法启动的。动态库的依赖问题会给程序的安装部署带来麻烦,在Linux环境下尤为严重,以笔者曾参与开发维护的一个虚拟桌面系统为例,咱们在开发过程当中依赖的一些比较有名的第三方库默认不会随着安装包发布,这就会致使用户在较低版本Linux中安装时常常会出现程序没法启动的问题,缘由就在于咱们编译连接使用都动态库和用户Linux系统中都动态库不兼容。解决这个问题的方法一般有两种,一个是用户升级系统中都动态库,另外一个是咱们讲须要都第三方库随安装包一块儿发布,固然这是在取得许可的状况下。
在了解了动态库的优缺点后,接下来咱们来看一下静态库。
静态连接是最古老也是最简单的连接技术。静态连接都最大优势就是使用简单,编译好的可执行文件是完备的,即静态连接下的可执行文件不须要依赖任何其它的库,由于静态连接下,连接器将全部依赖的代码和数据都写入到了最终的可执行文件当中,这就消除了动态连接下的库依赖问题,没有了库都依赖问题就意味着程序都安装部署都获得了极大都简化。请你们不要小看这一点,这对当今那些拥有海量用户的后端系统来讲相当重要,好比相似微信这种量级的系统,其后端会部署在成千上万台机器上,这么多的机器其系统的安装部署以及升级会给运维带来极大挑战,而静态连接下的可执行文件因为不依赖任何库,由于部署很是方便,仅仅用一个新的可执行文件进行覆盖就能够了,所以极大的简化了系统部署以及升级。笔者以前所在的某电商广告后端系统就彻底使用静态连接来简化部署升级。
而静态库的缺点相信你们都已经清楚了,那就是静态连接会致使可执行文件过大,且多个程序静态连接同一个静态库的话会致使磁盘浪费的问题。
到这里关于静态库和动态库的讨论就告一段落了,相信你们对于这两种连接类型都有了清晰都认知。接下来让咱们稍做休息,开始连接器的下一个重要功能,重定位。
《完全理解连接器:五,重定位》,欢迎关注微信公众号,码农的荒岛求生,获取更多内容。
本文分享自微信公众号 - 码农的荒岛求生(escape-it)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。