一、动态链接的基本实现:
将程序按照模块拆分成相对独立部分(可执行文件和共享对象),在程序装载运行时才将他们链接成完整程序。动态链接的可执行文件是依赖共享对象的。而不像静态链接将所有模块链接成单一可执行文件后再进行装载运行。
动态链接的符号重定位过程在程序装载时才进行。
二、地址无关代码PIC(position-independent code)
问题来源:共享对象A最终装载地址在编译时不确定,而其他模块B的代码段可能包含对象A中函数和数据的引用。如果采用装载时直接对B代码段的外部引用进行重定位修改,那么导致被修改后共享对象B的代码段在不同进程间不相同(因为不同进程地址空间不同),导致共享对象B在内存中不能使用同一个副本,失去动态链接的优势。
所以引入地址无关代码。。。
PIC基本思想:将指令中需要修改的部分分离出来,跟数据部分.data放在一起,使指令部分.text保持不变,实现不同进程间共享。
具体分以下4种情况:
(A)模块内部函数调用:模块内部跳转、函数调用都是基于相对地址的,而模块内部函数地址相对固定,即已经满足PIC条件,无需修改。实际上还有全局符号介入问题。
(B)模块内部数据访问:模块内部,任何一条指令与它需要访问的模块内部数据间相对位置固定,因此也满足PIC条件。
(C)模块间数据访问:在数据段里建立指向这些外部变量的指针数组,全局偏移表(GOT)。当代码需要引用这些变量时,通过GOT中对应项间接引用。GOT表在.data中,与指令相对位置固定,满足PIC条件。至于GOT表中内容,则是在共享对象被装载进行重定位时填充。
(D)模块间调用、跳转:与(C)相似,只是GOT中保存目标函数地址。时间上使用更高效的延时绑定PLT方式。
至此,共享模块代码段满足PIC条件。。。
但是还有一种例外。。。可执行文件引用共享模块全局变量的问题。
可执行文件(主程序)并不是地址代码无关,代码段对全局变量的访问方式与普通变量一样,使用绝对地址,因此不能用GOT方式。同时可执行文件在运行时代码段不进行重定位。为了链接正常,连接器在创建可执行文件时,在其.bss创建该全局变量副本。而其他共享对象(包括定义此变量的共享对象)在装载时通过GOT指向该副本。
可执行文件对引用共享模块的函数仍然使用GOT方式,并不存在问题。
三、动态链接方式程序启动过程
四、延时绑定PLT -- 链接性能的优化
问题来源:动态链接比静态链接慢的很重要原因是:所有共享对象在进行装载时,都需要重定位GOT(包括外部全局变量和函数调用等),即填充GOT表内容,然后进行间接寻址。而函数间的引用是最主要一部分引用。
基本思路:共享模块函数只有在第一次被引用时才进行重定位(通过查找全局符号表找到被引用函数地址,并填入GOT相应位置,完成重定位)。
具体实现:当共享对象A引用外部模块函数foo()时,我们增加PLT项进行跳转。每个外部函数都有一个PLT项,并构成独立于.text的.plt段(可以看成特殊的代码段)。
foo@plt:
jmp *(foo@GOT) //首先查看GOT表中是否已经保存过该函数
push n //如果没有,则将该函数重定位表所在下标n和模块压栈(用于到时候重定位)
push moudleID
jmp _dl_runtime_resolve() //调用动态链接器函数,在全局表查找该函数地址,并填充GOT表内容
五、动态链接下可执行文件和共享对象结构
.interp elf可执行文件专门用于存放动态连接器路径的段
.dynamic 保存动态链接器所需基本信息:文件依赖哪些共享对象,符号表位置,动态链接重定位表位置。与静态链接ELF“文件头”类似。
.synsym 动态符号表,只存放与动态链接相关符号,用于形成一张全局符号表
.rel.dyn 修正.got和数据段 .rel.plt 对函数引用.got.plt的修正
六、全局符号介入 -- 还有一个小问题
问题来源:新的共享对象装载时会将其符号表合并成全局符号表。如果两个共享对象定义了同一个符号?实际上动态链接器做了一下处理:当符号被加入全局符号表后,其后面的加入的符号将会被忽略(即全局变量介入现象)。如果共享对象A中定义的函数foo()被忽略,那么A中对foo()函数调用不能采用模块内部函数调用方式(实际上已经属于外部函数了。。。)
解决:统一将对内部函数的调用当做外部函数的调用看待。。。当然我们也可以在函数前添加static解决问题。。
七、显示运行时链接
想法:运行时加载,让程序在运行时自行控制加载/卸载指定模块。这种运行时加载的共享对象成为动态链接库(dynamic loading library)。
为此动态链接器专门提供了一些API用于动态库的装载。详见daimajishu.iteye.com/blog/1089674