7.1 为什么要动态链接
要解决空间浪费和更新困难这两个问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态地链接在一起。简单地讲,就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想
磁盘和内存中只存在一份Lib.o,而不是两份。另外在内存中共享一个目标文件
7.2简单的动态链接例子
如果foobar()是一个定义与其他静态目标模块中的函数,那么链接器将会按照静态链接的规则,将Program1.o中的foobar地址引用重定位;如果foobar()是一个定义在某个动态共享对象中的函数,那么链接器就会将这个符号的引用标记为一个动态链接的符号,不对它进行地址重定位,把这个过程留到装载时再进行。
Lib.so中保存了完整的符号信息(因为运行时进行动态链接还须使用符号信息),把Lib.so也作为链接的输入文件之一,链接器在解析符号时就可以知道:foobar是一个定义在Lib.so的动态符号。这样链接器就可以对foobar的引用做特殊的处理,使它成为一个对动态符号的引用。
动态链接程序运行时地址空间分布
Lib.so与Program1一样,它们都是被操作系统用同样的方法映射至进程的虚拟地址空间,
Program1除了使用Lib.so以外,它还用到了动态链接形式的C语言运行库libc-2.6.1.so。
ld-2.6.so,它实际上是Linux下的动态链接器。动态链接器与普通共享对象一样被映射到了进程的地址空间,在系统开始运行Program1之前,首先会把控制权交给动态链接器,由它完成所有的动态链接工作以后再把控制权交给Program1,然后开始执行。
readelf -l Lib.so 可以看到
动态链接模块的装载地址是从地址0x00000000开始的。我们知道这个地址是无效地址
共享对象的最终装载地址在编译时是不确定的,而是在装载时,装载器根据当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间
7.3 地址无关代码 SO的代码
7.3.2 装载时重定位
7.3.3 地址无关代码 PIC, Position-independent Code
四类地址引用方式。
共享对象模块中的地址引用按照是否为跨模块分成两类:模块内部引用和模块外部引用;
按照不同的引用方式:指令引用和数据访问
第一种是模块内部的函数调用、跳转等。
第二种是模块内部的数据访问,比如模块中定义的全局变量、静态变量。
第三种是模块外部的函数调用、跳转等
第四种是模块外部的数据访问,比如其他模块中定义的全局变量。
产生的汇编码
第一种 模块内部调用–call 函数
因为被调用的函数与调用者都处于同一个模块,它们之间的相对位置是固定的。模块内部的跳转、函数调用都可以是相对地址调用,或者是基于寄存器的相对调用,所以对于这种指令是不需要重定位的
call 函数
e8 偏移 ( 偏移看大小端和负数(补码))
类型二 模块内部数据访问–相对寻址。PC+偏移量
我们知道,一个模块前面一般是若干个页的代码,后面紧跟着若干个页的数据,这些页之间的相对位置是固定的
类型三 模块间数据访问 —GOT Global Offset Table
模块间的数据访问目标地址要等到装载时才决定。我们前面提到要使得代码地址无关,基本的思想就是把跟地址相关的部分放到数据段里面,很明显,这些其他模块的全局变量的地址是跟模块装载地址有关的。ELF的做法是在数据段里面建立一个指向这些变量的指针数组,也被称为全局偏移表(Global Offset Table,GOT),当代码需要引用该全局变量时,可以通过GOT中相对应的项间接引用,它的基本机制如图7-7所示
当指令中需要访问变量b时,程序会先找到GOT,然后根据GOT中变量所对应的项找到变量的目标地址。每个变量都对应一个4个字节的地址,链接器在装载模块的时候会查找每个变量所在的地址,然后填充GOT中的各个项,以确保每个指针所指向的地址正确。由于GOT本身是放在数据段的,所以它可以在模块装载时被修改,并且每个进程都可以有独立的副本,相互不受影响。
GOT如何做到指令的地址无关性
可以在编译时确定GOT相对于当前指令的偏移。
然后我们根据变量地址在GOT中的偏移就可以得到变量的地址
GOT中的各个项在连接时填充
类型四 模块间调用、跳转
GOT中相应的项保存的是目标函数的地址
7.3.4 共享模块的全局变量问题
–每个主程序一个副本,如果变量在共享模块中被初始化,复制数据段
–进程私有,线程共享
–R_X86_64_COPY
所有的使用这个变量的指令都指向位于可执行文件中的那个副本。
ELF共享库在编译时,默认都把定义在模块内部的全局变量当作定义在其他模块的全局变量,也就是说当作前面的类型四,通过GOT来实现变量的访问。
当共享模块被装载时,如果某个全局变量在可执行文件中拥有副本,那么动态链接器就会把GOT中的相应地址指向该副本,这样该变量在运行时实际上最终就只有一个实例。
如果变量在共享模块中被初始化,那么动态链接器还需要将该初始化值复制到程序主模块中的变量副本;如果该全局变量在程序主模块中没有副本,那么GOT中的相应地址就指向模块内部的该变量副本。
其他需求情况
共享数据段
多进程共享全局变量又被叫做“共享数据段”,在介绍Windows DLL的时候会碰到它。
线程私有存储
而多个线程访问不同的全局变量副本又被叫做“线程私有存储”(Thread Local Storage)
7.3.5 数据段地址无关性
7.4 延迟绑定(PLT)Procedure Linkage Table
延迟绑定(Lazy Binding)的做法,基本的思想就是当函数第一次被用到时才进行绑定(符号查找、重定位等),如果没有用到则不进行绑定。
bar@plt:
jmp *(bar@GOT) //.got.plt中存没存bar?没存就是下一条指令地址
push n //符号引用在重定位表“.rel.plt”中的下标。
push moduleID //将模块的ID压入到堆栈
jump _dl_runtime_resolve
7.5
7.5.1 “.interp”段
7.5.2 “.dynamic”段
“.dynamic”段可以看成是动态链接下ELF文件的“文件头”
readelf -d Lib.so
7.5.3 动态符号表
“.symtab”(Symbol Table) 静态的 readelf -s
“.dynsym”只保存了与动态链接相关的符号
“.symtab”中往往保存了所有符号,包括“.dynsym”中的符号
7.5.4 动态链接重定位表
静态链接:“.rel.text”代码段的重定位表,“.rel.data”数据段的重定位表。
动态链接:
.rel.dyn 数据 修正“.got”以及数据段
.rel.plt 函数
其实就是许多xxx@plt的代码,跳转真正函数或修正(首次)“.got.plt”
R_386_RELATIVE比较特殊
static int a;
static int* p = &a; p就是relative
共享对象被装载到地址A,那么实际上该变量a的地址为A+B,
即p的值需要加上一个装载地址A
7.5.5 动态链接时进程堆栈初始化信息
7.6 动态链接的步骤和实现
共享对象全局符号介入(Global Symbol Interpose)
7.7显示运行时链接
总结
静态链接 的外部符号 绝对地址
- 生成的xx.o 外部符号地址都是0
- 数据段的重定位表,每个重定位入口是一个外部符号的引用,表明在汇编码偏移位置,要把0修改
- ld时,全局符号表UND的,要根据重定向入口改汇编码
- 最后生成可执行文件
动态链接 的地址无关代码
模块内
- 不知道装载地址
- 汇编码为:本模块的pc(隐含模块装载的位置+指令在模块的偏移)+目标符号与指令的偏移
外模块的地址无关代码 通过GOT间接寻址
- .data的GOT地址:PC+偏移 ; 符号:符号在GOT的偏移
- GOT在装载时填上
- GOT间接寻址
GOT+延迟绑定