承接《彻底理解链接器:二》
目录
动态库
动态链接
动态链接下可执行文件的生成
在前三小节中我们了解了静态库、静态链接以及使用静态链接下可执行文件是如何生成的。接下里我们讲解一下动态库,那么什么是动态库?
动态库(Dynamic Library),又叫共享库(Shared Library),动态链接库等,在Windows下就是我们常见的大名鼎鼎的DLL文件了,Windows系统下大量使用了动态库。在Linux下动态库是以.so为后缀的文件,同时以lib为前缀,比如进行数字计算的动态库Math,编译链接后产生的动态库就叫做libMath.so。从名字中我们知道动态库也是库,本质上动态库同样包含我们已经熟悉的代码段、数据段、符号表。只不过动态库的使用方式以及使用时间和静态库不太一样。
在前面几个小节中我们知道,使用静态库时,静态库的代码段和数据段都会直接打包copy到可执行文件当中,使用静态库无疑会增大可执行文件的大小,同时如果程序都需要某种类型的静态库,比如libc,使用静态链接的话,每个可执行文件当中都会有一份同样的libc代码和数据的拷贝,如图所示,动态库的出现解决了此类问题。
动态库允许使用该库的可执行文件仅仅包含对动态库的引用而无需将该库拷贝到可执行文件当中。也就是说,同静态库进行整体拷贝的方式不同,对于动态库的使用仅仅需要可执行文件当中包含必要的信息即可,为了方便理解,你可以将可执行文件当中保存的必要信息仅仅理解为需要记录动态库的名字就可以了,如图所示,同静态库相比,动态库的使用减少了可执行文件的大小。
从上面这张图中可以看出,动态库的使用解决了静态链接当中可执行文件过大的问题。我们在前几节中将静态链接生成可执行文件的过程比作了装订一本书,静态链接将引用的其它人的著作也装订到了书里,而动态链接可以想象成作者仅仅在引用的地方写了一句话,比如引用了《码农的荒岛求生》,那么作者就在引用的地方写上“此处参考《码农的荒岛求生》”,那么读者在读到这里的时候会自己去找到码农的荒岛求生这本书并查找相应的内容,其实这个过程就是动态链接的基本思想了。
到这里我们就可以回答之前提到过的问题了,helloworld程序中的printf函数到底是在哪里定义的,答案就是该函数是在libc.so当中定义的,Linux下编译链接生成可执行文件时会默认动态链接libc.so(Windows下也是同样的道理),使用ldd命令就会发现每个可执行文件都依赖libc.so。因此虽然你从没有看到过printf的定义也可以正确的使用这个函数。
接下来我们讲解一下动态链接。
我们知道静态库在编译链接期间就被打包copy到了可执行文件,也就是说静态库其实是在编译期间(Compile time)链接使用的,那么动态库又是在什么时候才链接使用的呢,动态链接可以在两种情况下被链接使用,分别是load-time dynamic linking(加载时动态链接) 以及 run-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来查看。
阶段二:加载可执行文件时依据动态库信息进行动态链接
由于在阶段一生成的可执行文件中保存了动态库信息,当可执行文件加载完成后,就可以依据此信息进行中动态库的查找以及符号决议了。
通过这个过程也可以清楚的看到静态库和动态库的区别,使用动态库的可执行文件当中仅仅保留相应信息,动态库的链接过程被推迟到了程序启动加载时。
为加深你对加载时动态链接这个过程的理解,我们用一个类比来结束本小节,沿用前几节读书的例子,我们正在读的书中引用了《码农的荒岛求生》以及其它著作,那么加载时动态链接就好比,读者开始准备读这本书的时候(还没有真正的读)就把所有该书当中引用的资料著作都找齐放到一旁准备查看,当我们真正看到引用其它文献的地方时就可以直接在一旁找到该著作啦。在这个类比当中,开始读书前的准备工作就好比加载时动态链接。
接下来我们讲解第二种动态链接,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段中的信息即可找到使用的动态库,从而完成动态链接。
这里需要强调一点,在编译链接过程中,可以同时使用动态库以及静态库。这两种库的使用并不冲突,那么在这种情况下生成的可执行文件中,可执行文件中包含了静态库的数据和代码,以及动态库的必要信息。
至此,关于静态库,静态链接,动态库,动态链接就讲述到这,那么接下来的问题就是静态库和动态库都有什么样的优缺点。
关于静态库动态库的优缺点以及后面的内容,我会在该系列的下一篇文章当中介绍,如果你喜欢这个系列的文章,也欢迎关注我的微信公共账号,码农的荒岛求生,获取更多内容。
彻底理解链接器系列
彻底理解操作系统系列