Linux动态链接(5)动态库符号搜索顺序

一、动态搜索与静态搜索
这里的动态搜索是指通过dlopen+dlsym来搜索动态库符号的过程,而静态搜索则是指程序在运行的过程中的惰性链接实现。这里其实又是一个比较边界的问题,但是也是可能存在的,另外这些问题可以促使感兴趣的同学看一下真正的实现代码。
问题是这样的:
假设说一个静态链接的文件通过dlopen打开一个文件,例如pthread.so文件,这个pthread库会依赖glibc.so,但是glibc的实现代码已经通过静态链接存在于主程序中,此时如果pthread库中执行printf(注意:pthread库是通过dlopen打开的,它并没有静态链接入可执行程序),此时pthread库中执行的printf到底位于exe中还是glibc.so中?
二、测试代码工程
[tsecer@Harry searchorder]$ ls
callee.c  caller.c  main.c  Makefile

[tsecer@Harry searchorder]$ cat callee.c 
#include <stdio.h>
void callee(void)
{
    printf("callee in %s\n",__FILE__);
}

[tsecer@Harry searchorder]$ cat caller.c  caller库在被加载的时候执行callee函数,这个函数在它的加载者main.exe和它的依赖库libcallee.so中均有定义。
void __attribute__((constructor)) caller(void)
{
    extern void callee(void);
    callee();
}

[tsecer@Harry searchorder]$ cat main.c 
#include <stdio.h>
#include <dlfcn.h>
void callee(void)
{
    printf("callee in %s \n",__FILE__);
}

int main()
{
    void * handle = dlopen("libcaller.so",RTLD_LAZY);
    printf("handle is %x\n",handle);
    void (*callcallee)();
    callcallee=dlsym(handle,"callee"); 之后将会看到,在caller中执行的callee函数和通过dlsym搜索到的符号不同
    (*callcallee)();
    return 0;    
}

[tsecer@Harry searchorder]$ cat Makefile 
default:libcaller.so
    gcc -L. -lcaller main.c -o main.exe -ldl
    LD_LIBRARY_PATH=. ./main.exe
libcaller.so:caller.c libcallee.so
    gcc  -shared -fPIC -o $@ $< -L. -lcallee
libcallee.so:callee.c
    gcc -shared -fPIC -o $@ $< 
clean:
    rm -f *.so *.o *.exe
    
[tsecer@Harry searchorder]$ make
gcc -shared -fPIC -o libcallee.so callee.c 
gcc  -shared -fPIC -o libcaller.so caller.c -L. -lcallee
gcc -L. -lcaller main.c -o main.exe -ldl
/usr/bin/ld: warning: libcallee.so, needed by ./libcaller.so, not found (try using -rpath or -rpath-link)
LD_LIBRARY_PATH=. ./main.exe
callee in main.c 这个函数调用是libcaller.so文件中的初始化函数calle调用的callee,它搜索到了主程序中的callee定义
handle is b7897410
callee in callee.c 这里是通过dlsym搜索到的callee定义,它搜索到了libcallee.so中定义。
[tsecer@Harry searchorder]$
三、搜索列表的构造
主要在dl-object.c文件中_dl_new_object函数,它构造了两个独立的搜索列表(用C库的表达方法叫scope),每个scope是一个so搜索链表,所以可以认为其中的scope都是四重指针, link_map ****scope,这是一个本质数据结构,大家可以慢慢理解。
  if (GL(dl_ns)[nsid]._ns_loaded != NULL)
    {
……
      /* Add the global scope.  */
      new->l_scope[idx++] = &GL(dl_ns)[nsid]._ns_loaded->l_searchlist;
    }
……
if (idx == 0 || &loader->l_searchlist != new->l_scope[0])
    {
      if ((mode & RTLD_DEEPBIND) != 0 && idx != 0)
    {
      new->l_scope[1] = new->l_scope[0];
      idx = 0;
    }

      new->l_scope[idx] = &loader->l_searchlist;
    }

  new->l_local_scope[0] = &new->l_searchlist;
 这里可以看到,每个so构造了两个搜索空间,它们相互独立又有联系,具体来说,l_scope的第一项为全局搜索空间(也就是RTLD_GLOBAL空间),第二项为局部搜索空间,它和l_local_scope搜索空间相同,而l_local_scope则只有自己的一个l_searchlist空间这空间在_dl_map_object_deps函数中初始化,它的初始化顺序是从自己的依赖中搜索的,所以称之为“局部”空间。这个搜索顺序和初始化函数执行的顺序刚好完全相反,使用广度优先搜索。为了了解这个代码中广度优先方法,大家需要首先理解广度优先算法的实现原理。在_dl_map_object_deps函数中,runp就是深度优先队列的头指针,依赖被不断的加到这个队列中,然后再循环,所以这个广度优先本质上还是比较简单的。
四、dlsym的搜索顺序
dlsym是一个主动搜索过程glibc-2.7\elf\dl-sym.c:do_sym函数中
    {
      /* Search the scope of the given object.  */
      struct link_map *map = handle;
      result = GLRO(dl_lookup_symbol_x) (name, map, &ref, map->l_local_scope,
                     vers, 0, flags, NULL);
    }
主动调用dlsym函数时,它只搜索自己和自己的依赖,所以能够搜索到libcallee.so中定义的callee函数。
五、惰性链接时搜索顺序
dl-runtime.c:_dl_fixup 函数

      result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
                    version, ELF_RTYPE_CLASS_PLT, flags, NULL);
这里优先搜索全局空间,所以搜索到主程序中callee定义,如果主程序main中没有定义,那么就会搜索到libcallee.so中实现。但是这个行为可以通过dlopen时使用RTLD_DEEPBIND标志位来让动态链接优先搜索到直接依赖中定义,有兴趣的同学可以自己试一下,我这里的glibc2.7版本是有定义这个标志的,不知道是什么时候引入C库的,如果提示找不到,那需要换一个高版本的c库。
 
 
 
 
 

posted on 2019-03-06 21:51  tsecer  阅读(556)  评论(0编辑  收藏  举报

导航