仅供个人学习参考

为什么要动态链接&&动态链接基本思想

  静态链接对于计算机内存和磁盘空间的浪费十分严重,且一旦程序中有任何模块更新,整个程序就需要重新链接后再发布给用户,用户每次都需要重新下载这个程序试想一下每次崩铁更新都要重新下载几十个G的文件那得多崩溃动态链接很好的优化了以上两个问题。
  其基本思想如下:把程序的各个模块分割成独立的文件,等到程序要运行时再将他们链接在一起,也就是将链接过程推迟。假设我们有program1.o、program2.o和lib.o三个目标文件。program1.o用到了lib.o,即program1.o依赖于lib.o,那么系统就会加载lib.o,如果他们还依赖于其他目标文件,那操作系统就再加载用到的文件,之后再进行链接工作,之后我们如果还需运行program2,系统只需加载program2,因为内存中已经存在一份lib.o
  在Linux系统中,elf动态链接文件被称为动态共享对象(DSO,Dynamic Shared Objects),简称共享对象,一般以.so为扩展名。在Windows系统中则以.dll为扩展名(动态链接库)

地址无关代码 (PIC)

  共享对象的最终装载地址在编译时是不确定的,那么我们如何确定它在进程虚拟空间中的位置?早期人们采用静态共享库,即手工分配各个模块的装载地址,但是单一个模块被多个程序使用,就容易导致地址冲突。比如模块a被一个人分配到0x1000到0x2000的位置,但是另外一个人用不到a,以为0x1000到0x2000是空闲的,就把b分配进去,这样就起冲突了,以后的人就不能同时使用a和b。
  为了解决模块装载时固定地址的问题,我们提出一个设想:共享对象在编译时不能假设自己在进程虚拟空间中的位置。首先想到的方法就是类比静态链接中的重定位,只不过我们将重定位推迟至装载时进行,即装载时重定位,又叫基址重置。但该方法有个很大的缺点是指令部分无法在多个进程之间共享,这样便失去了动态链接的一大优势。我们的目标是希望指令共享的部分不需要因为装载地址的改变而改变,所以我们将指令部分中需要修改的部分分离出来,与数据部分合并,这样剩余部分可保持不变,而数据部分可以在每个进程中拥有一个副本,这种方法是被称为地址无关代码的技术。
模块中地址引用方式可分为四种

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

被调函数与调用者都处于同一模块,这种指令不需要重定位,相对地址调用就可以了

类型二 模块内部数据访问 (比如模块中定义的全局变量、静态变量)

相对寻址,任何一条指令与它所需要访问的模块的数据之间的相对位置是固定的,只要当前指令地址(pc)加上偏移量就能得出数据地址了

模块三 模块间数据访问

这个麻烦点,因为模块间的数据访问目标地址需要等到装载时才决定。elf文件的做法是在数据段内建立一个指向这些变量的指针数组,也称为全局偏移表(GOT表),当代码需要引用这些全局变量时,可以通过GOT表中相对应的项来间接引用。
got
计算方式:通过pc值加上一个偏移量得到GOT表的位置,再根据变量地址在GOT表中的偏移就可以得到变量的地址

模块四 模块间调用、跳转 (比如其他模块定义的全局变量)

同三,就是把变量地址换成函数地址

PIE

以地址无关方式编码的可执行文件被称为地址无关可执行文件(PIE)

延迟绑定

  在动态链接下,程序模块之间包含了大量的函数调用,所以程序在开始执行前,动态链接会耗费不少时间用于解决模块之间的函数引用的符号查找和重定位,这会牺牲性能。而很多函数在实际运行中很少甚至不会用到,为了提高性能,ELF采用了一种叫做延迟绑定的机制。其基本思想就是程序在第一次被用到时才绑定(符号查找、重定位等)
  ELF采用PLT(Procedure Linkage Table)的方法来实现。
plt
如图,调用函数的方式在got表的基础上再加一层,先通过plt表查询函数在got表对应的项的地址,再通过got表进行间接寻址,如果是第一次调用函数,got表是没有函数地址的,需要plt表去绑定该函数(其中需要用到plt表中的前三个参数),之后got表就有对应函数的地址了。