7.7. Linux动态链接器的实现

Linux动态链接器本身是个共享对象:
catherine~/repos/my_bible $ /lib/ld-linux.so.2 
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]
You have invoked `ld.so', the helper program for shared library executables.
This program usually lives in the file `/lib/ld.so', and special directives
in executable files using ELF shared libraries tell the system's program
loader to load the helper program from this file.  This helper program loads
the shared libraries needed by the program executable, prepares the program
to run, and runs it.  You may invoke this helper program directly from the
command line to load and run an ELF executable file; this is like executing
that file itself, but always uses this helper program from the file you
specified, instead of the helper program file specified in the executable
file you run.  This is mostly of use for maintainers to test new versions
of this helper program; chances are you did not intend to run this program.

  --list                list all dependencies and how they are resolved
  --verify              verify that given object really is a dynamically linked
                        object we can handle
  --library-path PATH   use given PATH instead of content of the environment
                        variable LD_LIBRARY_PATH
  --inhibit-rpath LIST  ignore RUNPATH and RPATH information in object names
                        in LIST
  --audit LIST          use objects named in LIST as auditors

Linux的动态链接器是Glibc的一部分,源码位于sysdeps/i386/dl-machine.h中的_start,_start()在sysdeps/i386/elf/start.S

_start调用位于elf/rtld.c的_dl_start()函数,它首先对ld.so进行重定位,完成自举后就可以调用其他函数并访问全局变量了。调用_dl_start_final,收集一些基本运行参数,进入_dl_sysdep_start,这个函数进行一些平台相关的处理之后进入_dl_main,这个就是动态链接器真正的主函数。

动态链接器本身是静态链接的,本身应该是PIC的,便于重定位和代码共享,本身ld.so可以直接执行,装载地址是0,作为共享库,内核在装载它时会为其选择一个合适的装载地址。


显式运行时链接:
运行时加载,让程序自己在运行时控制加载制定的模块,并可以在不需要该模块时将其卸载。动态装载库。

在Linux中,动态库实际上跟一般的共享对象没区别。主要区别是共享对象由动态链接器在程序启动之前负责装载和链接的。而动态库是通过一系列API程序自己控制的,打开动态库dlopen(),查找符号dlsym(),错误处理dlerror(),以及关闭动态库dlclose()。<dlfcn.h>

dlopen():
void *dlopen (const char *filename, int flag)
第一个参数是被加载动态库的路径,如果是绝对路径直接尝试打开,如果是相对路径,查找如下:
a。环境变量LD_LIBRARY_PATH指定的一系列目录
b。由/etc/ld.so.cache里面指定的共享库路径
c。/lib,/usr/lib
如果库在多个目录下由不同副本会导致系统极为不可靠。

如果filename是0,那么dlopen会返回全局符号表的句柄,我们可以在运行时找到全局符号表里的任何符号,并可执行他们。全局符号表包含了程序的可执行文件本身,被动态链接器加载到进程中的所有共享模块已经在运行时通过dlopen打开并使用RTLD_GLOBAL方式的模块中的符号。

第二个参数表示函数符号的解析方式,常量RTLD_LAZY表示使用延迟绑定,即PLT机制。而RTLD_NOW表示当模块被加载时即完成所有函数的绑定工作,如果有任何未定义的符号引用的绑定工作无法完成。另外还有RTLD_GLOBAL跟上面两种的一个配合使用,表示被加载的模块的全局符号合并到进程全局符号表中,使得以后加载的模块可以使用这些符号。

dlopen返回被加载的模块的句柄,如果失败返回NULL,如果已经被加载过了返回同一个句柄。如果模块间由依赖关系,需要手动加载被依赖模块。

实际上dlopen还在加载模块时执行模块中的初始化部分代码。.init段的代码。

dlsym():
void *dlsym (void *handle, char *symbol);
第一个参数是dlopen()返回的动态库句柄
第二个参数是所要查找的符号的名字。
如果dlsym()找到相应的符号,返回符号的值,如果没有找到返回NULL。
dlsym()返回值对于不同类型的符号,意义不同。
如果查找函数符号,返回函数地址。如果查找变量符号,返回变量地址。如果查找常量符号,返回该常量的值。但如果该常量刚好是NULL或0,那么需要使用dlerror()函数。
如果符号找到了,dlerror()返回NULL,如果没有,dlerror返回相应的错误信息。

符号优先级:
当多个同名符号冲突时,先装入的符号优先,装载序列。不管是动态链接器装入的还是dlopen()装入的,都采用装载序列。

dlsym()查找符号时优先级分为两种类型。第一种情况,如果我们是在全局符号表中进行符号查找即dlopen的参数filename是NULL,那么使用装载序列。第二种情况,如果对于某个dlopen()打开的共享对象进行符号查找的话,采用依赖序列的优先级。以被dlopen()打开的共享对象为根节点,对它所依赖的共享对象进行广度优先遍历,直到直到符号为止。


dlerror():
判断上次调用是否成功,上次失败返回char *字符串,否则NULL。

dlclose():
将一个已经加载的模块卸载,系统维持一个加载引用计数器,每次使用dlopen()加载某模块,相应计数器加1;每次使用dlclose()卸载某模块,相应计数器减1.当计数器减到0,才真正卸载模块,先执行.fini段的代码,然后将相应符号从符号表中去除,取消进程空间的映射关系,然后关闭模块文件。


运行时装载的程序:
可以通过命令行来执行共享对象里的任意函数:由命令行给出共享对象路径,函数名和相关参数,然后程序通过运行时加载将该模块加载到进程中,查找相应的函数,并执行它,然后将执行结果打印出来。
runso /lib/foobar.so function arg1 arg2 arg3 return_type
为了支持不同的函数参数组合,通过了解函数调用约定,在调用函数之前伪造好相应的栈,为了能够直接操作栈,不得不使用嵌入汇编代码。