linux下动态链接

linux下的动态链接

动态连接器ld.so

首先本文是对linux下的动态链接过程的探讨,由于需要我们再这里将.elf文件叫做可执行文件,将动态链接库.so文件叫做共享对象文件,他们两个统称为模块。首先是动态连接器,他其实就是一个.so文件,当程序将ld.so、共享对象文件、可执行文件装载进入虚拟内存之后。程序的控制权会先交给动态链接器,它做完相关的工作之后才会将控制权交还给程序,至于它做了什么接下来说。

地址无关代码技术

在讲动态链接之前我们不得不先讲解一下地址无关代码技术。当源程序被编译成目标文件.o时,如果加上相关的参数那么就会启用地址无关代码技术。可执行文件时-fPIE,动态链接库.so是-fPIC。

地址无关代码有四种情况 :模块内部的函数调用、跳转;模块内部的数据访问,比如全局变量、静态变量;模块外部的函数调用、跳转;模块外部的数据访问,比如其他模块中定义的全局变量。

第一种情况:这种情况最为简单,因为模块内部的函数调用就是相对地址的引用,本身就是地址无关代码。

第二种情况:对于数据之间的访问,由于不能直接访问数据所在的地址,所以要借助一些功能函数。

image-20230303194700729

这个函数就是对于数据访问的主要部分,__i686.get_pc_thunk.cx这个函数的作用就是把指令的返回地址放在了ecx中。偏移0x118c就是下一个位置对于代码段末的偏移,0x28就是数据对于data段起始位置的偏移。如果基地址是0x10000000,那么访问的地址就是0x10000000+0x454+0x118c+0x28=0x10001608这个地址所在的数据。

image-20230303195912929

第三种情况:这个时候需要借助got表(Global Offset Table),got表上存放的是全局变量的地址,然后模块在引用的时候就只需要引用got表里面的数据,而got表处在了data段,它与该模块的其他地址的相对地址不会改变,所以我们只需要找到got表的位置就行。那么这就和上一种情况完全一样了,就是模块内部访问数据。

第四种情况:这个和第三个一样,也是借助got表,不过这个是直接可以call一个相对地址,上面还得借助别的函数来实现相对地址的使用。

GOT表和PLT表

GOT表是存放其他模块里面符号的地址的,用来本模块访问其他模块的符号。got表分为两个部分,一个是保存全局变量引用的地址叫.got表,另一个就是用来保存函数引用的地址.got.plt表,他的前三项有点特殊。

plt表是为了延迟绑定技术而生的,因为我们将got表中的全部函数进行定位的工作量很大,效率很低。所以我们采用延迟绑定技术,只有在函数被调用的时候才会进行对got表进行定位。

/*
bar@plt:
jmp *(bar@GOT)
push n
push moduleID
jump _dl_runtime_resolve
*/

可执行文件在装载到虚拟地址的时候jmp *(bar@GOT)并不会初始化为跳到GOT表,而是会跳到push n这条指令,由函数_dl_runtime_resolve将GOT表中的地址进行重定位,之后再次调用的时候jmp *(bar@GOT)就会跳到GOT表里面,而不是push n。而那个函数的根据压入栈中的符号下标(也就是函数名),模块ID就可以完成GOT表的重定位。前面说到的.got.plt表的前三项其实就是这个函数调用的三个指令。

image-20230303202634825

动态链接相关的结构

.interp段,这个段的内容很简单就是一个字符串,这个字符串就是可执行文件所需要的动态链接器的路径。

.dynamic段,这个段在整个动态链接中都十分重要,它里面保存了,依赖于那些共享对象文件,动态链接符号表的位置,动态链接重定位表的位置,共享对象初始化代码的地址。他是一个结构体数组

typedef {
    ELF32_Sword d_tag,
    union {
        ELF32_Word d_val,
        ELF32_Addr d_ptr
    } d_un;
} ELF32_Dyn;

d_tag类型 d_und的含义
DT_STMTAB 动态链接符号表的地址,d_ptr表示的是.dynsym的地址
DT_STRTAB 动态链接字符串表的地址,d_ptr表示的是.dynstr的地址
DT_STRSZ 动态链接字符串表的大小,d_val表示大小
DT_HASH 动态链接哈希表的地址,d_ptr表示的是.hash的地址
DT_RPATH 动态链接共享对象搜索路径
DT_INIT 初始化代码的地址
DT_FINIT 结束代码地址
DT_NEED 依赖的共享文件对象,d_ptr代表的是他的文件名
DT_REL,DT_RELA 动态链接重定位表的地址

动态链接符号表.symtab,那些从动态链接库里面导入的函数,对于可执行文件来说是导入函数,对于共享对象文件来说是导出函数。为了表示这些模块之间的导入导出信息,ELF有一个动态符号表.dynsym,里面专门存放与动态链接相关的符号信息,而符号表symtab则是拥有所有的符号。.symtab的结构和静态链接的符号表几乎一样,重要的就是符号名和符号值。

动态链接重定位表.rel.dyn,.rel.plt分别是.got和.got.plt段的重定位表。动态链接对可执行文件的重定位其实就是对全局符号表的修改,当动态链接器需要进行重定位时,他会先查找printf的地址,“printf”位于“libc-2.6.1.so”中。假设链接器在so文件的全局符号表中找到的printf函数地址为0x08801234,那么链接器就会将这个地址填入到.got.plt相应的位置中去,从而实现了地址的重定位。

步骤和实现

首先会将可执行文件装载进虚拟内存,然后根据他的.dynamic段里面的信息将动态链接器装载进来。这个时候程序的控制权交给了链接器,链接器首先会进行自举,程序入口就是自举代码,将自己的got表全部重定位。之后同样时根据.dynamic里面的信息将可执行文件所涉及到的所有共享文件对象装载到虚拟内存,并收集他们的got表,之后根据他们的重定位表将合并的got表里面每个符号进行重定位。最后才会将控制权交给程序运行。

动态链接库

上面我们说到了,程序在开始执行前就把所有的共享文件对象全部装载到虚拟内存中。有一种更灵活的模块加载方式叫做显示运行时链接,有时候也叫做运行时加载。有一些共享文件对象不需要进行任何修改就可以进行运行时装载,这种共享文件对象被称为动态装载库这种文件和共享文件对象文件没有区别,只不过共享文件对象是在程序启动之前由动态链接器进行装载和链接,而动态链接库是由程序调用动态链接器一系列的API进行装载和链接。

#第一个函数dlopen(),它打开一个动态链接库,将他加载到进程的地址空间,完成初始化过程
void * dlopen(const char *filename,int flag);
#第一个参数是动态链接库的路径,flag是加载的方式,RTLD_LAZY是延迟绑定即PLT机制
#RTLD_NOW是加载完成之后就完成所有函数的绑定工作
#返回的是加载模块的句柄


#第二个函数是dlsym(),通过这个函数找到所需要的符号
void * dlsym(void * handle,char *symbol);
#第一个参数是dlopen函数返回的句柄,第二个参数是字符串
#如果找到了相应的符号就返回符号的值,函数就是地址,常量就是值。


#第三个函数是dlerror(),这个函数一般在其他三个函数调用后使用来判断是否调用成功
char *dlerror();
#如果返回的是NULL就说明上面一个函数调用成功,否则就会返回错误信息

#第四个函数是dlclose(),他就是将加载的模块卸载掉。
posted @ 2023-03-03 22:11  比翼飞  阅读(239)  评论(0编辑  收藏  举报