文章详情

  • 游戏榜单
  • 软件榜单
关闭导航
热搜榜
热门下载
热门标签
php爱好者> php文档>Intel平台下Linux中ELF文件动态链接的加载、解析..

Intel平台下Linux中ELF文件动态链接的加载、解析..

时间:2007-04-09  来源:Echo CHEN

但有一个特别要指出的是PT_LOAD的那些,把所有的可以加载的节都在加载的数据结构中loadcmds中构建完成,是一个好的想法。特别是指针的妙用,值得学习(1467 c = &loadcmds[nloadcmds++];)。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd() 1498 c = loadcmds; ………… 1501 maplength = loadcmds[nloadcmds - 1].allocend - c->mapstart; 1502 1503 if (__builtin_expect (type, ET_DYN) == ET_DYN) 1504 { ……………. 1521 l->l_map_start = (ElfW(Addr)) __mmap ((void *)0, maplength, 1522 c->prot, MAP_COPY | MAP_FILE, 1523 fd, c->mapoff); 1524 1525 l->l_map_end = l->l_map_start + maplength; 1526 l->l_addr = l->l_map_start - c->mapstart; ……….. 1535 __mprotect ((caddr_t) (l->l_addr + c->mapend), 1536 loadcmds[nloadcmds - 1].allocend - c->mapend, 1537 PROT_NONE); 1538 1539 goto postmap; 1540 } 

在1521-1526行之间就是把整个文件都进行了映射,妙处在1498行与1501行,是把头与尾的两个PT_LOAD program header 的内容都计算在内了。而1503行就是我们这里的情景,因为这是动态链接库的加载。而1535行的修改虚拟内存的属性,就是把映射在最高地址的空白失效。这是一种保护。为了防止有人利用这里大做文章。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd() 1546 while (c < &loadcmds[nloadcmds]) 1547 { 1548 1549 postmap: 1550 if (l->l_phdr == 0 1551 && (ElfW(Off)) c->mapoff <= header->e_phoff 1552 && ((size_t) (c->mapend - c->mapstart + c->mapoff) 1553 >= header->e_phoff + header->e_phnum * sizeof (ElfW(Phdr)))) …… 1555 l->l_phdr = (void *) (c->mapstart + header->e_phoff - c->mapoff); 1556 1557 if (c->allocend > c->dataend) 1558 { ……….. 1561 ElfW(Addr) zero, zeroend, zeropage; 1562 1563 zero = l->l_addr + c->dataend; 1564 zeroend = l->l_addr + c->allocend; 1565 zeropage = ((zero + GL(dl_pagesize) - 1) 1566 & ~(GL(dl_pagesize) - 1)); 1567 1568 if (zeroend < zeropage) ………. 1571 zeropage = zeroend; 1572 1573 if (zeropage > zero) 1574 { ……. 1576 if ((c->prot & PROT_WRITE) == 0) 1577 { 1578 /* Dag nab it. */ 1579 __mprotect ((caddr_t) (zero & ~(GL(dl_pagesize) 1580 - 1)), GL(dl_pagesize), 1581 c->prot|PROT_WRITE) < 0); 1582 1583 } 1584 memset ((void *) zero, '\0', zeropage - zero); 1585 if ((c->prot & PROT_WRITE) == 0) 1586 __mprotect ((caddr_t) (zero & ~(GL(dl_pagesize) - 1)), 1587 GL(dl_pagesize), c->prot); 1588 } 1589 1590 if (zeroend > zeropage) 1591 { …….. 1593 caddr_t mapat; 1594 mapat = __mmap ((caddr_t) zeropage, zeroend - zeropage, 1595 c->prot, MAP_ANON|MAP_PRIVATE|MAP_FIXED, 1596 ANONFD, 0); 1597 1598 } 1599 } 1600 1601 ++c; 1602 } 

这里所作的与上面的相类似,根据在前面从PT_LOAD program header 得到的文件映射的操作属性进行修改,但在zeroend>zerorpage的时候不同,把它映射成为进程独享的数据空间。这也就是一般的初始化数据区BSS的地方。因为zeroend是在文件中的映射的页面对齐尾地址,而zeropage是文件中的内容映射的页面对齐尾地址,这其中的差就是为未初始化数据准备的,这在1593-1597行之间体现,要把它的属性改成可写的,且全为0。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd() 1606 if (l->l_phdr == NULL) 1607 { …….. 1611 ElfW(Phdr) *newp = (ElfW(Phdr) *) malloc (header->e_phnum 1612 * sizeof (ElfW(Phdr))); 1613 1614 l->l_phdr = memcpy (newp, phdr, 1615 (header->e_phnum * sizeof (ElfW(Phdr)))); 1616 l->l_phdr_allocated = 1; 1617 } 1618 else 1619 /* Adjust the PT_PHDR value by the runtime load address. */ 1620 (ElfW(Addr)) l->l_phdr += l->l_addr; 

把phdr 就是program header 也纳入struct link_map的管理之中,一般的情况是不会有的,所以要copy过来。

 _dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd() 1625 elf_get_dynamic_info (l); 

这里调用的函数elf_get_dynamic_info是在加载过程中最重要的一个之一,因为在这之后的几乎所有的对动态链接管理的内容都要用要与这里的l_info数据组相关。

 _dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd() >> elf_get_dynamic_info() 2826 static inline void __attribute__ ((unused, always_inline)) 2827 elf_get_dynamic_info (struct link_map *l) 2828 { 2829 ElfW(Dyn) *dyn = l->l_ld; 2830 ElfW(Dyn) **info; 2831 2832 2833 info = l->l_info; 2834 2835 while (dyn->d_tag != DT_NULL) 2836 { 2837 if (dyn->d_tag < DT_NUM) 2838 info[dyn->d_tag] = dyn; …………… 2853 ++dyn; 2854 } …………. 2858 if (l->l_addr != 0) 2859 { 2860 ElfW(Addr) l_addr = l->l_addr; 2861 2862 if (info[DT_HASH] != NULL) 2863 info[DT_HASH]->d_un.d_ptr += l_addr; 2864 if (info[DT_PLTGOT] != NULL) 2865 info[DT_PLTGOT]->d_un.d_ptr += l_addr; 2866 if (info[DT_STRTAB] != NULL) 2867 info[DT_STRTAB]->d_un.d_ptr += l_addr; 2868 if (info[DT_SYMTAB] != NULL) 2869 info[DT_SYMTAB]->d_un.d_ptr += l_addr; ………………. 2874 ………… 2876 if (info[DT_REL] != NULL) 2877 info[DT_REL]->d_un.d_ptr += l_addr; …………. 2879 2880 if (info[DT_JMPREL] != NULL) 2881 info[DT_JMPREL]->d_un.d_ptr += l_addr; 2882 if (info[VERSYMIDX (DT_VERSYM)] != NULL) 2883 info[VERSYMIDX (DT_VERSYM)]->d_un.d_ptr += l_addr; 2884 } …………. 2889 } 

上面的__attribute__ 中的unused 是为了消除编译器在-Wall 情况下对于其中可能没有用到在函数中的局部变量发出警告,而alwayse_inline,很好解释,就是内联函数的强制标志。

2829行的l->l_ld是在前面的__dl_map_object_from_fd中的1455被给定的。也就是所有关于动态链接节的所在地址(参看 附录B中的解释)。

很明显在2835至2854行之间的循环就是把l_info的内容都填充好。 这为之后有很大的作用,因为这些节是可以找到如函数名与定位信息的,这里的的妙处是把数组的偏移量与d_tag相关联,代码简洁。

2856至2885便是对动态链接库的调整过程(这里调整的每一个节都是与函数解析有重要关系的,详细内容可参看 附录A),如果我们考虑的更远一点,在前面的函数中的1521行一开始把整个文件连续的映射入内存,在这里就很好的得到解释,如果不是连续的,就没有办法在这里作一个统一的调整了。

 _dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd() 1662 /* Finally the file information. */ 1663 l->l_dev = st.st_dev; 1664 l->l_ino = st.st_ino; 1667 return l; 1670 } 

最后就是把设备号与节点号加入就完成了最后的dl_map_object就行了,回头看1414行中对已经加载的文件的搜索,就可以明白这里的作用了。

再回到dl_open_worker中

_dl_open() >> dl_open_worker() 2550 /* It was already open. */ 2551 if (new->l_searchlist.r_list != NULL) 2552 { ……. 2556 if ((mode & RTLD_GLOBAL) && new->l_global == 0) 2557 (void) add_to_global (new); 2558 2559 /* Increment just the reference counter of the object. */ 2560 ++new->l_opencount; 2561 2562 return; 2563 } 

这就是对已经被打开了的,就对l_opencount加一返回了。但为什么要在2551行之后作出这一判断呢,那是在下面的代码有关,_dl_map_object_deps会把l_searchlist加载入。

 _dl_open() >> dl_open_worker() 2565 /* Load that object's dependencies. */ 2566 _dl_map_object_deps (new, NULL, 0, 0, mode & __RTLD_DLOPEN); …………… 2573 l = new; 2574 while (l->l_next) 2575 l = l->l_next; 2576 while (1) 2577 { 2578 if (! l->l_relocated) 2579 { 2580 _dl_relocate_object (l, l->l_scope, lazy, 0); 2581 } 2582 2583 if (l == new) 2584 break; 2585 l = l->l_prev; 2586 } 

在这里的_dl_map_object_deps会填充l_searchlist.r_list,对于这个函数与下面的_dl_relocate_object由于与函数的解析关系比较大,所以我放在《Intel平台下linux中ELF文件动态链接的加载、解析及实例分析(中)-----------函数解析与卸载篇》讲解。但可以把这个当作这个新加载的动态链接库的所依赖的动态链接库的struct link_map* 放入这个指针的列表中(就是l_search_list中),_dl_relocate_object是对这个动态链接库中的函数重定位,而这里用的,这里之所以用的是while (1) 2576行,是因为在前面用的_dl_map_object_deps会把这个动态链接库所依赖的动态链接库也加载进来,这其中就会有没有重定位的。

_dl_open() >> dl_open_worker() 2592 for (i = 0; i < new->l_searchlist.r_nlist; ++i) 2593 if (++new->l_searchlist.r_list[i]->l_opencount > 1 2594 && new->l_searchlist.r_list[i]->l_type == lt_loaded) 2595 { 2596 struct link_map *imap = new->l_searchlist.r_list[i]; 2597 struct r_scope_elem **runp = imap->l_scope; 2598 size_t cnt = 0; 2599 2600 while (*runp != NULL) 2601 { ………… 2605 if (*runp == &new->l_searchlist) 2606 break; 2607 2608 ++cnt; 2609 ++runp; 2610 } 2611 2612 if (*runp != NULL) 2613 /* Avoid duplicates. */ 2614 continue; ………… 2642 imap->l_scope[cnt++] = &new->l_searchlist; 2643 imap->l_scope[cnt] = NULL; 2644 } 

这段代码如果从实现功能上来讲是很简单的,就是在我们刚新加入的动态链接库new中的l_searchlist中(这些都是在前面被dl_object_deps加载入的被依赖的动态链接库数组)imap->l_scope查找,如果里面runp有&new->l_searchlist,就不用对原来的imap->l_scope扩充了,但如果没有就要完成2616到2644行的扩充工作。

但在这之后的背景原因,却是&new->l_searchlist其实就是new本身。在一般情况下,如果这个依赖的动态链接库在new被加载之前已经加载(具体的原因会在下一篇文章关于动态链接库函数解析中说明),那就会遇到这种情况。而我们又不能保证两个动态链接库之间的互相依赖情况的发生,如下图,那这里的解决办法便是一个补救措施了。




_dl_open() >> dl_open_worker() 2647 _dl_init (new, __libc_argc, __libc_argv, __environ); 

这是要调用动态链接库自备的初始函数。这有点类似与insmod时调用的init_module的内容。至于这其中所传递的__libc_argc, __libc_argv, __environ三个参数是在你的可执行文件被运行的时候由bash引入的输入参数与环境变量,一般的动态链接库是没有什么用处了。

_dl_open() >> dl_open_worker() >> _dl_init() 1118 void 1119 internal_function 1120 _dl_init (struct link_map *main_map, int argc, char **argv, char **env) 1121 { 1122 1123 ElfW(Dyn) *preinit_array = main_map->l_info[DT_PREINIT_ARRAY]; 1124 ElfW(Dyn) *preinit_array_size = main_map->l_info[DT_PREINIT_ARRAYSZ]; 1125 unsigned int i; 1126 1127 1128 ElfW(Addr) *addrs; 1129 unsigned int cnt; 1130 1131 1132 addrs = (ElfW(Addr) *) (preinit_array->d_un.d_ptr + main_map->l_addr); 1133 for (cnt = 0; cnt < i; ++cnt) 1134 (init_t) addrs[cnt]) (argc, argv, env); …………. 1146 i = main_map->l_searchlist.r_nlist; 1147 while (i-- > 0) 1148 call_init (main_map->l_initfini[i], argc, argv, env); 1149 1150 1151 1152 1153 } 

先是调用 DT_PREINIT的内容,这是在init之的init方法。我想这个之所以要实现,不光是为让动态链接库的开发者有更好的开发接口,而且还是在以它所依赖的动态链接库之前进行一些初始化工作,借鉴于面向对象的构造函数。

 _dl_open() >> dl_open_worker() >> _dl_init() >> call_init() 1072 static void 1073 call_init (struct link_map *l, int argc, char **argv, char **env) 1074 { 1075 1076 if (l->l_init_called) 1078 return; 1079 1082 l->l_init_called = 1; ……….. 1089 if (l->l_info[DT_INIT] != NULL) 1090 { 1091 init_t init = (init_t) DL_DT_INIT_ADDRESS(l, l->l_addr + l->l_info[DT_INIT]->d_un.d_ptr); 1092 1093 /* Call the function. */ 1094 init (argc, argv, env); 1095 } 1098 ElfW(Dyn) *init_array = l->l_info[DT_INIT_ARRAY]; 1099 if (init_array != NULL) 1100 { 1101 unsigned int j; 1102 unsigned int jm; 1103 ElfW(Addr) *addrs; 1104 1105 jm = l->l_info[DT_INIT_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr)); 1106 1107 addrs = (ElfW(Addr) *) (init_array->d_un.d_ptr + l->l_addr); 1108 for (j = 0; j < jm; ++j) 1109 ((init_t) addrs[j]) (argc, argv, env); 1110 } 1111 1112 1113 } 

1076-1082行的内容一看便知,是防止两次初始化。下面是对DT_INIT与DT_INIT_ARRAY的函数调用,值得注意的是,前面调用call_init时是对l_initfine的数组进行的,这里就包括了这个新的动态链接库所依赖的。就这样完成了dl_open_worker()这个过程。

到此,我们至少大致上已经把动态链接库的过程说了一遍(当然,除了_dl_map_object_deps和_dl_relocate_object)到现在我们已经明白了以下几点:

1、 动态链接库的struct link_map* 的产生与组织过程(这个在_dl_new_object中实现)

2、 动态链接库是如何被提取信息入struct link_map*中的,并被加载的(这个在open_verify 与dl_map_object_from_fd,elf_get_dynamic_info这三个函数中实现)

3、 动态链接库本身的初始化过程(这个在_dl_init中实现)

总体上函数调用结构在下图中一个示意图。




但还有几个问题没有被提到

1、 可执行文件中的函数被如何定位到动态链接库的函数体中的。

2、 一个动态链接库与依赖的动态链接库之间是什么关系,它们之间是如何联系。

3、 一个函数是怎样被动态解析,它又是使函数调用方与实现方成为一体的。

这些问题我会在《Intel平台下linux中ELF文件动态链接的加载、解析及实例分析(中)-----------函数解析与卸载篇》进行阐明,敬请期待。





回页首


附录A:动态链接section 类型及说明

类型 数值 d_un所指 EXEC可选性 DYN可选性 说明
DT_NULL 0 不用 必须 必须 这个表示动态链接section的结束标志
DT_NEEDED 1 d_val 可选 可选 这个节d_val是包含了以null结尾的字符串,这些字符串是这个动态链接文件或可执行文件的依赖文件名称与路径的节的开始地址
DT_PLTRELSZ 2 d_val 可选 可选 这里的d_val是过程链接表(procedure linkage table)的大小,它与DT_JMPREL结合使用
DT_PLTGOT 3 d_ptr 可选 可选 这里的d_ptr是过程链接表或全局偏移量表的起始地址。
DT_HASH 4 d_ptr 必须 必须 这里的d_val是符号哈希表的起始地址。
DT_STRTAB 5 d_ptr 必须 必须 这里d_ptr所给出的是符号名称字符串表的起始地址。
DT_SYMTAB 6 d_ptr 必须 必须 这里的d_ptr是Elf32_sym数据结构在的节表中的起始地址。
DT_STRSZ 10 d_val 必须 必须 这d_val是上面的DT_STRTAB节的大小。
DT_SYMENT 11 d_val 必须 必须 这里的d_val是DT_SYMTAB中的每个Elf32_Sym数据结构的大小
DT_INIT 12 d_ptr 可选 可选 这里的d_ptr是一个动态链接库被加载时调用的初始函数所在节的起始地址。
DT_FINI 13 d_ptr 可选 可选 这里的d_ptr是一个动态链接库被卸载时,调用解构函数所在节的起始地址。
DT_REL 17 d_ptr 必须 可选 这里的d_ptr与上面的DT_RELA相似,是Elf32_Rel数据结构所在节的起始地址,它在intel平台下用。
DT_RELSZ 18 d_val 必须 可选 这d_val与上面的DT_REL上面的相对应,表明上面的那个节的大小。
DT_RELENT 19 d_val 必须 可选 这里的d_val是DT_REL中的一个Elf32_Rel的数据结构的大小。
DT_PLTREL 20 d_val 可选 可选 这里的d_val是与过程链接表(procedure linkage table)有关的,就是DT_REL 或DT_RELA的值,也就是这个ELF文件用的是DT_REL的话那d_val就是17,而如果是DT_RELA的话就是7
DT_JMPREL 23 d_ptr 可选 可选 这是我们这里最重要的Elf_Dyn,因为d_ptr所指的就是GOT(global object table)全局对象表,这其实是一个导入函数与全局变量的地址表。
DT_INIT_ARRAY 25 d_ptr 可选 可选 这里的d_ptr是要初始化函数跳转表起始相对地址。
DT_FINI_ARRAY 26 d_ptr 可选 可选 这里的d_ptr是要解构时调用的函数跳转表起始相对地址。
DT_INIT_ARRAYSZ 27 d_val 可选 可选 这里的d_val表明前面的DT_INIT_ARRAY的大小。
DT_FINI_ARRAYSZ 28 d_val 可选 可选 这里的d_val是前面的DT_FINI_ARRAY的大小。
DT_ENCODING 32 d_val或d_ptr 没有规定 没有规定 现在这个节还没有规定,但很明显就是为以后的加密而准备的。
DT_PREINIT_ARRAY 32 d_ptr 可选 不用 这里d_ptr是在调用main函数之前的调用初始函数跳转表的起始地址。
DT_PREINIT_ARRAYSZ 33 d_val 可选 不用 这里的d_val是前面的DT_PREINIT_ARRAY的大小

上面只列出了在我们这里要用到的项目,而ELF文件规范的设计者还为它留下了可以在不同的系统与平台中独自享用的项目,这里不列出了。





回页首


附录B:动态链接库program header 类型的说明

名称 说明
PT_NULL 0 这是program header 数组的分界标志符。
PT_LOAD 1 这个标志说明它所指的文件内容要被加载到内存单元,加载的内容由p_offset(在ELF文件中的偏移量)p_filesz(被加载的内容在文件中的大小)。而加载的要求是p_vaddr(被建议的加载的开始地址)p_memsz(被加载的建议内存大小)
PT_DYNAMIC 2 表示它所对应的dynamic section 内容,也就是在 附录A中所有的Elf32_Dyn数据结构所在的program heaer
PT_INTERP 3 这里所指的是一个字符串,它指的是为加载可执行文件而用的动态链接库名称,在linux下,这是/lib/ld-linux.so.2
PT_NOTE 4 为软件开发商加入标识而用的,表明软件的开发说明。
PT_SHLIB 5 这是为日后的扩充面预留。
PT_PHDR 6 表示program header array自身在内存中的映射地址与大小。


参考资料

  • John Levine "Linkers and Loaders" (是对动态链接的一般性理论作了一个概观介绍)可以在以下的网址上看到它的网络版 http://www.iecc.com/linker/

  • Executable and Linkable Format (ELF) (这专门介绍ELF文件格式的ABI的好文章,网络版在 www.skyfree.org/linux/references/ELF_Format.pdf可以得到)

  • glibc2-3-2版本 本文的源代码来源。可以在 ftp://ftp.gnu.org中下载而得。
相关阅读 更多 +
排行榜 更多 +
盛世天下

盛世天下

角色扮演 下载
镇魂街破晓手游

镇魂街破晓手游

角色扮演 下载
磨碎物品

磨碎物品

休闲益智 下载