http://hi.baidu.com/hust_chen/blog/item/54a8c516231d0c0ec93d6d3e.html
表面上看,动态链接库(dll)的加载及解析是一个十分繁复的过程,其中牵涉到的数据结构及其之间的关系也让人望而生畏。Whatever,学习这 事情,说到底是没有捷径可走的,除了死啃仅有的一些资料,如ELF format、Loaders and Linkers,借用objdump及gdb来跟踪程序的执行流程,对于理解过程也是颇有帮助的,anyway,不嫌烦就好了。 ·DT_NEEDED:d_tag=1,d_val中的值为needed library的name在string table中偏移量。如果所需要动态库的数量为n个,则相应类型为DT_NEEDED的entry也应该有n条; ·DT_PLTGOT:d_tag=3,d_ptr中的值为.plt(procedure linkage table)或.got(global offset table)的起始地址,在MIPS体系下,这个值是指向.got的起始地址; ·DT_HASH:d_tab=4,d_ptr中的值为hash table的起始地址; ·DT_STRTAB:d_tab=5,d_ptr中的值为string table的起始地址; ·DT_SYMTAB:d_tab=6,d_ptr中的值为symbol table的起始地址; ·DT_RPATH:d_tab=15,d_val中的值为needed library所在的路径名称(注:needed library不一定就会在这个路径下,detail参见dll的加载过程)在string table中的起始地址。 这里面牵涉到了一些重要的数据结构: (1) hash table:其作用是根据symbol name找到其在symbol table中的位置。简单地说,是以symbol name作为查找的key,返回指向symbol table的index(表示第index个symbol entry)。当然,实际的做法没有这般简单,因为要考虑到hash table的地址冲突问题。hash table包括两部分:bucket与chain。假设x代表symbol name的值,y=bucket[x%nbucket],如果y所指向的symbol entry不是所求的entry,那么就得利用chain[y]来查找下一个index,直到成功为止。 这里还牵涉到一个问题:如何得知y所指向的symbol entry是否所求的entry?一个显然的答案是用已知的symbol name与该symbol entry所对应的name作匹配,但因为symbol table中的entry并没有包含symbol name这个字段,所以必须要借助string table来得出其所对应的symbol name(参见下面)。 (2) symbol table:其保存的是符号的相关信息。其中,比较重要的是st_name与st_value。st_name是该entry所对应的name在string table中的偏移量;st_value是该symbol在代码段中的起始地址。 (3) string table:纯粹意义上的由一组字符串组成的表格。每个字符串以'/0'标记结束,特别注意的是,string table第一个元素的值为'/0'。 那为什么不直接把symbol name放到symbol table中,而间接用string table保存呢?因为symbol name的长度是不确定的,为了保证symbol table的每个entry的大小一致,便把symbol name移到string table中。
dll的加载过程: 根据DT_NEEDED可以得到所需dll的名称在string table中的偏移量,通过访问string table便可以得到dll的名称了。下一步便是找这些dll所在的路径,以便将其加载到内存中。1)搜索DT_RPATH所指向的路径(也是通过 string table来得到路径的名称);2)搜索环境变量中的路径;3)搜索/usr/lib及/lib。如果在这几个地方都找不到所需的dll,那么加载便失败 了。
dll函数的解析过程: 所谓的lazy mode,也就是当需要dll中的某个函数时方将其起始地址计算出来,而不是事先便把dll的所有函数的起始地址都计算好,这是因为并非dll中的每个函数都有机会被调用。 由于文件中可能多次调用同一外部函数,这便要考虑如何使得每个外部函数只被解析一次,而不是每调用一次便解析一次。实现的方法是利用.got 与.plt(或者是MIPS体系下的.MIPS.stubs)。.got是全局偏移量表,存放的是全局符号(自然包括所调用的外部函数的符号)所对应的二 进制代码在ELF文件中的起始地址。.plt是一段一段的小代码,每段小代码对应一个动态库中的函数符号,细节可以查看Loaders and Linkers。 大致的过程如下(以MIPS为例):调用外部函数的二进制代码中肯定有一句无条件转移语句,该语句表明程序会跳到got[n]所指向的地址中,在该函数符 号没有被解析前,这个地址是指向stub中相应的某段小代码(这是由编译器事先绑定好的),这段小代码中有个跳转语句,是转移到got[0]所指向的地 址,这个地址其实便是符号解析函数所在的起始地址,当完成符号解析这个工作后,便会把got[n]中的内容改为该函数符号的起始地址,这样,当第二次调用 该函数时,便会无须再进行第二次解析。相比之下,IA32的函数解析过程较为复杂,不过在很多地方都很找到相关资料。 至于如何计算函数的起始地址,well,通过hash table、symbol table、string table便可以了,不过前提是要搞清解析哪个函数符号,一般来说,这可以通过分析ELF文件来找出传递的参数,从而获取想要的信息。 当然,想偷懒也是可以的,C标准库提供了函数dlopen()、dlsym(),主要是用于解析函数符号的。 |