理解动态链接

今天我将从以下两个方面来记录一下我对动态链接的学习理解。

1.共享库的作用和生成方法

2.加载和链接共享库的方法

首先,我们先来看一下共享库的定义:共享库是致力于解决静态库缺陷的一个现代化产物。一个共享程序库就是一个共享函数库,应用程序可以在运行时连接到该程序库,而不是在编译时连接。(静态库的具体内容这里就不过多解释了)

既然共享库是为了解决静态库的缺陷而出现的,那么静态库有哪些缺陷呢?我们来看一下

静态库的缺点

1.多个进程的虚拟存储空间可能存在同一段公共代码的多个拷贝。


如图,我们可以看到,一个printf函数在主存中有多份相同的代码,造成很大的空间浪费。

2.单个进程的虚拟存储空间可能存在同一段公共代码的多个拷贝。


扫描二维码关注公众号,回复: 1782071 查看本文章

如图,一个程序中多次用到了printf函数,这样在主存中也会储存多个printf函数的相同代码。

3.对系统库小的修改都需要每个程序显示重新链接。

静态链接形成的是完全链接的可执行文件,任何对库的修改都要求我们重新编译形成可执行文件。

针对这些缺点推出了共享库,我们来看一下共享库的优点有哪些。

共享库的优点

1.一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。此时没有任何共享库的代码和数据节真的被拷贝到可执行文件中。

(图来自网上,侵权删)

基于虚拟存储器的代码共享使得在物理内存中只存在某个模块的唯一一份代码。但是通过虚拟存储器的内存映射机制,我们可以将物理地址空间中的代码映射到不同进程的虚拟地址空间中。

2.由于动态链接形成的只是部分链接的可执行文件,并不是完全链接的可执行文件。在运行时无需停止服务器就可以更新已存在的函数,以及添加新的函数。(后面会解释验证)


介绍完共享库的概念和优点。我们接下来开始在Linux系统上尝试生成一个共享库。
生成指令:gcc -shared -fPIC -o libvector.so addvec.c multvec.c

-fPIC选项指示编译器生成与位置无关的代码

-shared选项指示链接器创建一个共享的目标文件

这里稍微解释一下位置无关的概念。所谓位置无关的代码,就是链接器不需要修改库代码就可以在任何地址加载和执行的代码。
比如:对同一个目标过程中的调用是不需要特殊处理的,因为引用是PC相对的。也就是我在寻找这些代码的地址时跟它的绝对地址是无关的。(R_386_PC32)而对外部定义的过程调用和对全局变量的应用通常就不是PIC,因为它们都要求在连接时重定位。(R_386_32)


我们接下来来生成一个libvector.so的库,这个库包含addvec和multvec两个函数。函数的具体内容如下。


在gcc上运行gcc -shared -fPIC -o libvector.so addvec.c multvec.c指令


生成如下的文件(这里只有这里只有addvec.c,multvec.c以及libvector.so与本题相关),其中libvector.so就是生成的库文件


生成共享库之后我们怎么去使用它呢?在这里,我将介绍两种动态链接的方法。分别是加载时链接以及运行时链接。

我们先介绍第一种方法。

加载时链接

基本思路:当创建可执行文件时,静态执行一些链接(拷贝了重定位和符号表信息),然后在程序加载时,动态完成链接过程。

具体过程:(结合下图理解)
动态链接器通过执行下面的重定位完成链接任务:
A.重定位libc.so的文本和数据到某个储存器段

B.重定位libvector.so的文本和数据到另一个储存器段

C.重定位p2中所有对有libc.so和libvector.so定义的符号和引用

D.最后,动态链接器将控制转移给应用程序。从这个时刻开始,共享库的位置就固定了,并且在执行过程中都不会改变。


注意点:

1.加载时动态链接并不完全只是动态链接,也包括了部分的静态链接。通过静态链接把库重定位的信息加载给链接器,通过这些信息重定位p2中用到的库中的引用。最后形成的p2就是一个部分链接的可执行文件,它其中包含了库的符号表和重定位信息。

2.库的代码和数据没有被加载到p2中,而是当我们在运行p2时,我们根据静态链接时获取的符号表和信息来定位到库代码所在的地址,然后执行相应的代码。

接下来我们构建一个主函数main2.c,内容如下:


这里说一下头文件vector.h。头文件vector.h定义了libvector.a中例程的函数原型。可以理解为这个头文件evctor.h包含了addvec和multvec两个函数的定义。这样子我们在c++运行时才能编译通过。但是如果我们直接在gcc里面进行手动链接,可以将这个头文件删去,不必实现这个头文件。

运行链接指令,生成部分链接文件p2。运行p2,得到正确的结果。



接下来,我们再看一下另一种连接方法--运行时链接。

Linux系统为动态链接器提供了一个简单的接口,允许应用程序在运行时加载和链接共享库。主要涉及一下四个函数。

(1)dlopen()
第一个参数:指定共享库的名称,将会在下面位置查找指定的共享库.
第二个参数:指定如何打开共享库。
-RTLD_NOW:将共享库中的所有函数加载到内存
-RTLD_LAZY:会推后共享库中的函数的加载操作,直到调用dlsym()时方加载某函数
(2)dlsym()
调用dlsym时,利用dlopen()返回的共享库的phandle以及函数名称作为参数,返回要加载函数的入口地址。
(3)dlerror()
该函数用于检查调用共享库的相关函数出现的错误。
(4)dlclose()
该函数用于关闭动态库。

根据这四个函数我们来构造一个main函数,命名为dll.c。具体代码如下


整个代码主要分为四个部分。获取返回库地址的指针,获取返回要调用函数的指针,通过指针使用调用函数,卸载库。

在gcc上运行如下指令

链接指令gcc -rdynamic -O2 -o p3 dll.c -ldl

-rdynamic 用来通知链接器将所有符号添加到动态符号表中
(目的是能够通过使用 dlopen 来实现向后跟踪)
-ldl选项,表示生成的对象模块需要使用共享库


运行成功,结果正确。

至此,我们已经把动态链接的概念和作用,生成和链接都讲完了。

我们前面讲过,动态链接时,如果库被修改了,我们可以直接运行以前生成的部分链接文件,而不必重新编译它,这里我们来验证一下。

我把addvec中的加号改成了减号。然后使用相应的指令生成新的库。

生成之后,直接运行我们之前生成的部分链接可执行文件p2(加载时链接)和p3(运行时链接)。发现函数的返回结果确实是变成减法的运算结果了。

这里我们可以这么理解。当你去改变库里面具体函数的内容时。只要函数名没有改变,函数代码的首地址就不会改变。我们还是能够找到函数代码的具体地址(前面静态链接时加载的符号表和重定位信息还是正确的)。而只要我们找到了首地址就可以顺序执行该函数的其他指令了。

最后介绍一些动态链接的运用实例:
A.分发软件。通过共享库来分发软件更新。他们生成共享库的新版本,然后用户可以下载并用它替代当前的版本。
下一次运行程序时就可以链接到新的共享库。

B.构建高性能Web服务器。其思路就是将生成动态内容的每个函数打包在共享库中。当一个来自Web的请求到达时,服务器动态地加载和链接适当的函数,然后直接调用它。函数会一直缓存在服务器的地址空间中,所以只需要一个简单的函数调用就可以处理后面的请求了。在运行时无需停止服务器,就可以更新已存在的函数,以及添加新的函数。

动态链接过程中还有很多有趣的细节,有兴趣的同学可以自己再去探究。包括延迟绑定等等。

该文章的代码样例均来自《深入理解计算机系统》。




猜你喜欢

转载自blog.csdn.net/alexwym/article/details/80563036