Linux 动态函式库解析
聂飞注:本文猜测是从一个港台版本文章经过繁体转成的简体版本,如文中提到的:
档案、函式等概念,我们大陆这边一般都称为文件、函数等,呵呵,不过,不影响读者欣赏。
原文地址(繁体)我没有找到,下面是从以下连接处整理得到:
摘要
使用动态函式库的好处有许多。首先,就是由於执行档主要呼叫的函式都包含於动态函式库中,所以档案所占的空间可以因而缩小。其次,当动态函式库的函式内容有所改变时,呼叫该动态函式库的程序,可以在最小修正甚至是不需重新编程的情况下,就可以叫用到新版本的函式库服务。
(2002-08-22 11:19:15)
By Wing
前言
用 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 执行档案格式的支援(通常都会有)
|
举个例子来说,若要执行 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:
|
由图中,我们可以注意到 e_type: ET_DYN,e_type 是在ELF 档案的格式中,用来描述目前该档的档案型态,我们所举的例子为 libc.so.6 这个动态函式库的档案,所以 e_type 的属性为 Shared Obj File。
当然棉,我们若再拿一个ELF执行档来比较也是不错的,所以如下图
|
我们可以注意到 e_type: ET_EXEC,这就是 ELF 档中对於执行档所定义的档案属性
动态连结 VS 静态联结
在 Linux 中,执行档我们可以编程成静态联结以及动态连结,以下我们举一个简短的程序作为例子:
#include int main() { printf(" test"); } |
若我们执行 :
[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 放到系统内定会去搜寻的目录中的其中一个,例如/usr/lib,然後再追踪一次系统搜寻函式库的过程,系统还是会依循
/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
的顺序来寻找 libreadline.so.3 这个动态函式库,不过,在搜寻到最後一个目录後,终於找到了 libreadline.so.3,也使得笔者用来测试的这苹用到动态函式库 libreadline.so.3 的执行档可以顺利的执行。
其 实,这种逐一目录寻找的方式很缺乏效率,因此 Linux 提供了一个动态函式库快取的机制,它所存在的档案位置为 /etc/ld.so.cache,举我们之前的例子来说,在ld.so.conf 里面纪录了系统搜寻动态函式库时所会依序去寻找的路径,如果把我们所要加入的动态函式库档案所存在的路径加入此处,或是以下路径的其中之一,这样我们执行 程序时,便可以缩短函式库搜寻所花的时间
/lib/
/usr/lib/
其实笔者 原本是把 libreadline.so.3 放到路径 /usr/lib/mmx,可是我发现在执行 ldconfig 时,它预设并不会主动到 /usr/lib/mmx目录中去取得其中动态函式库档案的资讯,每当我在执行有用到 libreadline.so.3的程序时,它仍然无法透过动态函式库快取取得 libreadline.so.3的路径资讯,而是用一个一个目录尝试开启的方法,直到在 /usr/lib/mmx目录中找到了 libreadline.so.3,因此笔者比较建议如果要新增动态函式库到Linux 中最好是直接新增到 /lib 或是 /usr/lib 目录下,不然就是把函式库所在的目录放到ld.so.conf 里面,再透过 ldconfig 建立动态函式库的快取资料档,这样 Linux 在执行时会更加的便利。
最後,笔者自己新增一个函式库的目录,把 libreadline.so.3 放到 /root/lib 中,并且修改 /etc/ld.so.conf 档案的内容如下
/usr/X11R6/lib
/usr/i486-linux-libc5/lib
/root/lib
接著笔者把动态函式库档案 libreadline.so.3 移到 /root/lib 目录下,执行ldconfig ˉD,读者们可以看到它会依序到以下目录去建立动态函式库的快取
/usr/X11R6/lib
/usr/i486-linux-libc5/lib
/root/lib
/usr/lib
/lib
当 我们再次执行有用到 libreadline.so.3 的执行档时,它便会直接去 /root/lib开启 libreadline.so.3,而不会再一个个目录的搜寻了,最後,读者请注意 libreadline.so.3 必须是一个 link,在笔者的电脑中是 link 到 libreadline.so.3.0,所以请执行
ln -s libreadline.so.3.0 libreadline.so.3
後再执行 ldconfig,不然会产生以下的错误讯息
ldconfig: warning: /root/lib/libreadline.so.3 is not a symlink
程序启动的流程
在 linux 的环境中最常见的可执行档的种类包括了 Script 档、Aout 格式的执行档、ELF 格式的执行档。在本文的这个部分,我会针对 Linux 系统是如何来辨别这些不同的可执行档,以及整体的执行流程来作一个说明。
我在此大略说明一下程序启动的流程,当我们在 shell 中输入指令时,会先去系统的路径中来寻找是否有该可执行档存在,如果找不到的话,就会显示出找不到该可执行档的讯息。如果找到的话,就会去呼叫 execve()来执行该档案,接下来 execve() 会呼叫 System Call sys_execv(),这是在Linux 中 User Mode 透过 80 号中断(int 80 ah=11)进入 Kernel Mode 所执行的第一个指令,之後在 Kernel 中陆续执行 do_exec()、 prepare_binprm()、read_exec()、search_binary_handler(),而在 search_binary_handler() 函式中,会逐一的去检查目前所执行档案的型态(看看是否为Script File、aout 或 ELF 档),不过 Linux 所采用的方式是透过各个档案格式的处理程序来决定目前的执行档所属的处理程序。
如下图,会先去检验档案是否为 Script 档,若是直进入 Script 档的处理程序。若不是,则再进入 Aout 档案格式的处理程序,若该执行档为 Aout 的档案格式便交由 Aout档案格式的处理程序来执行。如果仍然不是的话,便再进入 ELF 档案格式的处理程序,如果都找不到的话,则传回错误讯息。
由这种执行的流程来看的话,如果 Linux Kernel 想要加入其他的执行档格式的话,就要在 search_binary_handler() 加入新的执行档的处理程序,这样一旦新的执行档格式产生後,在 Linux 下要执行时,因为在do_load_script、do_load_aout_binary、do_load_elf_binary都会传回错误,因此只 有我们自己的 do_load_xxxx_binary 函式可以正确的接手整个执行档的处理流程,因此便可以达成新的档案格式置入的动作哩。
在函式 do_load_elf_binary () 执行时,首先会去检视目前的档案是否为 ELF 格式,如下程序码
if (elf_ex.e_ident[0] != 0x 7f ' ' strncmp(&elf_ex.e_ident[1], "ELF", 3) != 0) goto out; |
便是去检查该档的前四个 bytes 是否为 0x 7f 加上 “ELF” (0x 45 0x 4c 0x46),若非,则结束 do_load_elf_binary 的执行。之後,便是去检视我们之前提过的 e_type 属性,来得知是否为 ET_EXEC(Executable File) 或是ET_DYN(Shared Object File) 这两个值的其中之一
if (elf_ex.e_type != ET_EXEC && elf_ex.e_type != ET_DYN) goto out; |
如果都不是这两个值之一,便结束 do_load_elf_binary 的执行之後便是一连串读取 ELF 档表格的动作,在此就不多说,有兴趣的读者可以自行参阅/usr/src/linux/fs/binfmt_elf.c 的内容即可。
在此我们检视一个执行档由启动到结束的完整流程,首先这个执行档具有如下的程序码
#include int main() { printf(" test "); } |
然後,透过如下的编程过程
gcc test.c ˉo test |
我们如果检视执行档的 ELF Header 可以得知它主要呼叫了 /lib/libc.so.6函式库中以下的函式
printf __deregister_frame_info __libc_start_main __register_frame_info |
接下来,我们便把程序的执行流程大略整理如下,而 execve("./test", ["./test"], []) 执行的流程,就是刚刚我们所提到的内容,若不熟悉的读者,可以再回头看看刚刚的内容,即可对 execve("./test", ["./test"], []) 的执行流程有大略的了解。在这里,我们会把整个执行流程更完整的来检视一遍。
首先,我们所在的执行环境会透过 execve("./test", ["./test"], []) 的函式呼叫来启动 test 执行档。
呼叫 open("/etc/ld.so.cache", O_RDONLY),以唯读模式开启 ld.so.cache,这个档案的功能是作为动态函式库的快取,它会记录了目前系统中所存在的动态函式库的资讯以及这些函式库所存在的位置。所以说,如 果我们在系统中安装了新的函式库时,我们便需要去更新这个档案的内容,以使新的函式库可以在我们的 Linux 环境中发生作用,我们可以透过 ldconfig 这个指令来更新 ld.so.cache 的内容。
呼叫 mmap(0, 9937, PROT_READ, MAP_PRIVATE, 3, 0),把 ld.so.cache 档案映射到记忆体中,mmap 函式的宣告为 mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset),在笔者的电脑上 ld.so.cache 的档案大小为 9937 bytes,PROT_READ代表这块记忆体位置是可读取的,MAP_PRIVATE 则表示产生一个行程私有的 copy-on-write 映射,因此这个呼叫会把整个 ld.so.cache 档案映射到记忆体中,在笔者电脑上所传回的映射记忆体起始位置为 0x40013000。
注: mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)代表我们要求在档案 fd中,起始位置为offset去映射 length 长度的资料,到记忆体位置 start ,而 prot 是用来描述该记忆体位置的保护权限(例如:读、写、执行),flags用来定义所映射物件的型态,例如这块记忆体是否允许多个 Process 同时映射到,也就是说一旦有一个 Process 更改了这个记忆体空间,那所有映射到这块记忆体的Process 都会受到影响,或是 flag 设定为 Process 私有的记忆体映射,这样就会透过 copy-on-write 的机制,当这块记忆体被别的 Process 修改後,会自动配置实体的记忆体位置,让其他的 Process 所映射到的记忆体内容与原本的相同。(有关mmap的其它应用,可参考本文最後的注一)
呼叫 open("/lib/libc.so.6", O_RDONLY),开启 libc.so.6。
呼叫 read(3, "177ELF111331250202"..., 4096) 读取libc.so.6的档头。
呼叫 mmap(0, 993500, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0),把 libc.so.6 映射到记忆体中,由档头开始映射 993500 bytes,若是使用 RedHat 6.1(或其它版本的 RedHat)的读者或许会好奇 libc.so.6 所 link 到的档案 libc- 2.1.2 .so 大小不是 4118715 bytes 吗? 其实原本 RedHat 所附的 libc.so.6 动态函式库是没有经过 strip 过的,如果经过 strip 後,大小会变为 1052428 bytes,而 libc.so.6 由档头开始在 993500 bytes 之後都是一些版本的资讯,笔者猜想应该是这样的原因,所以在映射档时,并没有把整个 libc.so.6 档案映射到记忆体中,只映射前面有意义的部分。与映射 ld.so.cache 不同的是,除了 PROT_READ 属性之外,libc.so.6 的属性还多了PROT_EXEC,这代表了所映射的这块记忆体是可读可执行的。在笔者的电脑中,libc.so.6 所映射到的记忆体起始位置为 0x40016000。
呼叫 mprotect(0x40101000, 30940, PROT_NONE),用来设定记忆体的使用权限,而 PROT_NONE 属性是代表这块记忆体区间(0x40101000—0x401088DC)是不能读取、写入与执行的。
呼叫 mmap(0x40101000, 16384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0xea000),映射 libc.so.6 由起始位置 0xea000 映射 16384bytes 到记忆体位置 0x40101000。
呼叫 mmap(0x40105000, 14556, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0),MAP_ANONYMOUS 表示没有档案被映射,且产生一个初始值全为 0 的记忆体区块。
呼叫 munmap(0x40013000, 9937),把原本映射到 ld.so.cache 的记忆体解除映射(此时已把执行档所需的动态函式库都映射到记忆体中了)。
呼叫 personality(0),可以设定目前 Process 的执行区间(execution domain),换个说法就是 Linux 支援了多个执行区间,而我们所设定的执行区间会告诉 Linux 如何去映射我们的讯息号码(signal numbers)到各个不同的讯息动作(signal actions)中。这执行区间的功能,允许 Linux 对其它 Unix-Like 的操作系统,提供有限度的二进位档支援。如这个例子中,personality(0) 的参数为 0,就是指定为 PER_LINUX 的执行区间(execution domain)。
#define PER_MASK (0x00ff) #define PER_LINUX (0x0000) #define PER_LINUX_32BIT (0x0000 | ADDR_LIMIT_32BIT) #define PER_SVR4 (0x0001 | STICKY_TIMEOUTS) #define PER_SVR3 (0x0002 | STICKY_TIMEOUTS) #define PER_SCOSVR3 (0x0003 | STICKY_TIMEOUTS | WHOLE_SECONDS) #define PER_WYSEV386 (0x0004 | STICKY_TIMEOUTS) #define PER_ISCR4 (0x0005 | STICKY_TIMEOUTS) #define PER_BSD (0x0006) #define PER_XENIX (0x0007 | STICKY_TIMEOUTS) #define PER_LINUX32 (0x0008) #define PER_IRIX32 (0x0009 | STICKY_TIMEOUTS) /* IRIX5 32-bit */ #define PER_IRIXN32 (0x 000a | STICKY_TIMEOUTS) /* IRIX6 new 32-bit */ #define PER_IRIX64 (0x000b | STICKY_TIMEOUTS) /* IRIX6 64-bit */ |
呼叫 getpid(),取得目前 Process 的 Process ID。
呼叫 mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0),传回值为 0x400130,MAP_ANONYMOUS 表示没有档案被映射,且产生一个初始值全为 0 的记忆体区块。
呼叫 write(1, " test ", 6),显示字串在画面上。
呼叫 munmap(0x40013000, 4096),解除记忆体位置0x40013000的记忆体映射。
呼叫 _exit(6),结束程序执行。
在这段所举的例子,只用到了一个函式库 libc.so.6,我们可以举像是 RedHat 中 Telnet 指令为例,首先检视他的 ELF Header
==>libncurses.so.4 tgetent ==>libc.so.6 strcpy ioctl printf cfgetospeed recv connect ............┅ sigsetmask __register_frame_info close free |
它主要呼叫了函式库 libncurses.so.4 的函式 tgetent,以及函式库 libc.so.6 中为数不少的函式,当然我们也可以去检视它执行的流程,与之前只呼叫了 libc.so.6 的printf 函式来比较,我们可以发现它主要的不同就是去载入了 libncurses.so.4
open("/usr/lib/libncurses.so.4", O_RDONLY) ; fstat(3, {st_mode=S_IFREG|0755, st_size=274985, ...}) ; read(3, "177ELF111331340335"..., 4096) ; mmap(0, 254540, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0); mprotect(0x40048000, 49740, PROT_NONE); mmap(0x40048000, 36864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x31000); mmap(0x40051000, 12876, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) ; close(3); |
结束语
最後,我想各位读者应该对於 Linux 上的动态函式库的架构有了进一步的了解,笔者根据自己电脑 Linux 的记忆体配置画了下面的架构图,相信会让有心了解整个运作的人,有了更清楚的一个印象。
在这张图中,我们所执行的程序是由记忆体 0x08048000 开始载入的,而所用到的动态函式库则是在记忆体位置 0x40000000 开始载入,以笔者的电脑为例,动态函式库载入的记忆体映射情况大略为
40000000-40001000 /usr/share/locale/en_US/LC_MESSAGES/SYS_LC_MESSAGES 40001000-40002000 /usr/share/locale/en_US/LC_MONETARY 40002000-40003000 /usr/share/locale/en_US/LC_TIME 40003000-4000b000 /lib/libnss_files- 2.1.2 .so 4000b000 -4000c 000 /lib/libnss_files- 2.1.2 .so 4000c 000 -400f 7000 /lib/libc- 2.1.2 .so 400f 7000-400fb000 /lib/libc- 2.1.2 .so 400fb000-400ff000 0 400ff000-40111000 /lib/ld- 2.1.2 .so 40111000-40112000 /lib/ld- 2.1.2 .so 40112000-4011b000 /lib/libnss_nisplus- 2.1.2 .so ......┅(more) |
若我们程序透过 malloc 配置动态的记忆体,则会配置在标示为 “Free Space”的记忆体空间中,程序所用到的堆叠(Stack) 是由 0xbfffffff 开始,往下延伸。
而在记忆体位置 0xc0000000 以上,则是属於 Kernel Mode 的部分,这部份包含了Linux Kernel 的 Image 以及我们之後所动态载入的模组。
文章到此正式结束了,读者若有任何的问题或是这篇文章有任何疏漏的部份,欢迎各位可以来信指教,谢谢各位。。。^_^My E-Mail: hlchou@mail2000.com.tw
注一:(http://www.cuspy.com/~mcculley/mapself/)笔者在写这篇文章时,在一个网页上看到一个很有意 思的记忆体区块拷贝效率比较,我们知道在Linux下面如果要把记忆体区块由 A 拷贝到 B,我们除了可以使用memcpy来完成以外,还可以透过mmap来开启档案/proc/self/mem,来完成拷贝记忆体区块的目的。举个例子来说, 如果我们要把记忆体区块由A拷贝到B共 chunksize 个bytes,可以透过如下的写法
memcpy(B, A, chunksize); |
透过 mmap 来做的话,可以藉由以下的写法
int self; self = open("/proc/self/mem", O_RDONLY); B = mmap(B, chunksize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED, self, (off_t)A); |
也就是透过 Linux 提供给每个 Process 的记忆体装置档案 mem,来完成记忆体的拷贝动作。
不过,虽然我们可以有这两种方法可以选择,可是遇到要拷贝记忆体时,却不免会遇到要选择何种方式来实做的问题,因此该网页的作者写了一个小程序 来测试这两种方式的优缺点,首先在 Linux 上每个记忆体的 Page 大小为 512bytes,因此测试时就是利用 512 bytes为单位来逐渐增加测试的记忆体区块大小。每个阶段都有一个固定的记忆体区块大小,与两个内容不同的记忆体区块作为拷贝时的来源端,每一个循环都 会先拷贝一个来源端到目的的记忆体区块中,再比较内容,若相同,则拷贝另一个来源端的资料到目的的记忆体区块中,再比较内容,如此重复10000 次(表示共拷贝了 20000 次到目的记忆体区块中),藉此来比较 memcpy 与 mmap 在执行记忆体区块拷贝时的效率。
如下表(笔者电脑配备: PII 350,64MB RAM)
memcpy mmap 512 0.14 0.23 1024 0.26 0.35 2048 0.51 0.59 4096 1.00 1.06 8192 2.56 2.10 16384 5.67 4.55 32768 11.71 8.96 65536 23.63 17.75 |
我们不难发现当记忆体区块为 512、1024、2048、4096 时,memcpy 都胜过mmap。不过当拷贝的记忆体区块越来越大时,mmap 明显表现的相当有效率,像最後测试的记忆体区块大小为 65536 bytes,mmap 相较於 memcpy所花的时间少了约 6 秒钟。
由此我们可以了解到,如果在 Linux 上我们所撰写的系统需要使用较大的记忆体区块拷贝时,透过 mmap 来作或许是一个不错的选择。