Chinaunix首页 | 论坛 | 博客
  • 博客访问: 7278908
  • 博文数量: 512
  • 博客积分: 12019
  • 博客等级: 上将
  • 技术积分: 6857
  • 用 户 组: 普通用户
  • 注册时间: 2005-08-01 16:46
文章分类

全部博文(512)

文章存档

2024年(2)

2022年(2)

2021年(6)

2020年(59)

2019年(4)

2018年(10)

2017年(5)

2016年(2)

2015年(4)

2014年(4)

2013年(16)

2012年(47)

2011年(65)

2010年(46)

2009年(34)

2008年(52)

2007年(52)

2006年(80)

2005年(22)

分类: LINUX

2007-12-13 15:13:23


动态链接,一个经常被人提起的话题。但在这方面很少有文章来阐明这个重要的软件运行机制,只有一些关于动态链接库编程的文章。本系列文章就是要从动态链接库源代码的层次来探讨这个问题。

当 然从文章的题目就可以看出,intel平台下的linux ELF文件的动态链接。一则是因为这一方面的资料查找比较方便,二则也是这个讨论的意思比其它的动态链接要更为重要(毕竟现在是intel的天下)。当 然,有了这么一个例子,其它的平台下的ELF文件的动态链接也就大同小异。你可以在阅读完了本文之后"举一隅,而反三隅"了。

由于这是一个 系列的文章,我计划分三部分来写,第一部分主要分析加载,涉及dl_open这个函数的内容,但由于这个函数所包含的内容实在太多。这里主要是它的 _dl_map_object与_dl_init这两个部分,因为这里是把动态链接文件通过在ELF文件中的得到信息映射到内存空间中,而 _dl_init中是一个特殊的初始化。这是对面向对象的函数实现的。

第二部分我将分析函数解析与卸载,这里要讲的内容会比较多,但每一个 内容都不会多。首先是在前一篇中没有说完的dl_open中的涉及的_dl_map_object_deps和_dl_relocate_object两 个函数内容,因为这些都与函数解析的内容直接相关,所以安排在这里。而下面的函数解析过程_dl_runtime_resolve是在程序运行中的动态解 析过程。这里从本质上来讲没有太多的代码,但它的精巧程度却是最多的(正是我这三篇文章的核心之处)。最后是一个dl_close的实现。这里是一个结尾 的工作,顺带一下是_dl_signal_cerror,与_dl_catch_error的错误例外处理。

第三部将给出injectso 实例分析与应用,会介绍一个应用了动态链接的实例,并可以在日后的程序调试过程中使用的injectso实例,它不仅可以让我们对前面所说的动态链接原理 有一个更感性的认识,而且就这个实例而言,还可以在以后的代码开发过程中来作为一种动态打补丁的工具,甚至有可能,我会在以后的文章中会用这个工具来介绍 新的技术。

关于 动态链接,可以说由来已久。如果追溯,最早的思想就在五十年代就有了,那时就想把一些公用的代码放在内存中的一个地方上,在别的地址用call便是了。到 后来又发展到了 loading overlays(就是把在程序运行生命期不同的代码在不同的时间段被加入内存),这是在六十年代的事。但这只能算是"滥觞"时期。接近于我们现在所说的 动态链接是在unix操作系统之后,因为从unix的设计结构而言,本身就是分成模块来实现一个复杂的功能的操作系统。但这些还不是现代意义上的动态链 接,原因是现代意义上的动态链接要符合两个特点:

1、 动态的加载,就是当这个运行的模块在需要的时候才被映射入运行模块的虚拟内存空间中,如一个模块在运行中要用到mylib.so中的myget函数,而在 没有调用mylib.so这个模块中的其它函数之前,是不会把这个模块加载到你的程序中(也就是内存映射),这些内容在内核中实现,用的是页面异常机制 (我可能在另一篇文章中提到这个问题)。

2、 动态的解析,就是当要调用的函数被调用的时候,才会去把这个函数在虚拟内存空间的起始地址解析出来,再写到专门在调用模块中的储存地址内,如前面所说的你 已经调用了myget,所以mylib.so模块肯定已经被映射到了程序虚拟内存之中,而如果你再调用mylib.so中的myput函数,那它的函数地 址就在调用的时候才会被解析出来。

(注:这里用的程序就是一般所说的进程process,而模块既可能是你的程序的二进制代码,也可能是被你的程序所依赖的别的共享链接文件-------同样ELF格式。)

在 这两点中很有点像现在的操作系统中对内存的操作,也就是只有当要用到一个内存空间中的时候才会进行虚拟空间映射,而不是过早的把所有的空间映射好,而只有 当要从这个内存空间读的时候才分配物理空间。这有点像第一条。而只有当对这个内存空间进行写的时候产生一个COW(copy on write)。这就有点像第二条。

这样的好处就是充分避免不必要的开销。因为任何一个程序在运行的时候,大部分情况下,不可能用到所有的调用函数。

这样的思想方法提出与实现都是在八十年代的sun公司的SunOS的系统上。

关于这一段历史,请你参见资料[1]。

ELF 二进制格式文件与现代的动态链接思想大致是在同一时段形成的,它的来源是AT&T公司的最早的unix中的a.out二进行文件格式。Bell labs的工作人员为了使这种在unix的早期主要的文件格式适应当时新的软件与操作系统的要求(如aix,SunOS,HP-UX这样的unix变种, 对更广泛的应用程序的扩展要求,对面向对象的支持等等),就发明了ELF文件格式。

我在这里并不详细讨论ELF文件的具体细节,这本来就可 以写一篇很长的文章,你可以参看资料[2]来得到关于它的ABI(application binary interface的规范)。但在ELF文件所采用的那种分层的管理方式却不仅在动态链接中起着重要的作用,而且这一思想可以说是我们计算机中的最古老, 也是最经典的思想。

对每个ELF文件,都有一个ELF header,在这里的每个header有两个数据成员,就是

Elf32_Off	e_phoff;Elf32_Off	e_shoff;

它们分别代表了program header 与section header 在ELF文件中的偏移量。Program header 是总纲,而section header 则是第一个小目。

Elf32_Addr	sh_addr;Elf32_Off	sh_offset;

Sh_addr这个section 在内存中的映射地址(对动态链接库而言,这是一个相对量,它与整个ELF文件被加载的l_addr形成绝对地址)。Sh_offset是这个section header在文件中的偏移量。

用一图来表示就是这样的,它就是用elf header 来管理了整个ELF文件:


举个例子,如果要从一个ELF动态链接库文件中,根据已知的函数名称,找到相应的函数起始地址,那么过程是这样的。

先 从前面的ELF 的ehdr中找到文件的偏移e_phoff处,在这其中找到为PT_DYNAMIC 的d_tag的phdr,从这个地址开始处找到DT_DYNAMIC的节,最后从其中找到这样一个Elf32_Sym结构,它的st_name所指的字符 串与给定的名称相符,就用st_value便是了。

这种的管理模式,可以说很复杂,有时会看起来是繁琐。如找一个function 的起始地址就要从 elf header >> program header >> symbol section >> function address 这样的四个步骤。但这里的根本的原因是我们的计算机是线性寻址的,并且冯*诺依曼提出的计算机体系结构相关,所以在前面说这是一个古老的思想。但同样也是 由于这样的一个ELF文件结构,很有利于ELF文件的扩充。我们可以设想,如果有一天,我们的ELF文件为了某种原因,对它进行加密。这时如果要在ELF 文件中保存密钥,这时候可以在ELF文件中开辟一个专门的section encrypt ,这个section 的type 就是ST_ENCRYPT,那不就是可以了吗?这一点就可以看出ELF文件格式设计者当初的苦心了(现在这个真的有这么一个节了)。

讲了这么多,还没有真正讲到在intel 32平台下linux动态链接库的加载与调用。在一般的情况下,我们所编写的程序是由编译器与ld.so这个动态链接库来完成的。而如果要显式的调用某一个动态链接库中的程序,则下面是一个例子。

#include  #include  main() { void *libc; void (*printf_call)();char* error_text; if(libc=dlopen("/lib/libc.so.5",RTLD_LAZY))   {    printf_call=dlsym(libc,"printf");    (*printf_call)("hello, world\n"); dlclose(libc);return 0;}error_text= dlerror();printf(error_test);return -2;}

在 这里先用dlopen来打开一个动态链接库文件,而这个过程比我们这里看到的内容多的多,我会在下面用很大的篇幅来说明这一点,而它返回的参数是一个指 针,确切的说是struct link_map*,而dlsym就是在这个struct link_map* 与函数名称一起决定这个函数在这个进程中的地址,这个过程用术语来说就是函数解析(function resolution)。而最后的dlclose就是释放刚才在dlopen中得到的资源,这个过程与我们在加载的share object file module,内核中的程序是大概相同的,只不过这里是在用户态,而那个是在内核态。从函数的复杂性而言这里还要复杂一些(最后有一点要说明,如果你想编 译上面的文件-------文件名如果是test那就不能用一般的gcc -o test test.c ,而应该是gcc -c test test.c -ldl这样才能编译通过,因为不这样编译器会找不到dlopen 与dlsym dlclose这些特别函数的库文件libdl.so.2, -ldl 就是加载它的标志的)。

本 文以及以后的两篇文章将都以上面的程序所展示的而讲解。也就是以dlopen >> dlsym >> dlclose 的方式 来讲解这个过程,但有几点先要说明: 我在这里所展示的源代码来自glibc 2.3.2版本。但由于原来的代码,从代码的移植与健壮的考虑,而有许多的防止出错,与关于不同平台的代码,在这里大部分是出错处理代码,我把这些的代码 都删除。并且只以intel 32平台下的代码为准。还有,在这里的还考虑到了多线程情况下的动态链接库加载,这里也不予以包括在内(因为现在的linux内核中没有对内核线程的支 持)。所以你所看到的代码,在尽量保证说明动态链接加载与函数解析的情况作了多数的删减,代码量大概只有原来的四分之一左右,同时最大程度保持了原来代码 的风格,突出核心功能。尽管如此,还是有高达2000行以上的代码,请大家耐心的解读。我也会对其中可能的难解之处作出详细的说明。让大家真正体会到代码 设计与动态解析的真谛。

第一个函数在dl-open.c中

    2672	void* internal_function   2673	_dl_open (const char *file, int mode, const void *caller)  2674	{  2675	   struct dl_open_args args;  2676	  2677	     __rtld_lock_lock_recursive (GL(dl_load_lock));  2678	  2679	   args.file = file;  2680	   args.mode = mode;  2681	   args.caller = caller;  2682	   args.map = NULL;  2683	  2684	   dl_open_worker(&args);  2685	      __rtld_lock_unlock_recursive (GL(dl_load_lock));  2686	     2687	}  

这里的internal_function是表明这个函数从寄存器中传递参数,而它的定义在configure.in中得到的。

# define internal_function __attribute__ ((regparm (3), stdcall))

这 其中的regparm就是gcc的编译选项是从寄存器传递3个参数,而stdcall表明这个函数是由调用函数来清栈,而一般的函数是由调用者来负责清 栈,用的是cdecl。 __rtld_lock_lock_recursive (GL(dl_load_lock));与__rtld_lock_unlock_recursive (GL(dl_load_lock));在现在还没有完全定义,至少在linux中是没有的,但可以参考在linux/kmod.c 中的request_module中为了防止过度嵌套而加的一个锁。

而其它的内容就是一个封装了。

dl_open_worker 是真正做动态链接库映射并构造一个struct link_map而这是一个绝对重要的数据结构它的定义由于太长,我会放在第二篇文章结束的附录中介绍,因为那时你可以回头再理解动态链接库加载与解析的 过程,而在下面的具体函数中出现了作实用性的解释,下面我们分段来看:

_dl_open() >> dl_open_worker()  2532	static void  2533	dl_open_worker (void *a)2534	{……………………..2547	args->map = new = _dl_map_object (NULL, file, 0, lt_loaded, 0, mode);

这里就是调用_dl_map_object 来把文件映射到内存中。原来的函数要从不同的路径搜索动态链接库文件,还要与SONAME(这是动态链接库文件在运行时的别名)比较,这些内容我在这里都删除了。

_dl_open() >> dl_open_worker() >> _dl_map_object()  1693	struct link_map *  1694	internal_function  1695	_dl_map_object (struct link_map *loader, const char *name, int preloaded,  1696			int type, int trace_mode, int mode)  1697	{  1698	  int fd;  1699	  char *realname;  1700	  char *name_copy;  1701	  struct link_map *l;  1702	  struct filebuf fb;  1703	  1704	  1705	  /* Look for this name among those already loaded.  */  1706	  for (l = GL(dl_loaded); l; l = l->l_next)  1707	  	{  1708	  	    if (!_dl_name_match_p (name, l))…………….  1721	  	    return l;  1722	  	}   1723	  1724	   fd = open_path (name, namelen, preloaded, &env_path_list,  1725				&realname, &fb);  1726	  1727	   l = _dl_new_object (name_copy, name, type, loader);  1728	  1729	   return _dl_map_object_from_fd (name, fd, &fb, realname, loader, type, mode);  1730	  1731	1732	}/*end of _dl_map_object*/

这里先在已经被加载的一个动态链接库的链中搜索,在1706与1721行中就是作这一件事。想起来也很简单,因为可能在一个可执行文件依赖好几个动态链接库。而其中有几个动态链接库或许都依赖于同一个动态链接文件,可能早就加载了这样一个动态链接库,就是这样的情况了。

下面open_path是一个关键,这里要指出的是env_path_list得到的方式有几种,一是在系统环境变量,二就是DT_RUNPATH所指的节中的字符串(参见下面的附录),还有更复杂的,是从其它要加载这个动态链接库文件的动态链接库中得到的环境变量-------这些问题我们都不说明了。

    _dl_open() >> dl_open_worker() >> _dl_map_object() >> open_path()  1289	static int open_path (const char *name, size_t namelen, int preloaded,  1290		   struct r_search_path_struct *sps, char **realname,  1291		   struct filebuf *fbp)  1292	  1293	{  1294	  struct r_search_path_elem **dirs = sps->dirs;  1295	  char *buf;  1296	  int fd = -1;  1297	  const char *current_what = NULL;  1298	  int any = 0;  1299	  1300	  buf = alloca (max_dirnamelen + max_capstrlen + namelen);  1301	  1302	  do  1303	    {  1304	      struct r_search_path_elem *this_dir = *dirs;  1305	      size_t buflen = 0; ………………  1310	     struct stat64 st;  1311	       1312	  1313	      edp = (char *) __mempcpy (buf, this_dir->dirname, this_dir->dirnamelen);  1314	      for (cnt = 0; fd == -1 && cnt < ncapstr; ++cnt)  1315		{  1316		  /* Skip this directory if we know it does not exist.  */  1317		  if (this_dir->status[cnt] == nonexisting)  1318		    continue;  1319	  1320		  buflen = ((char *) __mempcpy (__mempcpy (edp, capstr[cnt].str,  1321						    capstr[cnt].len), name, namelen)- buf);  1322	  1323		   1324		  fd = open_verify (buf, fbp);  1325	          1326	          1327		  __xstat64 (_STAT_VER, buf, &st);  1328		    1329		   1341		}  1342	…………….    1358		}    

在 这上面的alloc是在栈上分配空间的函数,这样就不用担心在函数结束的时候出现内存泄漏的情况(好的程序员真的要对内存的分配熟谙于心)。1313行就 是把r_search_path_elem的dirname copy过来,而在1320至1321行的内容就是为这个路径加上最后的'/'路径分隔号,而capstr就是根据不同的操作系统与体系得到的路径分隔 号。这其实是一个很好的例子,因为__memcpy返回的参数是dest string所copy的最后的一个字节的地址,所以每copy之后就会得到新的地址,如果用strncpy来写的话,就要用这样的方法

strncpy(edp, capstr[cnt].str, capstr[cnt].len);edp+=capstr[cnt].len;strncpy(edp,name, namelen);edp+=namelen;buflen=edp-buf;

这就要用四句,而这里用了一句就可以了。

下 面的open_verify是打开这个buf所指的文件名,fbp是从这个文件得到的文件开时1024字节的内容,并对文件的有效性进行检查,这里最主要 的是ELF_IMAGIC核对。如果成功,就返回一个大于-1的文件描述符。整个open_path就这样完成了打开文件的方法。

_dl_new_object是一个分配struct link_map* 数据结构并填充一些最基本的参数。

  _dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_new_object()  2027	struct link_map *  2028	internal_function  2029	_dl_new_object (char *realname, const char *libname, int type,  2030			struct link_map *loader)  2031	  2032	{    2033	  struct link_map *l;  2034	  int idx;  2035	  size_t libname_len = strlen (libname) + 1;  2036	  struct link_map *new;  2037	  struct libname_list *newname;  2038	  2039	   new = (struct link_map *) calloc (sizeof (*new) + sizeof (*newname)  2040					    + libname_len, 1);  2041	………………..  2046	  2047	  new->l_name = realname;  2048	  new->l_type = type;  2049	  new->l_loader = loader;  2050	  2051	  new->l_scope = new->l_scope_mem;  2052	  new->l_scope_max = sizeof (new->l_scope_mem) / sizeof (new->l_scope_mem[0]);  2053	  2054	 if (GL(dl_loaded) != NULL)  2055	    {  2056	      l = GL(dl_loaded);  2057	      while (l->l_next != NULL)  2058		l = l->l_next;  2059	      new->l_prev = l;  2060	      /* new->l_next = NULL;	Would be necessary but we use calloc.  */  2061	      l->l_next = new;  2062	  2063	      /* Add the global scope.  */  2064	      new->l_scope[idx++] = &GL(dl_loaded)->l_searchlist;  2065	    }  2066	  else  2067	    GL(dl_loaded) = new;  2068	  ++GL(dl_nloaded); ………….  2080	  2081	    return new;  2082	  2083	}  

在2039 行的内存分配是一个把libname 与name的数据结构也一同分配,是一种零用整取的策略。从2043-2053行都是为struct link_map 的成员数据赋值。从2054-2067行则是把新的struct link_map* 加入到一个单链中,这是在以后是很有用的,因为这样在一个执行文件中如果要整体管理它相关的动态链接库,就可以以单链遍历。

如果要加载的动态链接库还没有被映射到进程的虚拟内存空间的话,那只是准备工作,真正的要点在_dl_map_object_from_fd()这个函数开始的。因为这之后,每一步都有关动态链接库在进程中发挥它的作用而必须的条件。

这上段比较长,所以分段来看,

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()  1391	struct link_map *  1392	_dl_map_object_from_fd (const char *name, int fd, struct filebuf *fbp,  1393				char *realname, struct link_map *loader, int l_type,  1394				int mode)  1395	  1396	{  1397	  1398	  struct link_map *l = NULL;  1399	  const ElfW(Ehdr) *header;  1400	  const ElfW(Phdr) *phdr;  1401	  const ElfW(Phdr) *ph;  1402	  size_t maplength;  1403	  int type;  1404	  struct stat64 st;  1405	  1406	  __fxstat64 (_STAT_VER, fd, &st);…………  1413	  for (l = GL(dl_loaded); l; l = l->l_next)  1414	    if (l->l_ino == st.st_ino && l->l_dev == st.st_dev)  1415	      {……….  1418		__close (fd);……………  1422		free (realname);  1423		add_name_to_object (l, name);  1424	  1425		return l;1426	}

这 里先开始就要从再找一遍,如果找到了已经有的struct link_map* 要加载的libname(的而比较的依据是它的与st_ino,这是物理文件在内存中编号,且文件的设备号st_dev相同,这是从比较底层来比较文件, 具体的原因,你可以参看我将要发表的《从linux的内存管理看文件共享的实现》)。之所以采取这样再查一遍,因为如果进程从要开始打开动态链接库文件, 走到这里可能要经过很长的时间(据我作的实验来看,对第一次打开的文件大概也就在200毫秒左右---------主要的时间是硬盘的寻道与读盘,但这对 于计算机的进程而言已经是很长的时间了。)所以,有可能别的线程已经读入了这个动态链接库,这样就没有必要再做下去了。这与内核在文件的打开文件所用的思 想是一致的。

  _dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()  1427	  1428	  /* This is the ELF header.  We read it in `open_verify'.  */  1429	  header = (void *) fbp->buf;  1430	  1431	  l->l_entry = header->e_entry;  1432	  type = header->e_type;  1433	  l->l_phnum = header->e_phnum;  1434	  1435	  maplength = header->e_phnum * sizeof (ElfW(Phdr));  1436	  

这一段所作的为下面的ELF文件的分节映射入内存做一点准备(要读写phdr的数组)。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()  1438	     /* Scan the program header table, collecting its load commands.  */  1439	    struct loadcmd  1440	      {  1441		ElfW(Addr) mapstart, mapend, dataend, allocend;  1442		off_t mapoff;  1443		int prot;  1444	      } loadcmds[l->l_phnum], *c;1445	size_t nloadcmds = 0;

这里把数据结构定义在函数内部,能保证这是一个局部变量定义,与面向对象中的private的效果是一样的。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()  1448	    for (ph = phdr; ph < &phdr[l->l_phnum]; ++ph)  1449	      switch (ph->p_type)  1450		{………..  1454		case PT_DYNAMIC:  1455		  l->l_ld = (void *) ph->p_vaddr;  1456		  l->l_ldnum = ph->p_memsz / sizeof (ElfW(Dyn));  1457		  break;  1458	  1459		case PT_PHDR:  1460		  l->l_phdr = (void *) ph->p_vaddr;  1461		  break;  1462	  1463		case PT_LOAD: …………..  1467		  c = &loadcmds[nloadcmds++];  1468		  c->mapstart = ph->p_vaddr & ~(ph->p_align - 1);  1469		  c->mapend = ((ph->p_vaddr + ph->p_filesz + GL(dl_pagesize) - 1)  1470			       & ~(GL(dl_pagesize) - 1));  1471		  c->dataend = ph->p_vaddr + ph->p_filesz;  1472		  c->allocend = ph->p_vaddr + ph->p_memsz;  1473		  c->mapoff = ph->p_offset & ~(ph->p_align - 1);…………..  1480		  c->prot = 0;  1481		  if (ph->p_flags & PF_R)  1482		    c->prot |= PROT_READ;  1483		  if (ph->p_flags & PF_W)  1484		    c->prot |= PROT_WRITE;  1485		  if (ph->p_flags & PF_X)  1486		    c->prot |= PROT_EXEC;    1488		  break;  …………1493	}

在ELF文件的规范中,根据不同的program header 不同,要实现不同的功能,采用不同的处理策略,具体的内容请参看附录2 中的说明。这里没有出现一般的default 但实际运行与下面的语句是等价的:

default:    continue;

真是达到程序简洁的特点。

但有一个特别要指出的是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文件动态链接的加载、解析及实例分析(中)-----------函数解析与卸载篇》进行阐明,敬请期待。

类型数值d_un所指EXEC可选性DYN可选性说明
DT_NULL0不用必须必须这个表示动态链接section的结束标志
DT_NEEDED1d_val可选可选这个节d_val是包含了以null结尾的字符串,这些字符串是这个动态链接文件或可执行文件的依赖文件名称与路径的节的开始地址
DT_PLTRELSZ2d_val可选可选这里的d_val是过程链接表(procedure linkage table)的大小,它与DT_JMPREL结合使用
DT_PLTGOT3d_ptr可选可选这里的d_ptr是过程链接表或全局偏移量表的起始地址。
DT_HASH4d_ptr必须必须这里的d_val是符号哈希表的起始地址。
DT_STRTAB5d_ptr必须必须这里d_ptr所给出的是符号名称字符串表的起始地址。
DT_SYMTAB6d_ptr必须必须这里的d_ptr是Elf32_sym数据结构在的节表中的起始地址。
DT_STRSZ10d_val必须必须这d_val是上面的DT_STRTAB节的大小。
DT_SYMENT11d_val必须必须这里的d_val是DT_SYMTAB中的每个Elf32_Sym数据结构的大小
DT_INIT12d_ptr可选可选这里的d_ptr是一个动态链接库被加载时调用的初始函数所在节的起始地址。
DT_FINI13d_ptr可选可选这里的d_ptr是一个动态链接库被卸载时,调用解构函数所在节的起始地址。
DT_REL17d_ptr必须可选这里的d_ptr与上面的DT_RELA相似,是Elf32_Rel数据结构所在节的起始地址,它在intel平台下用。
DT_RELSZ18d_val必须可选这d_val与上面的DT_REL上面的相对应,表明上面的那个节的大小。
DT_RELENT19d_val必须可选这里的d_val是DT_REL中的一个Elf32_Rel的数据结构的大小。
DT_PLTREL20d_val可选可选这里的d_val是与过程链接表(procedure linkage table)有关的,就是DT_REL 或DT_RELA的值,也就是这个ELF文件用的是DT_REL的话那d_val就是17,而如果是DT_RELA的话就是7
DT_JMPREL23d_ptr可选可选这是我们这里最重要的Elf_Dyn,因为d_ptr所指的就是GOT(global object table)全局对象表,这其实是一个导入函数与全局变量的地址表。
DT_INIT_ARRAY25d_ptr可选可选这里的d_ptr是要初始化函数跳转表起始相对地址。
DT_FINI_ARRAY26d_ptr可选可选这里的d_ptr是要解构时调用的函数跳转表起始相对地址。
DT_INIT_ARRAYSZ27d_val可选可选这里的d_val表明前面的DT_INIT_ARRAY的大小。
DT_FINI_ARRAYSZ28d_val可选可选这里的d_val是前面的DT_FINI_ARRAY的大小。
DT_ENCODING32d_val或d_ptr没有规定没有规定现在这个节还没有规定,但很明显就是为以后的加密而准备的。
DT_PREINIT_ARRAY32d_ptr可选不用这里d_ptr是在调用main函数之前的调用初始函数跳转表的起始地址。
DT_PREINIT_ARRAYSZ33d_val可选不用这里的d_val是前面的DT_PREINIT_ARRAY的大小

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

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











相信读者已经看过了Intel平台下Linux中ELF文件动态链接的加载、解析及实例分析(一): 加载的内容了,了解了ELF文件被加载的时候所经历的一般过程。那我们现在就来解决在上一篇文章的最后所提到的那几个问题,以及那些在dl_open_worker中没有讲解的代码。

由于源代码过分的冗长,并且由于效率的考虑,使原本很简单的代码变成了一件 TRAMPOLINE 的事情,所以我对它进行了大幅度的改变,不仅删除了所有不必要的代码,而且还用伪代码来展现它最初的设计思想。

    13	_dl_map_object_deps  (struct link_map *lmap)    14	    15	{    16	    17		struct list_head*  add_list;    18		char* load_dl_name;    19		struct link_map* curlmap;    20		Elf32_Dyn*  needed_dyn;    21		struct link_map*  new_lmap;    22		int lmap_count=1;    23	    24		add_lmap_to_list(lmap,add_list);    25	    26	  	for_each_in_list(add_list,curlmap)    27	  		{    28	  			for_every_DT_NEEDED_section(curlmap,needed_dyn)    29	  				{    30	  					load_dl_name=  get_needed_name(curlmap,needed_dyn);        31	    32	  					new_lmap=_dl_map_object(load_dl_name);    33	    34	    35	  					add_to_list_tail_uniq(add_list,new_lmap);    36	  				} 		    37	  		}    38		    39		lmap_count=count_the_list(lmap);    40	    41		lmap->l_initfini=(struct link_map**)malloc ((2*lmap_count+1)*(struct link_map*));    42	    43		lmap->l_searchlist.r_list=&lmap->l_initfini[lmap_count+1];    44		lmap->l_searchlist.r_nlist=lmap_count;    45	    46		    47		copy_each_add_list_to_searchlist(lmap,add_list,lmap_count);    48	    49		free_the_add_list(add_list);    50	    51	}

先 说明,其实加载一个动态链接库的依赖动态链接库不是一件简单的事,因为所有的动态链接库可能还有它自己所依赖的动态链接库,如果采用递归简单方法实现不仅 是不可能的-----因为你可以参看第一篇的文章,那里提到了一个在加载动态链接库中的加锁问题,而且也是没有必要的,你并不能保证这样的动态链接库依赖 关系会不会形成一个依赖循环,就像下面的一张图所显示的那样:

这样最简单的想法就是我们不重复的加载所有的动态链接库,这里就用一个单链实现-----在原来的程序中也是用这个方法,但那里用来分配的方法是在栈中直接实现,这样可以加快程序的运行,但程序可读性大大减弱了。

23 行就首先就把 lmap 自己加入这个 struct list 中去,在 26 行的 for_each_in_list(add_list,curlmap) 其实是就是把 curlmap=curlmap->next,并判断它的 curlmap!=NULL,

28 行的 for_every_DT_NEEDED_section(curlmap,needed_dyn)

主要就是 needed_dyn=curlmap->l_info[DT_NEEDED]; 但这里要注意的是,在一个动态链接库中可能有不只一个,就像在 readelf -a 的例子

  Tag        Type                         Name/Value 0x00000001 (NEEDED)                     Shared library: [libstdc++-libc6.2-2.so.3] 0x00000001 (NEEDED)                     Shared library: [libm.so.6] 0x00000001 (NEEDED)                     Shared library: [libc.so.6]

更确切的是要在 lmap-> l_ld 的 dynamic section 中查找它的 d_tag 为 DT_NEEDED 中

30 行的 get_needed_name 用的方法是这样的

load_dl_name=curlmap->l_addr+need_dyn->d_un.d_ptr+curlmap->l_info[DT_STRTAB];

很 明显这里就会把这个动态链接库映射来完成它的加载,而 35 行是要把 add_list 扩充,这里只会对同一个动态链接库加载一次,所以不会有前面的循环加载,再回过头来看 26 行到 37 行之间的那个循环,如果在 35 行中加入了那个没有重复的动态链接库。那整个循环就可能继续循环下去。

从 39 行到 51 行之中就把这个函数中已经得到的依赖动态链接库 copy 入 l_searchlist 与 l_initfini 这两个的重要数组中, 巧妙的是它们采用了一起分配的。最后前面的那个临时单链表。

在 学习汇编语言的时候,我们对不同的寻址方式肯定有很深的印象。但对于在汇编语言中同样重要的转移指令,只是一笔带过(用到了call 与 jxx ----------- 这里的 jxx 是指如 jmp jae jbe 这样的有条件转移指令和无条件转移指令)。然而,如果讲到动态链接库的链接实现则一定要提到这一内容。

所谓相对转移,就是这个二进制代码的中的它是可以在重定位的环境中不经修改,就可以运行的。如下面的情况,

719:	e9 e2 fe ff ff       	jmp    600 

变成一般的地址是这样的

   movl %eip,%eax   addl $0xfffffee2,%eax   movl %eax,%eip

这 里旁边的 719 就是这个 ELF 文件与起始地址相比的偏移量,而在里面的 e9 e2 fe ff ff 如果写成看的往后退 0x11e 因为这是 ff ff fe e2(intel 是 little endian 表示方法)所表示的 -0x11e 的数。如果把 719 加上 5 再减去 600 就是这个数了。这便是处理器的相对转移。

还有另一种转移方式,就是绝对转移。

2b6:	ff d0                	call   *%eax

这个如果用最简单的代码来表示是

addl $2,%eippushl %eipmovl (%eax),%eip

很明显,就是把 eip 的内容变成了eax 中的内容,如果用 jmp 也是一样的

ljmp   *(%edx)

上面的两种转移方式适应于不同的环境要求,如果是在一个ELF文件中的,采用相对转移可带来的好处有以下的几点:

1、 可以不用再访问一次内存,在指令的执行时间上得到了大大的提高(这在PCI的总线结构中现在主流的最高主频是133MHZ,而随便一个INTEL CPU的主频都能超过它)。

2、 可以适应在动态加载与动态定位的内存环境,而不用再对原来的代码修改便能实现(代码段也不能在运行的时候修改),因为整个动态链接库或可执行文件都是以连续的地址映射的。

但同样带来了几个问题:

1、 这样的相对转移没有办法在运行的时候准确的转移到别的动态链接库中的函数地址(因为虽然大部分的动态链接库的加载地址是可以预计的,但从理论上来说是随机的)。

2、 这样的代码在平台之间的移植性带来很大的问题,因为不同的机器没有办法知道这样的数字是代表一个地址,还是代表了一个二进制数。所以在对平台移植有高要求的体系中用的是c++的虚函数指针------相对地址转移的发展。如COM,corba体系中就是这样的。

上面的这两项缺点正好是绝对转移的优势。作一个对比,绝对转移就相当于内存寻址时的立即寻址,而相对转移相当于内存寻址的相对寻址。

在一般的动态链接库中实际运用更是用了一个聪明的办法。请看下一段的汇编语言片段:

 2f7:	e8 00 00 00 00       	call   2fc  2fc:	5b                   	pop    %ebx 2fd:	81 c3 b0 10 00 00    	add    $0x10b0,%ebx

这 里的2f7中的call 2fc 是什么意思呢,从我们上面的方法来看,这里是什么呢?就是把函数运行到了2fc处,根据是我上面所说的,因为是一个相对转 移。e8 00 00 00 00。如果用一般的观点看这没有什么用处。但妙处就在这里,2fc处的pop %ebx,是把什么送到%ebx中呢,如果每一次call 都会把下一条要执行的指令的地址压入栈中,那%ebx中在这里的内容就是2d4这一条指令在内存中的地址了,回想动态链接库的绝对地址是没有办法在编译时 得到,但这样却可以--------很巧妙,不对吗?

那后面的add $0x10b0,%ebx又是什么用处?如果我们这里假定在内存中的地址是2fc,那加上10b0之后的值是0x13ac了,看在这里是什么呢?

Disassembly of section .got:000013ac <.got>:    13ac:	34 13                	xor    $0x13,%al	...

这是一个got节, 它的全称是global object table 就是全局对象表。它这里存储着要转移的地址。如果在动态链接库中,或是要调用一个在它之外的函数是怎样实现呢?我们往下看:

 306:	8d 83 74 ef ff ff    	lea    0xffffef74(%ebx),%eax 30c:	50                   	push   %eax 30d:	e8 ce ff ff ff       	call   2e0 

这里就要调用一个call 2e0 所在的函数。那在0x2e0处又是什么呢?

 2e0:	ff a3 0c 00 00 00    	jmp    *0xc(%ebx) 2e6:	68 00 00 00 00       	push   $0x0 2eb:	e9 e0 ff ff ff       	jmp    2d0 

很 明显,我们前面已经说了%ebx中所保存的就是.got节的起始地址,而这里就是转移到在.got起始地址偏移0xc处所存储的地址量。而0x2e0所在 的地址是在.plt(procedure linkage table)的节中。正是plt got的互相配合,才达到了动态链接的效果。下面的_dl_relocate_object函数就是在把动态链接库加载之后将got中的内容初始化的作 用,作好了以后函数解析的准备。

举个例子。同样来自上面的动态链接库文件中内容。如果我们在这里面调用了printf这个普通的函数,它的rel在文件中的位置是

 Relocation section '.rel.plt' at offset 0x2c8 contains 1 entries: Offset     Info    Type            Symbol's Value  Symbol's Name 000013b8  00000e07 R_386_JUMP_SLOT       00000000  printf   

这个值如果在文件中找到0x13b8(这是相对偏移量)的内容就是

 13b8:	e6 02 

由于intel 是little endian 所以这个数翻译过来是0x02e6,那这里是什么呢?

 2e0:	ff a3 0c 00 00 00    	jmp    *0xc(%ebx) 2e6:	68 00 00 00 00       	push   $0x0 2eb:	e9 e0 ff ff ff       	jmp    2d0 

这下就会全部明白了吧。它就是压入0x0(这其实就是我们前面的printf在rel节中的索引数0------它是第一项)。而下面跳到的就是2d0(这是一个相对转移)处

 2d0:	ff b3 04 00 00 00    	pushl  0x4(%ebx) 2d6:	ff a3 08 00 00 00    	jmp    *0x8(%ebx)

前 面已经说过%ebx得到的是got的起始地址,所以这就是压got[1]入栈,再转移到got[2]中所包含的地址去,你可以看前面在 elf_machine_runtime_setup中的2162行与2167行,它就是这个动态链接库自身的struct link_map*的指针,与_dl_runtime_resolve所在的地址。下面一张图就可以形象的说明这一点。

如果是第一次的函数调用,它所走的路线就是我在上图中用红线标出的,而要是在第二次以后调用,那就是蓝线所标明的。原因在前面的代码中已经给出了。

    82	int _dl_relocate_object (struct link_map* lmap,int lazy_mode)    83	{    84	 	elf_machine_runtime_setup(lmap,lazy_mode);     85		elf_machine_lazy_rel (lmap,lazy_mode);    86	 	    87	}

这 里要分两步来完成,第一步的elf_machine_runtime_setup是把这个动态链接库所代表的数据结构lmap的地址写入一个在ELF文件 中特别地方,而elf_machine_lazy_rel是对所有的要被调用的动态链接库外部的函数重定位的实现。这两步非常重要,因为如果没有这两步, 那要实现动态链接库的函数动态解析是不可能的,这个你可以在上面的 相对转移,绝对转移 中的论述得到详细的了解。

    54	void elf_machine_runtime_setup(struct link_map* lmap,int lazy_mode)    55	{    56	 	Elf32_Addr *got;    57	    58	 	 got = (Elf32_Addr *) lmap->l_info[DT_PLTGOT].d_un.d_ptr;    59	    60	got[2]=&_dl_runtime_resolve    61	 	 got[1]=lmap;    62	}

明显的,那个被写入的ELF文件中的地址就是它的DT_PLTGOT节中的第二个项目-----第60行的内容。而写入第一项的内容就是要调动的处理函数的地址,这一点在后面所提到的动态解析中的入口地址。

    64	void elf_machine_lazy_rel (struct link_map* lmap,int lazy_mode)    65	{    66	 	Elf32_Addr rel_addr=lmap->l_info[DT_REL].d_un.d_ptr;    67	 	int rel_num=lmap->l_info[DT_RELSZ].d_un.d_ptr;    68	 	int i;    69	 	Elf32_Addr l_addr=lmap->l_addr;    70	    71		Elf32_Rel*  rel;    72		for (i=0,rel=(Elf32_Rel*)rel_addr;ir_offset);    75				*reloc_addr +=l_addr;		      76			}    77		    78	}

这 里的elf_machine_lazy_rel我只列出了在intel平台下的那种情况,其它的还要特别的内容,在这里很明显,我们只是写把原来的在 ELF文件的内容加上一个文件加载的地址,这就是lazy mode,因为动态链接库的函数很可能在整个程序运行中不会被调用--------这一点与虚拟内存管理的原理是一样的。

前面的60行的代码----设定了动态解析的入口地址与给出的在动态链接库中的在达到调用一个外部函数时所有的函数路线,已经到了_dl_runtime_resolve处

  2087	#  define ELF_MACHINE_RUNTIME_TRAMPOLINE asm ("\\  2088		.text\\n\\  2089		.globl _dl_runtime_resolve\\n\\  2090		.type _dl_runtime_resolve, @function\\n\\  2091		.align 16\\n\\  2092	_dl_runtime_resolve:\\n\\  2093		pushl %eax		\\n\\  2094		pushl %ecx\\n\\  2095		pushl %edx\\n\\  2096		movl 16(%esp), %edx	\\n\\  2097		movl 12(%esp), %eax	\\n\\  2098		call fixup		\\n\\  2099		popl %edx		\\n\\  2100		popl %ecx\\n\\  2101		xchgl %eax, (%esp)	\\n\\  2102		ret $8			\\  2103		.size _dl_runtime_resolve, .-_dl_runtime_resolve\\n\\  2104	\\n\\  2105		");

从这里定义的名称ELF_MACHINE_RUNTIME_TRAMPOLINE,我们就可以看出这个函数不简单(TRAMPOLINE在英语中是蹦床的意思,就是要make your brain curving的那种怪怪的东西),后面的代码也确实说明了这一点。

在 前面的.text是下面的代码是可执行,.globl _dl_runtime_resolve是表明这个函数是全局性的,如果没有这一项,那我们前面看的got[2]=& _dl_runtime_resolve就不能编译通过-----编译器可能找不到它的定义。.type _dl_runtime_resolve, @function是函数说明。 .align 16处便是16字节对齐。

我们知道在前面的调用函数过程中已经压入了两个参数(第一个是动 态链接库的struct link_map* 指针,另一个是rel的索引值)这里先保存以前的寄存器值,而到这个时候16(%esp)就是第二个参数,12(%esp)第一个参数,这里作的原因是下 面的fixup的函数以寄存器传递参数。

我先不管fixup具体内容是什么,单就看它结束的内容就很能说明代码作者的优秀。先pop两个寄 存器的值,而又xchg %eax,(%esp)与栈顶的内容,这有两个目的,一是恢复了eax的值,另一个作用是栈顶是函数返回的地址,而fixup返回的eax就是我们想找的 函数有内存中的地址。这就自然跳到那个地方去了。但如果你认为这就好了,那也错了,因为你不要忘记我们之前还压入了两个参数在栈中。所以用了ret $8,这在intel的指令中表示

popl %eip add $0x8,%esp

的组合。(很精彩!!!!!!!)

你还可以参看《程序的链接和装入及Linux下动态链接的实现》 网址为 http://www-900.ibm.com/developerWorks/cn/linux/l-dynlink/index.shtml 里面的有一幅图正好说明此的ELF_MACHINE_RUNTIME_TRAMPOLINE。

那直接看fixup函数的内容

   124	Elf32_Addr  fixup(struct link_map* lmap,Elf32_Word reloc_offset)   125	{   126		Elf32_Sym* symtab=lmap->l_info[DT_SYMTAB].d_un.d_ptr;   127		char* strtab=lmap->l_info[DT_STRTAB].d_un.d_ptr;   128		Elf32_Rel* reloc = (Elf32_Rel*) (lmap->l_info[DT_JMPREL].d_un.d_ptr+reloc_offset);   129		Elf32_Sym* sym=&symtab[Elf32_R_SYM(reloc->r_info)];   130		char* symname=sym->st_name+strtab;   131		Elf32_Addr reloc_addr=lmap->l_addr+reloc->r_offset;   132	   133		Elf32_Addr  symaddr=0;   134	   135	   136	   137		symaddr=do_lookup(lmap,symname);   138	   139	   140	   141		if (symaddr>0)   142			{   143				*reloc_addr=symaddr;   144				return symaddr;   145		  	}   146			   147	   exit(-2);   148		   149	   150	   151		   152		   153	}

这里是给出了从一个动态链接库中可重定向的reloc_offset得到要解析函数的名称,如果用图示的方式表示就如下图:

你可能会想:其实还可以用另一种方法,就是把这个reloc sym的st_value直接写入前面的这个调用重定向函数相对应的got中。这样解析时的速度会更快。但现实这样却可能对整个ELF文件结构体系带来很大的麻烦。我将对每一点说明:

  1. 如果是这个reloc sym的地址,那对于一个动态链接库而言,它的加载地址本身就是动态确定的。
  2. 如 果用的是那个Elf32_Sym的st_value地址,那倒是可以与lmap->l_i nfo[DT_STRTAB]一起得到这个sym的name,但如果考虑到在编译的时候有些函数是只对本模块有效,可见的,如在一个文件中定义为 static的函数,则它就是局部可见的,那个时候就不可能是解析为这个函数,而且对c++函数还有更为复杂的情况,这样就会要求一个字段来表示它的属 性,这就是要有了st_info这个数据成员变量。这也就要有了sym的参与了。
  3. 光有Elf32_Sym还是不行,因为就重定位而 言它本身还有一点信息,就是这一个relocation symbol是在本地解析,还是在另外一个真正意义上的动态链接库内被解析,这一情况主要是发生在几个文件编写的模块中,它们编写的一些函数就在链接的时 候被确定了,而另一些则没有,区分的就是relocation 中的r_info了。

从上面的分析来看,一种规范的设计有许多的考虑因素,如果只单一的考虑,那是不行的,特别是要对多个操作系统与平台统一的规范,不能因为就是考虑效率一条就可以了。

在143行是对前面要重定位的函数实现真正的解析函数到位,这样在这个函数被再次调用的时候就不用再来一次了,本来这时就对这个relocation symbol r_info的判断,现在都已经略去了。

真正的解析在do_lookup中实现了,我这里还是它的实现伪代码:

    90	Elf32_Addr  do_lookup(struct link_map* lmap,char* symname)    91	{    92		struct link_map* search_lmap=NULL;    93		Elf32_Sym* symtab;    94		Elf32_Sym* sym;    95		char* strtab;    96		char* find_name;    97		int symindx;    98		    99		Elf32_Word hash=elf_hash_name(symname);   100		for_each_search_lmap_in_search_list(lmap,search_lmap)   101			{   102				symtab=search_lmap->l_info[DT_SYMTAB].d_un.d_ptr;   103				strtab=search_lmap->l_info[DT_STRTAB].d_un.d_ptr;   104	           for (symindx=search_lmap->l_buckets[hash % search_lmap->l_nbuckets];   105	           symindx!=0;symindx=search_lmap->l_chain[symindx])   106	           	{   107	           		sym=&symtab[symindx];   108	   109					find_name=strtab+sym->st_name;   110	           		if (strcmp(find_name,symname)==0)   111	           			return sym->st_value+search_lmap->l_addr;   112	           	}   113	   114	   return 0;   115	    116	   117				   118			}   119	}

100行for_each_search_lmap_in_search_list就是从前面在_dl_map_object_deps中得到的l_searchlist中取下的它本身的依赖动态链接库,中间查找的方法就如下面那张图中所显示的。

上面所表示的就是一个在hash表中symidx偏移处所存的就是下一个偏移所在。最后如果strcmp==0就可以得到了,否则就会返回一个0表示失败了。

现在我们已经把函数的解析过程分析完毕,有必要作一个小结工作:

  1. 在调用函数的动态链接库中,它所用的方法是从plt节的代码执行绝对转移,而转移的地址存放在got节中。
  2. 在 被调用函数的动态链接库中(就是函数实现的动态链接库),它的函数在以DT_HASH与DT_SYMTAB,DT_STRTAB组织起来。组织的方式如下 面的一张图,以symtab中的Elf32_Sym中的st_value表示这个可导出的标记在动态链接库中的偏移量,st_name则是在动态链接库 strtab中的偏移量。
  3. 在调用动态链接库与被调用动态链接库的联系能过的是Elf32_Rel(对MIPS等的体系结构中是 Elf32_Rela),它的r_info体现了这个要导入标记(就是调用方中)的性质,而r_offset则是这个标记在动态链接库中的偏移量。(这个 可以看elf_machine_lazy_rel中的实现)

实际上卸载与加载只是反过程而已,但原来的代码为了提高效率实现在栈内分配内存,不过这样倒使原来简单易懂的变的过于复杂,所以,我这里作了很大的修改,这里是伪代码的实现。

   245	void  dl_close(struct link_map* lmap)   246	{   247		struct link_map** dep_lmaplist=NULL;   248		int i;   249		Elf32_Addr*  fini_call_array;   250		void*  fini_call;   251		struct link_map* curlmap;   252		struct list * has_removed_list=malloc(sizeof(struct list));   253	   254		has_removed_list->lmap=lmap;   255		has_removed_list->next=NULL;   256	   257		if (lmap->l_opencount>1)   258			{   259			 lmap->l_opencount--;   260			return;   261			}   262			   263		lmap->l_opencount--;   264	   265	   266		dep_lmaplist=lmap->l_initfini;   267	   268		   269		   270		for (i=0;dep_lmaplist[i]!=NULL;i++)   271			{   272			  	   273			    try_dl_close(dep_lmaplist[i],has_removed_list);   274			}   275	   276	   277		if (lmap->l_info[DT_FINI_ARRAY].d_un.d_ptr!=NULL  && lmap->l_opencount ==0)   278				{   279				   280				fini_call_array=lmap->l_info[DT_FINI_ARRAY].d_un.d_ptr   281					+lmap->l_addr;   282				unsigned int sz=lmap->l_info[DT_FINI_ARRAYSZ].d_un.d_ptr   283					+lmap->l_addr;   284				   285				while(sz-->0)   286					{   287					   /*call the fini function*/   288					   ((void*)fini_call_array[sz])();   289		   290					}   291				}   292	   293		if (lmap->l_info[DT_FINI].d_un.d_ptr!=NULL && lmap->l_opencount ==0)   294				{   295				fini_call=lmap->l_info[DT_FINI].d_un.d_ptr   296					+lmap->l_addr;   297				   298				((void*)fini_call)();   299				}   300	   301	   302		munmap(lmap->l_map_start,lmap->l_map_end-lmap->l_map_start);   303	   304	   305		   free (lmap->l_initfini);   306	   307		      free (lmap->l_scope);   308		         309		 if (lmap->l_phdr_allocated)   310		    free ((void *) lmap->l_phdr);   311	   312		 free_list(has_removed_list);   313	   314		  free (lmap);   315		   316	   317		return;   318	}

这 里的has_removed_list就是记录整个在这一次dl_close操作中已经被卸载了的动态链接库,主要是为了防止再次卸载已经卸载的动态链接 库。其实先开始判断这是否是已经没有再依赖它本向的动态链接库了。如果没有了(减去1,等于0就是了),那才可以继续去了,接下来不要先把它自己加入这个 动态链接库,试着去卸载它所依赖的动态链接库,这些全做完之后就是它本身的各要点,一是它的DT_FINI_ARRAY中的卸载函数,还有就是 DT_FINI中的函数,这之完了,便是加载到内存内容的去映射化,213行。再就是对struct link_map申请的内存就是了。

你可以看try_dl_close之后的代码就能明白这种可能有的深度的递归过程。

   233	void try_dl_close(struct link_map* lmap,struct list*     234	has_removed_lmap_list)   234	{   235	   if(in_the_list(has_removed_lmap_list,lmap))   236	   	return ;   237	   dl_close_with_list(lmap,has_removed_lmap_list);   238	   return ;   239	   240	}   156	void   dl_close_with_list(struct link_map* lmap,struct list* has_removed_lmap_list)   157	{   158		struct link_map** dep_lmaplist=NULL;   159		int i;   160		Elf32_Addr*  fini_call_array;   161		void*  fini_call;   162		   163		   164	   165		   166	   167		if (lmap->l_opencount>1)   168			{   169			 lmap->l_opencount--;   170			return;   171			}   172		add_to_list_tail_uniq(has_removed_lmap_list,lmap);	   173	   174		lmap->l_opencount--;   175	   176	   177		dep_lmaplist=lmap->l_initfini;   178	   179		   180		   181		for (i=0;dep_lmaplist[i]!=NULL;i++)   182			{   183			  	   184			    try_dl_close(dep_lmaplist[i],has_removed_lmap_list);   185			}   186	   187	   188		if (lmap->l_info[DT_FINI_ARRAY].d_un.d_ptr!=NULL  && lmap->l_opencount ==0)   189				{   190				   191				fini_call_array=lmap->l_info[DT_FINI_ARRAY].d_un.d_ptr   192					+lmap->l_addr;   193				unsigned int sz=lmap->l_info[DT_FINI_ARRAYSZ].d_un.d_ptr   194					+lmap->l_addr;   195				   196				while(sz-->0)   197					{   198					   /*call the fini function*/   199					   ((void*)fini_call_array[sz])();   200		   201					}   202				}   203	   204		if (lmap->l_info[DT_FINI].d_un.d_ptr!=NULL && lmap->l_opencount ==0)   205				{   206				fini_call=lmap->l_info[DT_FINI].d_un.d_ptr   207					+lmap->l_addr;   208				   209				((void*)fini_call)();   210				}   211	   212	   213		munmap(lmap->l_map_start,lmap->l_map_end-lmap->l_map_start);   214	   215	   216		   free (lmap->l_initfini);   217	   218		      free (lmap->l_scope);   219		         220		 if (lmap->l_phdr_allocated)   221		    free ((void *) lmap->l_phdr);   222	   223		  free (lmap);   224		   225	   226		return;   227	   228	}

综 合来看,dl_close这个函数如果是最终要卸载整个可执行文件的工作的话,那就要最高层的可执行文件开始,这里采用对可能有错综复杂的依赖关系的动态 链接库使用了一个mark_removed与dl_close相结合的方法,在不断的递归调用中,把所有的动态链接库l_opencount减少到0。最 后释放所有的内存空间。这种情况如果你与linux内核中delet_module的调用相对比,也可以看的更清楚。

动 态链接库的实现发展到现今已经相当完善,它在理论与实践方面对于我们学习操作系统和编译语言提供了一个很好的范例。但是,动态链接库的实现毕竟还是只能在 一个操作系统,一个单机,一种编程语言(如果是c++编程语言,则这一点也满足不了,因为不同的编译器可能对function name mangling-----函数名称混译也不同),对于现在网络化的信息产业是不够的。所以,出现了以这个为目标的二进制实现规范,这就是OMG (object model group )所制定出来的 CORBA,和由 Microsoft 所制定出来的 COM,我可能以后的日子中详细来探讨这些最新发展。

[1]glibc-2.3.2 sourcecode 这是我这里主要的代码来源,可以在 中下载

[2]John R.Levine "Linkers and Loaders" 介绍动态链接库技术的经典

[3] Hongjiu Lu "ELF: From The Programmer's Perspective" 好的ELF编程的参考。在 http://linux4u.jinr.ru/usoft/WWW/www_debian.org/Documentation/elf/elf.html 可以看到


王瑞川,从事 Linux 开发工作,愿与志同道合的人士一起探讨,电子邮件地址是 jeppeterone@163.com



阅读(3810) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~