程序员的自我修养-链接、装载与库-7 动态链接

动态链接

静态链接的好处:使得不同部门的开发者能够相对独立的开发和测试自己的程序模块,促进了开发效率,原先限制程序的规模也随之扩大。

     缺点:浪费内存空间和磁盘空间,模块更新困难

  种种罪行:

  空间浪费:想想一下每个程序内部除了printf, scanf, strlen等公用库函数,还有非常多的其他库函数以及他们所需的辅助数据结构。在Linux中一个普通的c程序需要的静态库至少1MB以上。

简而言之就是,相同的目标模块每一个程序都会保留一份obj文件。

  更新困难:一旦程序中有任何模块更新,整个程序都需要重新连接,发布给用户。

动态链接

  一种简单办法就是把程序的模块互相分割开来,形成独立的文件,而不再把它们静态地链接在一起。就是不对那些组成程序的目标文件进行链接等到程序要运行时才进行链接。把链接这个过程推迟到运行时在进行,这就是动态链接。


当我们运行Program1这个程序时,系统先加载Program1.o文件,当系统发现Program1.o中用到了Lib.o(Pro.o 依赖于 Lib.o),那么系统接着加载Lib.o,这样将所有以来文件加载至内存。如果所有需要的目标文件加载完毕,如果依赖关系满足,即所有依赖的目标文件目标文件都存在㔿磁盘,系统开始进行连接工作。这个链接和静态链接非常相似,包括符号解析、地址重定位等。完成这些步骤之后,系统开始把控制权交给Program1.o的程序入口处程序开始运行。这时如果我们需要运行Program2,那么系统只需要加载Program2.o,而不需要重新加载Lib。o因为在内存中已经存在了一份Lib.o副本,系统需要做的只是将Program2.o和lib.o链接起来。 

优点:

  节省空间;模块化更新;提高程序的可扩展性和兼容性:在运行时可以动态的选择加载各种程序模块(开发插件)/ 一个程序在不同的平台运行时可以动态地连接到由操作系统提供的动态链接库。

缺点:

  当程序所依赖的某个模块更新后,由于新的模块与旧的模块之间接口不兼容,导致原有的程序无法运行。“DLL Hell”//  链接速度:慢一些

实现:

  使用obj目标文件来进行动态链接是理论可行的。但是实际上两者之间还是有些区别的。

  在Linux动态链接文件被称为动态共享对象DSO,dynamic shared objects. 通常就是我们常见的.dll为扩展名的文件。Linux 下C语言常用的运行库glibc以动态连接形式的版本保存在/lib目录下,文件名叫:libc.so:整个系统只有一份C语言的动态链接库。所有C语言编写的动态链接的程序都可以使用它。当程序装载的时候。

  例子:我们有三个源代码文件:Program1.c、Program2.c、Lib.h

  使用GCC将lib.c翻译成一个共享对象文件:

gcc -fPIC -shared -o Lib.so Lib.c

  -shared:标识产生共享对象

  -fPIC:地址无关代码技术,可以节省内存

   将Program1.c和Program2.c编译成可执行文件:

gcc -o Program1 Program1.c ./Lib.so
gcc -o Program2 Program2.c ./Lib.so 

  这样就得到了两个程序都使用了Lib.so中的foobar函数.

  然后,我单独修改了foobar函数在里面增加了sleep函数,让程序一直随眠.我只重新生成了Lib.so文件.然后再运行Program1可执行文件,发现直接调用了新的foobar函数.

这就是动态链接的一个好处,并不用重新编译可执行文件.

/* Lib.c */
#include <stdio.h>

void foobar(int i)
{
    printf("Printing from Lib.so %d\n", i);
    sleep(-1);
}

  后台运行程序,然后查看进程的虚拟地址空间分布:

  发现多了几个文件映射.除了Program1和Lib.so还有一个C语言运行库libc-2.21.so.  另外ld-2.21.so是一个Linux下的动态连接器.动态连接器与普通对象一样被映射到了进程的地址空间,在系统开始运行Program1之前,首先会把控制权交给动态连接器.由它完成所有的动态链接工作之后再把控制权交给Program1,然后开始执行.\

装载时重定位:

  共享库中的函数foobar的虚拟地址实在装载的时候确定的,一旦确定后就遍历模块中的重定位表,将所有绝对地址的引用重定位到该地址.

地址无关代码:

  上面所有的装载时重定位是解决动态模块中有绝对地址因哟家哪个的办法之一,但是缺点是指令部分无法在多个进程之间共享.为了节省内存,还有一种更好的办法:我们希望程序模块中 共享的指令部分在装载时不需要因为装载地址的改变而改变.实现的思路就是将指令中需要被修改的部分分离出来跟数据部分放到一起.而数据部分可以在每个进程中拥有一个副本.这就是地址无关代码(PIC, Position-independent Code)的技术.

  类型一:模块内调用或跳转

    这个天然是地址无关代码.因为都可以根据相对地址来进行调用,这种指令不需要重定位.

    类型二:模块内部数据访问

    任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的.

  类型三:模块间数据访问

    模块间的数据访问要等到装载时才决定.ELF的做法是在数据段里面建立一个指向这些变量的指针数组,也被成为全局偏移表(GOT global offset table).当代码需要引用该全局变量时,吗可以通过GOT中相对应的项间接引用.    

    类型四:模块间调用&跳转

      可以采用上面的方法来解决.可以通过GOT间接访问函数地址.

延迟绑定(PLT)

  一种优化动态链接速度的技术.

  懒绑定,第一次使用到该函数的时候才进行绑定(符号查找,重定位),不用到不绑定.

整个过程:

  首先,操作系统会读取可执行文件的头部,检查文件的合法性,然后从头部中的Program Hearder中读取每个Segment的虚拟地址,文件地址和属性,并将他们映射到进程的虚拟地址空间的相应位置.

  然后:

    静态链接:操作系统就可以吧控制权交给可执行文件的入口地址,程序开始执行.

    动态链接:因为科执行文件依赖的很多共享对象,这时候文件里很多外部符号的引用还处于无效地址的状态.操作系统会先启动一个动态连接器.

        动态链接器开始执行对可执行文件链接工作.最后将控制权交给可执行文件的入口,程序开始执行.

动态链接重定位表

  在静态链接的时候那些不确定的地址引用(模块间的)最终在链接的时候被修正.但是在动态链接中,导入符号的地址在运行时才确定.

  

posted on 2017-01-21 15:30  暴力的轮胎  阅读(459)  评论(0编辑  收藏  举报

导航