1、在现在的应用程序中,几乎所有的可执行文件都包括了对外部动态库的调用(除非在编译过程中加入-static选项)。因此动态链接器是ld学习的重点,但也是难点,要学习共享库和动态链接过程,如下三个概念必须了解,PIC的概念及GOT和PLT Section在ELF中的作用。
2、PIC(Postion Independent Code):为了使共享库映射到任何地址都不影响到函数及数据的引用,共享库的代码和数据需要具有PIC特性为了支持PIC的特性,对本模块或者外部模块函数的调用及数据的访问都不使用绝对地址,而是使用相对地址。给编译器-fPIC选项则可以编译出PIC属性的.so代码。
3、GOT(Global Offset Table) 为了支持PIC,共享库对函数和数据的访问使用相对于GOT表入口的地址的偏移进行访问,GOT表存放在数据段中,在编译后代码段或数据段到GOT表的偏移是位置独立的,也即是固定的,不管代码数据段映射到哪里。在需要链接动态库的可执行文件或共享库文件中都在数据段中包括有该GOT表,它其实存放的就是一个地址,所以占4字节。而对于对外部模块函数的访问则还需要引入另外一个特殊的机制:PLT。
4、PLT(Procedure Linkage Table):每个PLT其实是一个小代码段,在符号还没有得到解析前,它的第一条指令是一条跳转指令直接跳转到对应的GOT项中所包含的地址,而这个时候GOTn被初始化为PLTn的第二条指令的地址,因此又回到了PLTn代码中继续进行执行,而这个时候执行的是一条pushl指令,把该函数在重定位PLT section(.rel.plt)的偏移offset压入堆栈而rel.plt中包括了两个字段,一个字段指向该函数在符号表中的offset和该符号对应的GOT的下标。然后jmp到PLT0代码中去而PLT0代码包括两条指令,一条是pushl指令把GOT[1]的内容放入到堆栈中去,然后就是直接jmp到GOT[2]中指定的地址中去而这个地方放的是_dl_runtime_resolve的地址,因此根据压入的两个参数调用该函数,该函数会把解析后的函数地址放入到参数指定的GOT中去。
5、由此可见动态链接的过程是这样的:静态链接程序判断编译的可执行文件中包含了对外部动态库函数或数据的引用,则在代码段和数据段中分别初始化PLT和GOT表,然后替换代码段中所有对外部函数的调用为一条jump指令到PLTn,而PLTn代码中又会jump到GOT中指定的地址。如果这个函数不是第一次调用(说明以前已经进行过解析了)则直接跳到一个绝对地址去,这个绝对地址就是这个库被映射到本进程中的地址。如果这个函数是第一次调用,则PLTn中的指令会向下执行,最后跳转到PLT0中去执行符号解析函数。
符号解析函数是动态链接程序ld.so的一个函数,它会根据压入栈的信息,找到该外部函数对应的实际映射的地址,然后把该地址放入到相应的GOT项中去。这也就是所谓的lazy linking过程。
6、几个特殊的GOT和PLT;动态链接器在初始化每个需要映射的动态库时会初始化几个特殊的GOT.
GOT[0]存放dynamic section的地址;
GOT[1]是一个重要数据结构link_map的的地址(也叫库映射数据结构),该链表维护了程序中使用的库的信息,通过该信息可以查找到待解析符号的地址信息。
GOT[2]存放符号解析函数的地址,因此通过传递的两个参数就可以把解析的结果放入到正确的GOT表项中。
PLT0则是一小段(包括两条指令)的特殊代码,压入GOT[1]的内容,并跳转GOT[2]地址去。如下是示例代码。
.PLT0: pushl 4(%ebx) /* GOT表的地址保存在寄存器ebx中 */
jmp *8(%ebx)
nop; nop
nop; nop
.PLTn: jmp )
pushl $offset
jmp
7、动态库在启动时隐式加载:程序在启动的时候,操作系统先映射动态链接器到指定空间,然后动态链接器进行自己的初始化过程包括对自己引用的符号的重定位和解析过程。接着初始化一张全局符号表链表,这张链表会把可执行文件,动态链接程序及其他所有的动态库的符号表链接起来,并且通过每个文件中的hash sections加速以后符号的查找过程。最后动态链接器从可执行文件的dynamic段中找到所有需要加载的库文件按照一定的搜索顺序按照下面的方法进行逐个地加载。
动态链接器ld.so先调用_mmap对动态库所有的PT_LOAD段进行映射,得到各个段在进程中的虚拟地址。然后处理dynamic段中包括动态链接需要用的的信息,把这些信息放入到link_map链表相关字段中去。初始化GOT[0/1/2];之后调用各动态库的初始化函数进行各库自己的初始化过程。之后跳转到程序入口(文本段的第一条指令,也是用户空间的第一条指令,最后才是执行到main函数)
8、动态库在运行时的显示加载:上面介绍的加载和动态链接过程都是操作系统kernel隐式的处理过程。而用户空间的程序也可以实现这些过程。这依赖于动态链接器提供的几个函数:dlopen()用来动态地加载动态库然后进行初始化,完成上面7的动作;dlsym()则完成6的动作,但直接把得到的结果返回给用户程序,由用户程序直接去调用该函数。
阅读(1575) | 评论(0) | 转发(0) |