动态链接共享库,是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来,这个过程称为动态链接。这部分工作由动态链接器完成。
动态链接是在程序加载或者程序运行时来完成的,在编译包含动态链接库的可执行文件时,没有任何的代码或者数据节真的被复制到可执行文件当中。链接器复制了一些重定位和符号表信息,使得运行时可以解析对动态库中代码和数据的引用。
当加载器加载和运行可执行文件时,它会注意到该可执行文件包含一个.interp节,这一节包含动态链接器的路径名,动态链接器本身就是一个共享目标(linux上为ld-linux.so)。加载器不会像平常一样将控制传递给引用,而是加载和运行这个动态链接器,然后动态链接器执行下面的重定位任务来完成链接任务:
-
重定位各个.so文件的文本和数据到某个内存段
-
重定位可执行文件中所有由.so文件定义的符号和引用
-
动态链接器将控制传递给应用程序。从这个时候开始,共享库的位置就固定了,并且在程序执行的过程中都不会变
应用程序直接加载和链接共享库的相关调用如下:
-
#include <dlfcn.h>
-
void *dlopen(const char *filename, int flag);
-
void *dlsym(void *handle, char *symbol);// 获取动态库中的符号
-
int dlclose(void *handle);
-
const char *dlerror(void);
位置无关代码——PIC
可以加载而无需重定位的代码称为位置无关代码,对同一个目标模块中符号的引用是不需要特殊处理,使之称为PIC,可以用PC相对寻址来编译这些引用,构造目标文件时由静态链接器重定位。
PIC数据引用:
编译器通过运用以下这个有趣的事实来生成对全局变量的PIC引用:无论我们在内存中的任何一处加载一个目标模块,数据段和代码段的距离总是保持不变。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量。
想要生成对全局变量PIC引用的编译器利用了上面的事实,他在数据段开始的地方创建了一个表,叫GOT,在GOT中,每一个被这个目标模块引用的全局数据目标都有一个8字节条目。编译器还为GOT中每一个条目生成一个重定位记录。在加载时,动态链接器就会重定位GOT当中的每个条目,使得它包含目标的正确的绝对地址
PIC函数调用
延迟绑定:将过程地址的绑定推迟到第一次调用该过程时
延迟绑定是通过两个数据结构之间的交互来实现的:.got.plt和.plt。如果一个目标模块调用定义在共享库中的任何函数,那么它就得有自己的.got.plt 和.plt
-
plt是一个数组,每个条目是16字节代码。plt[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有自己的plt条目,plt[1]调用启动main函数,从plt[2]开始就是用户代码调用的函数了
-
.got.plt是一个数组,每个条目8字节。和plt联合使用时got[0]和got[1]包含动态链接器在解析函数地址时会用到的信息。got[2]是动态链接器在ld-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址要在运行时被解析。每个条目都有一个相匹配的plt条目。初始时,每个got条目都指向对应plt条目条目的第二条指令。
当一个共享库的函数被第一次调用时,延迟解析它的运行时地址的步骤如下:
-
不直接调用,程序进入对应的PLT条目
-
每个plt条目的第一条指令就是一个间接跳转,跳转到对应的got条目,现在对应的got条目是初始值,即指向plt条目的第二条指令,所以现在只是间接的回到了plt项的第二条指令
-
将函数id压栈,执行plt[0], 即动态链接器
-
动态链接器通过got中与其相关的几项,来找到指定函数的运行时位置,然后用这个位置重写got对应的项的内容,再把控制转还给相应的函数
这样,在后续的调用该函数的时候,直接plt--->got就可以了,不用再到ld-linux.so动态链接库当中去找了。
阅读(1636) | 评论(0) | 转发(0) |