动态链接

动态链接

要解决空间浪费和更新困难这两个问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态地链接在一起。简单地讲,就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接( Dynamic Linking)的基本思想。

动态库的基本实现

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有的程序模块都链接成一个个单独的可执行文件。那么我们能不能按照前面例子中所描述的那样,直接使用目标文件进行动态链接呢?这个问题的答案是:理论上是可行的,但实际上动态链接的实现方案与直接使用目标文件稍有差别。我们将在后面分析目标文件和动态链接文件的区别。

动态链接涉及运行时的链接及多个文件的装载,必需要有操作系统的支持,因为动态链接的情况下,进程的虚拟地址空间的分布会比静态链接情况下更为复杂,还有一些存储管理、内存共享、进程线程等机制在动态链接下也会有一些微妙的变化。目前主流的操作系统几乎都支持动态链接这种方式,在 Linux系统中,ELF动态链接文件被称为动态共享对象(DSO, Dynamic Shared objects),简称共享对象,它们一般都是以“so”为扩展名的一些文件; 而在 Windows系统中,动态链接文件被称为动态链接库( Dynamical Linking Library),它们通常就是我们平时很常见的以“dll”为扩展名的文件。

从本质上讲,普通可执行程序和动态链接库中都包含指令和数据,这一点没有区别。在使用动态链接库的情况下,程序本身被分为了程序主要模块( Progran1)和动态链接库( Lib. so),但实际上它们都可以看作是整个程序的一个模块,所以当我们提到程序模块时可以指程序主模块也可以指动态链接库。

在 Linux中,常用的C语言库的运行库glib,它的动态链接形式的版本保存在“/lib”目录下,文件名叫做“ libc.so”。整个系统只保留一份C语言库的动态链接文件“libc.so”,而所有的C语言编写的、动态链接的程序都可以在运行时使用它。当程序被装载的时候,系统的动态链接器会将程序所需要的所有动态链接库(最基本的就是libc.so)装载到进程的地址空间,并且将程序中所有未决议的符号绑定到相应的动态链接库中,并进行重定位工作。

程序与libc.so之间真正的链接工作是由动态链接器完成的,而不是由我们前面看到过的静态链接器ld完成的。也就是说,动态链接是把链接这个过程从本来的程序装载前被推迟到了装载的时候。可能有人会问,这样的做法的确很灵活,但是程序每次被装载时都要进行重新进行链接,是不是很慢?的确,动态链接会导致程序在性能的一些损失,但是对动态链接的链接过程可以进行优化,比如我们后面要介绍的延迟绑定( Lazy Binding)等方法,可以使得动态链接的性能损失尽可能地减小。据估算,动态链接与静态链接相比,性能损失大约在5%以下。当然经过实践的证明,这点性能损失用来换取程序在空间上的节省和程序构建和升级时的灵活性,是相当值得的。

动态链接程序运行时地址空间分布

对于静态链接的可执行文件来说,整个进程只有一个文件要被映射,那就是可执行文件本身,我们在前面的章节己经介绍了静态链接下的进程虚拟地址空间的分布。但是对于动态链接来说,除了可执行文件本身之外,还有它所依赖的共享目标文件。那么这种情况下,进程的地址空间分布又会怎样呢?

我们还是以上面的 Program1为例,但是当我们试图运行 Program1并且查看它的进程空间分布时,程序一运行就结束了。所以我们得对程序做适当的修改,在Libc中的 foobar.c 函数里面加入sleep函数

然后就可以查看进程的虚拟地址空间分布:

我们看到,整个进程虚拟地址空间中,多出了几个文件的映射。Lib.so 与 Program1 一样,它们都是被操作系统用同样的方法映射至进程的虚拟地址空间,只是它们占据的虚拟地址和长度不同。 ProgramI除了使用Lb.so以外,它还用到了动态链接形式的C语言运行库libc-2.61so。另外还有一个很值得关注的共享对象就是ld-2.6so,它实际上是 Linux下的动态链接器。动态链接器与普通共享对象一样被映射到了进程的地址空间,在系统开始运行Program1之前,首先会把控制权交给动态链接器,由它完成所有的动态链接工作以后再把控制权交给 Program1,然后开始执行。

我们通过 readelf工具来查看 Lib. so的装载属性,就如我们在前面查看普通程序一样:

除了文件的类型与普通程序不同以外,其他几乎与普通程序一样。还有有一点比较不同的是,动态链接模块的装载地址是从地址0x00000始的。我们知道这个地址是无效地址,并且从上面的进程虚拟空间分布看到, Lib. so的最终装载地址并不是0x000000是0xb7efc000。从这点我们可以推断,共享对象的最终装载地址在编译时是不确定的,而是在装载时,装载器根据当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间给相应的共享对象。

当然,这仅仅是一个推断,至于为什么要这样做,为什么不将每个共享对象在进程中的地址固定,或者在真正的系统中是怎么运作的,我们将在下一节进行解释。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏瓜大三哥

vim命令总结

2237
来自专栏企鹅号快讯

前端实现本地图片读取与简单压缩功能

在上一篇文章Javascript 基础夯实 —— 通过代码构建一个包含文件的 FormData 对象中提到了前端压缩图片的功能,所以本篇文章就来实现一下这个功能...

2988
来自专栏小狼的世界

Python3.6学习笔记(四)

程序运行中,可能会遇到BUG、用户输入异常数据以及其它环境的异常,这些都需要程序猿进行处理。Python提供了一套内置的异常处理机制,供程序猿使用,同时PDB提...

1574
来自专栏生信宝典

Linux学习 - 管道、标准输入输出

Linux下的标准输入、输出、重定向、管道 在Linux系统中,有4个特殊的符号,<, ‘>’, ‘|’, ‘-‘,在我们处理输入和输出时存在重要但具有迷惑性的...

3325
来自专栏程序员的知识天地

Python实现一个代码行数统计工具

我们经常想要统计项目的代码行数,但是如果想统计功能比较完善可能就不是那么简单了, 今天我们来看一下如何用python来实现一个代码行统计工具。

1731
来自专栏debugeeker的专栏

《coredump问题原理探究》windows版8.7节堆布局heap corruption第二个例子

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xuzhina/article/detai...

932
来自专栏运维

Unix & Linux 大学教程 学习总结

两年前我看这本书时,是一本812页的厚书,现在我总结成了40句话,什么时候成了1句话就好了。

1371
来自专栏Linux驱动

12.Linux之输入子系统分析(详解)

在此节之前,我们学的都是简单的字符驱动,涉及的内容有字符驱动的框架、自动创建设备节点、linux中断、poll机制、异步通知、同步互斥/非阻塞、定时器去抖动。 ...

2856
来自专栏数据结构与算法

利用MingW检验程序运行内存

今天zhx老师在讲课的时候提到了一种检验程序内存的方法 一般计算内存的方法就是手算,手动计算代码中每个变量所占的内存然后加起来 具体可以参考这篇文章 zhx老师...

2806
来自专栏陈纪庚

SPA页面初试

982

扫码关注云+社区

领取腾讯云代金券