by falcon
2008-02-26
Linux支持动态连接库,不仅节省了磁盘、内存空间,而且可以提高程序运行效率[1]。不过引入动态连接库也可能会带来很多问题,例如动态连接库的调试[4]、升级更新[5]和潜在的安全威胁[6][7]。这里主要讨论符号的动态链接过程,即程序在执行过程中,对其中包含的一些未确定地址的符号进行重定位的过程[3][8]。
本篇主要参考资料[3]和[8],前者侧重实践,后者侧重原理,把两者结合起来就方便理解程序的动态链接过程了。另外,动态连接库的创建、使用以及调用动态连接库的部分参考了资料[1][2]。
下面先来看看几个基本概念,接着就介绍动态连接库的创建、隐式和显示调用,最后介绍符号的动态链接细节。
1、基本概念
1.1 ELF
ELF是Linux支持的一种程序文件格式,本身包含重定位、执行、共享(动态连接库)三种类型。(man elf)
代码:
Code:
[Ctrl+A Select All]
演示:
$ gcc -c test.c #通过-c生成可重定位文件test.o,这里不会进行链接 |
虽然ELF文件本身就支持三种不同的类型,不过它有一个统一的结构。这个结构是:
文件头部(ELF Header)
程序头部表(Program Header Table)
节区1(Section1)
节区2(Section2)
节区3(Section3)
...
节区头部表(Section Header Table)
无论是文件头部、程序头部表、节区头部表,还是节区,它们都对应着C语言里头的一些结构体(elf.h中定义)。文件头部主要描述ELF文件的类型,大小,运行平台,以及和程序头部表和节区头部表相关的信息。节区头部表则用于可重定位文件,以便描述各个节区的信息,这些信息包括节区的名字、类型、大小等。程序头部表则用于描述可执行文件或者动态连接库,以便系统加载和执行它们。而节区主要存放各种特定类型的信息,比如程序的正文区(代码)、数据区(初始化和未初始化的数据)、调试信息、以及用于动态链接的一些节区,比如解释器(.interp)节区将指定程序动态装载/连接器ld-linux.so的位置,而过程链接表(plt)、全局偏移表(got)、重定位表则用于辅助动态链接过程。
1.2 符号
对于可执行文件除了编译器引入的一些符号外,主要就是用户自定义的全局变量,函数等,而对于可重定位文件仅仅包含用户自定义的一些符号。
$ gcc -c test.c #生成可重定位文件test.o |
1.3 重定位:"是将符号引用与符号定义进行连接的过程"[8]
从上面的演示可以看出,重定位文件test.o中的符号地址都是没有确定的,而经过“静态"链接(gcc默认调用ld进行链接)以后有两个符号地址已经确定了,这样一个确定符号地址的过程实际上就是链接的实质。链接过后,对符号的引用变成了对地址(定义符号时确定该地址)的引用,这样程序运行时就可通过访问内存地址而访问特定的数据。
我们也注意到符号printf在可重定位文件和可执行文件中的地址都没有确定,这意味着该符号是一个外部符号,可能定义在动态连接库中,在程序运行时需要通过动态链接器(ld-linux.so)进行重定位,即动态链接。
通过这个演示可以看出printf确实在glibc中有定义。
$ nm /lib/libc.so.6 | grep "/ printf$" |
1.4 动态链接
动态链接就是在程序运行时对符号进行重定位,确定符号对应的内存地址的过程。
Linux下符号的动态链接默认采用Lazy Mode方式[3],也就是说在程序运行过程中用到该符号时才去解析它的地址。这样一种符号解析方式有一个好处:只解析那些用到的符号,而对那些不用的符号则永远不用解析,从而提高程序的执行效率。
不过这种默认是可以通过设置LD_BIND_NOW为非空来打破的(下面会通过实例来分析这个变量的作用),也就是说如果设置了这个变量,动态链接器将在程序加载后和符号被使用之前就对这些符号的地址进行解析。
1.5 动态连接库
上面提到重定位的过程就是对符号引用和符号地址进行链接的过程,而动态链接过程涉及到的符号引用和符号定义分别对应可执行文件和动态连接库,在可执行文件中可能引用了某些动态连接库中定义的符号,这类符号通常是函数。
为了让动态链接器能够进行符号的重定位,必须把动态连接库的相关信息写入到可执行文件当中,这些信息是什么呢?
$ readelf -d test | grep NEEDED |
ELF文件有一个特别的节区,.dynamic,它存放了和动态链接相关的很多信息,例如动态链接器通过它找到该文件使用的动态连接库。不过,该信息并未包含动态连接库libc.so.6的绝对路径,那动态链接器去哪里查找相应的库呢?
通过LD_LIBRARY_PATH参数,它类似shell解释器中用于查找可执行文件的PATH环境变量,也是通过冒号分开指定了各个存放库函数的路径。该变量实际上也可以通过/etc/ld.so.conf文件来指定,一行对应一个路径名。为了提高查找和加载动态连接库的效率,系统启动后会通过ldconfig工具创建一个库的缓存/etc/ld.so.cache。如果用户通过/etc/ld.so.conf加入了新的库搜索路径或者是把新库加到某个原有的库目录下,最好是执行一下ldconf以便刷新缓存。
需要补充的是,因为动态连接库本身还可能引用其他的库,那么一个可执行文件的动态符号链接过程可能涉及到多个库,通过read -d可以打印出该文件直接依赖的库,而通过ldd命令则可以打印出所有依赖或者间接依赖的库。
$ ldd test |
lib.so.6通过read -d就可以看到的,是直接依赖的库;而linux-gate.so.1在文件系统中并没有对应的库文件,它是一个虚拟的动态连接库,对应进程内存映像的内核部分,更多细节请参考资料[11];而/lib/ld-linux.so.2正好是动态链接器,系统需要用它来进行符号重定位。那ldd是怎么知道/lib/ld-linux.so就是该文件的动态链接器呢?
那是因为ELF文件通过专门的节区指定了动态链接器,这个节区就是.interp。
$ readelf -x .interp test |
可以看到这个节区刚好有字符串/lib/ld-linux.so.2,即ld-linux.so的绝对路径。
我们发现,与libc.so不同的是,ld-linux.so的路径是绝对路径,而libc.so仅仅包含了文件名。原因是:程序被执行时,ld-linux.so将最先被装载到内存中,没有其他程序知道去哪里查找ld-linux.so,所以它的路径必须是绝对的;当ld-linux.so被装载以后,由它来去装载可执行文件和相关的共享库,它将根据PATH变量和LD_LIBRARY_PATH变量去磁盘上查找它们,因此可执行文件和共享库都可以不指定绝对路径。
下面着重介绍动态连接器本身。
1.6 动态连接器(dynamic linker/loader)
Linux下elf文件的动态链接器是ld-linux.so,即/lib/ld-linux.so.2。从名字来看和静态连接器ld(gcc默认使用的连接器,见参考资料[10])类似。通过man ld-linux可以获取与动态链接器相关的资料,包括各种相关的环境变量和文件都有详细的说明。
对于环境变量,除了上面提到过的LD_LIBRARY_PATH和LD_BIND_NOW变量外,还有其他几个重要参数,比如LD_PRELOAD用于指定预装载一些库,以便替换其他库中的函数,从而做一些安全方面的处理[6][9][12],而环境变量LD_DEBUG可以用来进行动态链接的相关调试。
对于文件,除了上面提到的ld.so.conf和ld.so.cache外,还有一个文件/etc/ld.so.preload用于指定需要预装载的库。
从上一小节中发现有一个专门的节区.interp存放有动态链接器,但是这个节区为什么叫做.interp(interpeter)呢?因为当shell解释器或者其他父进程通过exec启动我们的程序时,系统会先为ld-linux创建内存映像,然后把控制权交给ld-linux,之后ld-linux负责为可执行程序提供运行环境,负责解释程序的运行,因此ld-linux也叫做dynamic