动态链接
静态链接所有模块的地址在编译时候就是已经确定的了,而动态链接中共享对象(.so结尾的文件)的最终地址在编译时是不确定的,而是在装载时确定。动态链接的基本思想就是把链接这个过程推迟到运行时再进行。产生共享对象的编译命令如下:
gcc -fPIC -shared -o Lib.so Lib.c
为了实现动态链接,首先遇到的问题就是共享对象地址的冲突问题,由于程序模块的指令和数据可能会包含一些绝对地址的引用,我们再在链接产生输出文件的时候,就要假设模块被装载的目标地址,为了能够使共享对象在任意地址装载,我们需要在链接时对所有绝对地址的引用不作重定位,而把这一步推迟到装载时再完成。一旦模块装载地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址引用进行重定位。
总结一下:静态链接采用链接时重定位,动态链接采用装载时重定位,但是这种方法需要修改指令,所以没法做到一份指令被多个进程共享,这也是上面-shared的参数的作用,而-fPIC就是为了解决这个问题而引出来的一种技术,叫做地址无关代码技术。
PIC技术:把指令中那些需要被修改的部分分离出来和数据部分放在一起,这样指令部分就可以保持不变了,而数据部分每个进程都有一个副本。
先来分析各种地址引用方式:
第一种:模块内部的函数调用;
第二种:模块内部的数据访问;
第三种:模块间的函数调用;
第四种:模块间的数据访问;
也许 第一种和第二种 你已经知道了怎么解决。这个不需要进行重定位,那个只是一个相对地址引用。而模块间的访问就要麻烦一点啦。因为访问模块间的数据要等到装载才能决定,我们要使的代码地址无关,基本思想就是把和地址相关的部分放到数据段里面,很明显,这些其他模块的变量地址是和模块装载地址有关的。
那么第三种和第四种怎么解决呢?ELF的做法是在数据段里面建立一个指向这些变量的指针数组,也被称为全局偏移表(GOT)。
当指令中需要访问外部变量时,会先找到GOT,然后根据GOT中变量所对应的项找到变量的目标地址。链接器在装载模块时会查找每个变量所在的地址,然后填充GOT中每个项。由于GOT是放在数据段的,所以每个进程可以有一个独立的副本。
PIC技术还没有完。上面所说的都是在共享库中的情况,但是在可执行文件还是这样吗?程序主模块的代码并不是地址无关代码,也就是说代码不会使用类似PIC的机制。它访问外部全局变量的方式和普通数据访问方式是一样的。所以变量的地址在链接的过程中就要定下来。为了能使链接过程正常进行,链接器会在创建可执行文件时在它的.BSS段创建一个变量的副本。那么问题就很明显了,现在变量定义在共享对象中,而在可执行文件的.BSS段也有一个副本,这样在运行过程中肯定是不可行的。
为了解决这个问题,那就是将所有的使用这个变量的指令都指向可执行文件中的那个副本。ELF共享库在编译时就默认把定义在模块内部的全局变量当作定义在其他模块的全局变量。当共享库在装载时,如果全局变量在可执行文件中拥有副本,那么动态链接器就会把GOT中的相应地址指向该副本,这样该变量在运行时实际上只有一个实例。如果该变量在 共享模块中被初始化,那么动态链接器还需要将该值复制给程序主模块中的变量副本。
显式运行时链接
支持动态链接的系统往往都支持一种更加灵活的模块加载方式---运行时加载。也就是让程序自己在运行时控制加载指定的模块,并且在不需要该模块时将其卸载。动态库的装载通过以下4个函数:打开动态库(dlopen)、查找符号(dlsym)、错误处理(dlerror)以及关闭动态库(dlclose)。这几个API的实现是在/lib/libdl.so.2里面,头文件定义在<dlfcn.h>。
dlopen:打开一个动态库,并将其加载到进程的地址空间,完成初始化过程
void * dlopen(const char *filename, int flag);
第一个参数是被加载的动态库路径
第二个参数表示符号的解析方式,常量RTLD_LAZY表示使用延迟绑定,当函数第一次被调用时才进行绑定,即PLT机制;而RTLD_NOW表示模块被加载时即完成所有的函数绑定工作。
返回值是被加载的模块的句柄。
dlsym:运行时装载的核心部分,可以通过这个函数找到所需的符号。
void * dlsym(void *handle, char *symbol);
第一个参数是dlopen返回的动态库句柄
第二个参数就是所要查找的符号的名字,如果查找的符号是函数,那么它返回函数的地址;如果是个变量,返回变量地址;如果是常量,那么返回该常量的值,如果是NULL或者0,那么需要借助dlerror来判断
dlerror:每次调用dlopen、dlsym或者dlclose后,都可以用dlerror来判断上一次调用是否成功,返回NULL则成功
dlclose:卸载一个已经加载的模块

浙公网安备 33010602011771号