前言
用 MS Windows 一段时间的读者,应该都听过动态函式库这个名词。在 Windows 9X/ME 或是 Windows NT/2000 中,常见到的动态函式库为副档名 “DLL” (Dynamic Loading Library)的档案。 而在 Linux 中,当然也有动态函式库的机制存在。如此一来,所撰写的程序便无需透过静态连结(Static Link),而可以在编程时透过动态连结(Dynamic Link)产生我们所要的执行档。 使用动态函式库的好处有许多。首先,就是由於执行档主要呼叫的函式都包含於动态函式库中,所以档案所占的空间可以因而缩小。其次,当动态函式库的函式内容有所改变时,呼叫该动态函式库的程序,可以在最小修正甚至是不需重新编程的情况下,就可以叫用到新版本的函式库服务。 对於发展 Embedded Linux 的业者来说,能够尽可能减少应用程序执行环境所需空间的大小,便可以把日後成品所需的 Flash 容量降到最低,在整体成本以及所耗用的记忆体空间来说,都可以得到许多的好处,而在动态函式库来著手所得到的效益也是相当可观的,尽可能的删去不必要的动态函式库,以及针对动态函式库改写来缩小或是透过工具删去用不到的函式,都可以带来许多的助益。 当然棉,动态函式库的好处还不只这些,相信读者们在文章中可以发现其它的妙用的。
档案格式(ELF VS A.out)
首先,我们必须先确定目前所执行的 Linux Kernel 版本有开启 ELF 与 A.out 执行档案格式的支援(通常都会有)
Kernel support for a.out binaries (CONFIG_BINFMT_AOUT) [M/n/y/?]
Kernel support for ELF binaries (CONFIG_BINFMT_ELF) [Y/m/n/?] 举个例子来说,若要执行 a.out 格式的执行档时,我们必须确认 CONFIG_BINFMT_AOUT 为 Y,也就是由 Kernel 直接支援 a.out 档案格式,或者 CONFIG_BINFMT_AOUT 为 M,也就是不把 a.out 的档案格式支援编入 Kernel 中,改以 Module 的形式存在,一旦 Kernel 需要执行 a.out 格式的程序时,在动态的载入该 Module,来启动具备执行 a.out 执行档的能力。不过 a.out 执行档的格式,是 Unix 上使用了相当久的的档案格式,ELF 是目前较新的的档案格式。a.out 档案格式共有三个 Section,分别为.text, .data, 及 .bss,并还包括了一个文字表(String Table)与符号表(Symbol Table)。与ELF 档案格式比较起来,a.out 相形之下显得较为缺乏弹性,ELF档案格式允许多个节区的存在,执行档可以根据需求提供应用程序执行环境的节区,并且 ELF 档支援了 32-bit 与 64-bit 的执行环境。其实,两者之间还有其它规格上的不同,有兴趣的读者也可以自行找一些相关的资料来比较即可了解。 再来呢,我们就来讨论动态函式库的档案格式。我们都知道在 Linux中有 a.out 与 ELF 两种档案的格式,其中目前我们最常见的便是 ELF 档案格式。在 Linux 的函式库目录中,我们常常可以见到 “*.so” 的档案,例如:“/lib/libc.so.6” 或是 “/lib/ld-linux.so.2”。这些便是在 Linux中所常见到的动态函式库档案。由下图我们可以看到动态函式库 libc.so.6 的 ELF Header:
libc.so.6 的 ELF Header e_ident ->EI_MAG0:7fh
->EI_MAG1:E
->EI_MAG2:L
->EI_MAG3:F
->EI_CLASS:32-bit objects
->EI_DATA:ELFDATA2LSB
->EI_VERSION:1h
->EI_PAD:0h
->EI_NIDENT:3h e_type: ET_DYN (Shared Obj File) e_machine:Intel 80386
e_version:Current version
e_entry:182a8h
e_phoff:34h
e_shoff:3bbf8ch
e_flags:0h
e_ehsize:34h
e_phentsize:20h
e_phnum:5h
e_shentsize:28h
e_shnum:40h
e_shstrndx:3dh 由图中,我们可以注意到 e_type: ET_DYN,e_type 是在ELF 档案的格式中,用来描述目前该档的档案型态,我们所举的例子为 libc.so.6 这个动态函式库的档案,所以 e_type 的属性为 Shared Obj File。 当然棉,我们若再拿一个ELF执行档来比较也是不错的,所以如下图
ls 的 ELF Header e_ident ->EI_MAG0:7fh
->EI_MAG1:E
->EI_MAG2:L
->EI_MAG3:F
->EI_CLASS:32-bit objects
->EI_DATA:ELFDATA2LSB
->EI_VERSION:1h
->EI_PAD:0h
->EI_NIDENT:2h e_type: ET_EXEC (Executable file) e_machine:Intel 80386
e_version:Current version
e_entry:8049130h
e_phoff:34h
e_shoff:bea4h
e_flags:0h
e_ehsize:34h
e_phentsize:20h
e_phnum:6h
e_shentsize:28h
e_shnum:1ah
e_shstrndx:19h 我们可以注意到 e_type: ET_EXEC,这就是 ELF 档中对於执行档所定义的档案属性。
动态连结 VS 静态联结
在 Linux 中,执行档我们可以编程成静态联结以及动态连结,以下我们举一个简短的程序作为例子:
#include
int main()
{
printf("\ntest");
} 若我们执行 :
[root@hlchou /root]# gcc test.c -o test 所产生出来的执行档 test,预设为使用动态函式库,所以我们可以用以下的指令 :
[root@hlchou /root]# ldd test
libc.so.6 => /lib/libc.so.6 (0x40016000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) 来得知目前该执行档共用了哪些动态函式库,以我们所举的 test 执行档来说,共用了两个动态函式库,分别为 libc.so.6 与 ld-linux.so.2。我们还可以透过下面的 file 指令,来得知该执行档的相关属性,如下
[root@hlchou /root]# file test
test: ELF 32-bit LSB executable, Intel 80386, version 1, dynamically linked (use
s shared libs), not stripped not stripped 表示这个执行档还没有透过 strip 指令来把执行时用不到的符号、以及相关除错的资讯删除,举个例子来说,目前这个test 执行档大小约为 11694 bytes
[root@hlchou /root]# ls -l test
-rwxr-xr-x 1 root root 11694 Oct 24 02:31 test 经过strip後,则变为 3004 bytes
[root@hlchou /root]# strip test
[root@hlchou /root]# ls -l test
-rwxr-xr-x 1 root root 3004 Oct 24 02:48 test 不过读者必须注意到一点,经过 strip 过的执行档,就无法透过其它的除错软件从里面取得函式在编程时所附的相关资讯,这些资讯对我们在除错软件时,可以提供不少的帮助,各位在应用上请自行注意。 相对於编程出来使用动态函式库的执行档 test,我们也可以做出静态联结的执行档 test
[root@hlchou /root]# gcc -static test.c -o test 透过指令 ldd,我们可以确定执行档 test 并没有使用到动态函式库
[root@hlchou /root]# ldd test
not a dynamic executable 再透过指令 file,可以注意到 test 目前为 statically linked,且亦尚未经过 strip
[root@hlchou /root]# file test
test: ELF 32-bit LSB executable, Intel 80386, version 1, statically linked, not stripped 相信大夥都会好奇,使用静态联结,且又没有经过 strip 删去不必要的符号的执行档的大小会是多少,透过 ls -l来看,我们发现大小变成 932358 bytes 比起静态联结的执行档大了相当多
[root@hlchou /root]# ls -l test
-rwxr-xr-x 1 root root 932258 Oct 24 02:51 test 若再经过 strip,则档案大小变为 215364 bytes
[root@hlchou /root]# strip test
[root@hlchou /root]# ls -l test
-rwxr-xr-x 1 root root 215364 Oct 24 02:55 test 与使用动态函式库的执行档 test 比较起来,大了约 70倍 (215364/3004)。因此,整体来说,在使用的环境中使用动态函式库并且经过 strip 处理的话,可以让整体的空间较为精简。许多执行档都会用到同一组的函式库,像 libc 中的函式是每个执行档都会使用到的,若是使用动态函式库,则可以尽量减少同样的函式库内容重复存在系统中,进而达到节省空间的目的。 笔者一年前曾写过一个可以用来删去动态函式库中不必要函式的工具,针对这个只用到了 printf 的程序来产生新的 libc.so 的话,我们可以得到一个精简过的 libc.so 大小约为 219068 bytes
[root@hlchoua lib]# ls -l libc.so*
-rwxr-xr-x 1 root root 219068 Nov 2 04:47 libc.so
lrwxrwxrwx 1 root root 7 Nov 1 03:40 libc.so.6 -> libc.so 与静态联结的执行档大小 215364 bytes 比较起来,若是在这个环境中使用了动态函式库的话成本约为 3004 + 219068 =222072 bytes,不过这是只有一个执行档的情况下,使用动态函式库的环境会小输给使用静态联结的环境,在一个基本的 Linux 环境中,如果大量的使用动态函式库的话,像是有 2 个以上的执行档的话,那用动态函式库的成本就大大的降低了,像如果两个执行档都只用到了 printf,那静态联结的成本为 215364 *2 =430728 bytes,而使用动态函式库的成本为3004 *2 + 219068=225076 bytes,两者相差约一倍。 很明显的,我们可以看到动态函式库在 Linux 环境中所发挥的妙用,它大幅的降低了整体环境的持有成本,提高了环境空间的利用率。
ld-linux.so.2
在 RedHat 6.1 中,我们可以在 /lib 或是 /usr/lib 目录底下找到许多系统上所安装的动态函式库,在文章的这个部分,笔者将把整个函式库大略的架构作一个说明。 其实 Linux 跟 Windows 一样,提供了一组很基本的动态函式库,在 Windows 上面我们知道 kernel32.dll 提供了其它动态函式库基本的函式呼叫,而在 Linux 上面则透过 ld-linux.so.2 提供了其它动态函式库基本的函式,在笔者电脑的 RedHat6.1 上,ld-linux.so.2 是透过 link 到 ld-2.1.2.so(这部分需视各人所使用的 glibc 版本不同而定)
-rwxr-xr-x 1 root root 368878 Jan 20 14:28 ld-2.1.2.so
lrwxrwxrwx 1 root root 11 Jan 20 14:28 ld-linux.so.2 -> ld-2.1.2.so ld-linux.so 是属於 Glibc (GNU C Library) 套件的一部分,只要是使用 Glibc 动态函式库的环境,就可以见到 ld-linux.so 的踪影。 接下来,我们透过指令 ldd 来验证出各个函式库间的阶层关系,首先如下图我们执行了 ”ldd ls”、”ldd pwd” 与 “ldd vi”,可以看出各个执行档呼叫了哪些动态函式库,像执行档 ls 呼叫了 /lib/libc.so.6 (0x40016000) 与 /lib/ld-linux.so.2 (0x40000000),而括号内的数字为该函式库载入记忆体的位置,在本文的稍後,会介绍到函式库载入时的细节,到时读者会有更深入的了解。 其实我们不难发现,在 Linux 上使用动态函式库的执行档,几乎都会去呼叫 libc.so.6 与 ld-linux.so.2 这两个动态函式库,笔者过去修改 Glibc 的套件时,也了解到在 Linux 中函式库的关系,ld-linux.so.2 算是最底层的动态函式库,它本身为静态联结,主要的工作是提供基本的函式给其他的函式库,而我们最常会呼叫的 libc.so.6 则是以 ld-linux.so.2 为基础的一个架构完成的动态函式库,它几乎负责了所有我们常用的标准 C 函式库,像是我们在 Linux 下写的 Socket 程序,其中的connect()、bind()、send() .....之类的函式,都是由 libc.so.6 所提供的。 也因此,libc.so.6 的大小也是相当可观的,在 RedHat 6.1 中经过 strip 後,大小约为 1052428 bytes。
[root@hlchoua /root]# ldd /bin/ls
libc.so.6 => /lib/libc.so.6 (0x40016000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
[root@hlchoua /root]# ldd /bin/pwd
libc.so.6 => /lib/libc.so.6 (0x40016000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
[root@hlchoua /root]# ldd /bin/vi
libtermcap.so.2 => /lib/libtermcap.so.2 (0x40016000)
libc.so.6 => /lib/libc.so.6 (0x4001b000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) 如下,我们透过 ldd 验证 vi 所用到的动态函式库 /lib/libtermcap.so.2,它本身是呼叫了 libc.so.6 的函式所组成的。
[root@hlchoua /root]# ldd /lib/libtermcap.so.2
libc.so.6 => /lib/libc.so.6 (0x40007000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000) 接下来,我们依序测试了 /lib/libc.so.6 与 /lib/ld-linux.so.2
[root@hlchoua /root]# ldd /lib/libc.so.6
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
[root@hlchoua /root]# ldd /lib/ld-linux.so.2
statically linked 我们可以整理以上的结论,画成如下的一个架构图
在这个图中,我们可以清楚的明白 ld-linux.so.2 负责了最基础的函式,而 libc.so.6 再根据这些基本的函式架构了完整的 C 函式库,供其它的动态函式库或是应用程序来呼叫。 透过笔者所写的一个 ELF 工具程序(注二),我们也可以清楚的看到 libc.so.6呼叫了 ld-linux.so.2 哪些函式
[root@hlchoua /root]# /I-elf /lib/libc.so.6|more
========================================================
open_target_file:/lib/libc.so.6
==>ld-linux.so.2
__register_frame_table
cfsetispeed
xdr_int32_t
utmpname
_dl_global_scope_alloc
__strcasestr
hdestroy_r
rename
__iswctype_l
__sigaddset
xdr_callmsg
pthread_setcancelstate
xdr_union
__wcstoul_internal
setttyent
strrchr
__sysv_signal ...┅(more) 其实,ldd 指令为一个 shell script 的档案,它主要是透过呼叫 ”run-time dynamic linker” 的命令,并以 LD_TRACE_LOADED_OBJECTS 为参数来秀出这些结果的。 如下,就是我们不透过 ldd 指令直接以 eval 搭配 LD_TRACE_LOADED_OBJECTS参数来检视 libcrypt.So.1 与 libm.so.6这两个动态函式库的结果。
[root@hlchoua /root]# eval LD_TRACE_LOADED_OBJECTS=1 '/lib/libcrypt-2.1.2.so'
libc.so.6 => /lib/libc.so.6 (0x40016000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
[root@hlchoua /root]# eval LD_TRACE_LOADED_OBJECTS=1 '/lib/libm.so.6'
libc.so.6 => /lib/libc.so.6 (0x40016000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
如何得知动态函式库的位置?
提到 Linux 的动态函式库,读者首先会面对到的问题应该是,当我们执行程序时,系统会到哪些目录去搜寻执行档所用到的函式库呢? 其实如果我们去检视 ”/etc/ld.so.conf” 档案中的内容如下:
/usr/X11R6/lib
/usr/i486-linux-libc5/lib 这里面所存放的是在 Linux 中搜寻动态函式库时的路径资讯,不过这并不是系统所会搜寻的所有路径,以笔者的 RedHat 6.1 来说,我的程序用到了 libreadline.so.3 这个动态函式库,可是笔者把这个函式库移除了,所以实际上,它并不存在这台电脑中,当我启动有用到 libreadline.so.3 的执行档时,系统会先去检视这个函式库是否在动态函式库的快取(档名为 ld.so.cache,在本文稍後会提到)中存在,如果不存在的话,系统仍会试著去找寻这个动态函式库的档案,它所搜寻的路径如下顺序
/lib/i686/mmx/libreadline.so.3
/lib/i686/libreadline.so.3
/lib/mmx/libreadline.so.3
/lib/libreadline.so.3
/usr/lib/i686/mmx/libreadline.so.3
/usr/lib/i686/libreadline.so.3
/usr/lib/mmx/libreadline.so.3
/usr/lib/libreadline.so.3 如果还是找不到的话,就会显示如下的错误讯息
[root@hlchoua bin]#./test
test: error in loading shared libraries: libreadline.so.3: cannot open shared object file: No such file or directory 如果先不透过 ldconfig 把函式库路径设定档 ld.so.conf 的内容处理过,直接把 libreadline.so.3 放到系统内定会去搜寻