转自:http://www.ibm.com/developerworks/cn/linux/sdk/shobj/index.html
别紧张,共享对象与面向对象技术无关!我们现在讨论的是 Linux 平台上的动态链接库(类似于 Windows 中的 DLL)。在编码过程中的不同时期,我们都曾经使用过一些类型的库,以便调用其中的简单函数(如 C 语言的 printf()
)或复杂函数(如 C++ 通用函数库中的 sort()
)。库使每天的编程变得容易并使开发人员关注于手头的任务。想象一下,如果您所编写的每一段代码都必须编写 printf() 和文件 I/O 函数,那是多么繁琐的事!
几乎任何一个软件的构建都非常依赖于允许软件去完成不同任务的库 -- 从打印到屏幕到登录到网络。某些库由系统提供,某些库由第三方供应商或用户自己编写。在编译过程中,这些库被链接至应用程序。如果应用程序使用了很多库,而且已链接所有代码,那么应用程序会变得大得惊人。这就是要使用“共享库”的原因,它是不链接至应用程序的源码,而是随应用程序的需要动态地加装。
在开始构建共享对象之前,先要了解编译的过程和共享对象是什么。如下所示,我们采用著名的且有历史意义的 hello world 代码示例。
#include "stdio.h" void main() { printf("Hello World!"); } |
使用下列命令行编译此程序:
$ gcc -o hello hello.c |
该命令创建了一个名为 hello 的可执行文件。现在编译器可以执行下列步骤:
- 句法检查:检查该文件的句法和语法。
- 编译:编译该文件,生成该代码的目标文件。在生成的目标文件中标记出了未解析的函数名,如
printf()
。(请参阅下面的 文件格式。) - 链接:调用称作链接程序的独立程序。(在 UNIX 中,该程序是
ld
。)该链接程序尝试通过在不同的库中搜索该代码来解析函数和变量。例如,printf()
代码驻留在文件libc.a
(或libc.so
)中。如果需要的不是标准库,则必须专门指定。
以上用于编译和链接代码的命令可以分解成下面两个命令:
>$ gcc -c hello.c $ ld -lc -o hello hello.c |
这是编译步骤(由 -c
选项指定)。第二步是链接,使用 ld
程序生成可执行 hello 程序。
这种链接称为 静态链接。使用静态编译时,库中的代码与应用程序合并在一起(从而使可执行文件变得很庞大)。任何有价值的应用程序使用提供的数百个函数来作为库。这些库中,有些是标准的,有些是第三方的,还有一些是内部的。使用静态编译时,最终的可执行文件变得非常庞大,而且所有的代码在运行时都必须载入内存,不管是否使用函数,其代码都放入内存。
如果有一种机制:库可以按需要动态地载入内存,这可以减少程序在内存中占用的空间,也可以把程序分割成几个小部分,这就太好了。这也易于分发、安装和更新。这种机制是存在的,这些动态链接库是这样命名的(在 Windows 中为 DLL,在 Linux 中为共享对象)。使用它们的应用程序作为动态可执行文件。
在开始探讨共享对象之前,简要地看一下库的命名规则。静态库一般由字母 lib开头,并有 .a 的扩展名。共享对象有两个不同的名称: soname和 real name。 soname 包含前缀 "lib",然后紧跟库名,其次是 ".so"(后面紧跟另一个圆点),以及表明主版本号的数字。soname 可以由前缀的路径信息来限定。real name 是包含库的已编译代码的真正文件名。real name 在 soname 后添加一个圆点、小的数字、另外一个圆点和发布号。(发布号和其相应的圆点是可选的。)
我们会看到由 Program-Library How-To 定义的名称,称作 linker name ,它指的是不带版本号的 soname。客户所使用的库名是指 linker name。通常,这是与 linket name 的链接。而 soname 是到 real name 的链接。
这里拿 soname /usr/lib/libhello.so.1
来作为示例。这是一个全限定 soname,它链接到 /usr/lib/libhello.so.1.5
。其相应的 linker name 是 /usr/lib/libhello.so
。这里似乎要管理许多名称,但有工具可以帮助您管理他们。(请参阅本文后面将要讲述的 实用程序和工具 中的 ldconfig
。)
现在我们来看实质性内容,并编写一个样本共享对象。库的 soname 是 libprint.so.1
,real name 是 libprint.so.1.0
。库有一个 printstring(char*)
的函数,它将打印字 "String: ",后面紧跟着以参数形式传递给它的任何字符串。
对于这个要用的库,有两个必须要编写的基本文件。第一个是头文件,它声明库导出的所有函数,而且代码中的客户机包括这些函数。第二个是要编译的和作为共享对象放入的函数的定义。对于我们这个示例,头文件类似于:
/* file libprint.h - for example use! */ void printstring(char* str); |
库的代码很基本,在下一个清单中显示。
/* file libprint.c */ #include "stdio.h" void printstring(char* str) printf("String: %s/n", str); } |
这里有两个特殊的函数, _init(void)
和 _fini(void)
。当载入库时,动态的加载器会自动调用这两个函数。通常已提供了这两个函数的缺省实现,但也可以忽略它,编写您自己的函数。我们把这两个函数添加到 libprint.c 代码中,以便在调用它们时,打印出诊断信息。
void _init() { printf("Inside _init()/n"); } void _fini() { printf("Inside _fini()/n"); } |
现在我们把它与清单 3 中的代码合并。很简单,不是吗?要编写一个定制的库,需要使用 libprint.c
和 libprint.h
的模板,然后编写相应的函数。现在让我们继续编写这个库。
下面一串命令是编译这个库:
$ gcc -fPIC -c libprint.c $ ld -shared -soname libprint.so.1 -o libprint.so.1.0 -lc libprint.o |
请注意,gcc 命令行中的 -fPIC
选项。这是生成 Position-Independent Code 所必须要的。把这个命令翻译出来就是:生成可以在进程的进程空间的任何地方载入的代码。这对于共享对象是非常重要的。使用这个选项,使得必须执行重定位的数量降低到最少。一旦载入可执行程序使用的共享对象,就必须给它分配一些空间。必须给文本和数据部分配一些位置。如果它们不是以“位置独立”方式来构建,那么载入共享对象时,程序要做大量的重定位,这会影响到性能。
现在我们分析一下传给 ld
的选项。 -shared
选项表明输出的文件被认为是共享的库。通过 -soname name
选项,可以指定 soname 是什么。 -o name
指定了共享对象的 real name。指定 soname 和 real name 是很重要的,因为在安装库时要用到它们。
既然已经构建了库,现在就安装它,并让一个小客户程序使用它。为了安装共享库,要使用 ldconfig
这个特殊程序。通常,共享库安装在 /usr/lib
、 lib
或 /usr/local/lib
目录下。一旦建立了库,它将被复制到这些目录中的其中之一。然后,运行 ldconfig
程序。
$ldconfig -v -n . ...: libprint.so.1 => ./libprint.so.1.0 |
现在已经创建了一个到 libprint.so.1.0
名为 libprint.so.1
的符号链接。安装中的下一步是创建链接名称的另一个链接,如下:
$ ln -sf libprint.so libprint.so.1 |
为了在 /usr/lib
、 lib
或 /usr/local/lib
目录中复制文件,需要超级用户权限。当应用程序运行时,会自动搜寻这些目录以解析库。如果没有超级用户权限,那么共享库可以安装在任何目录,但在运行使用这个库的执行程序之前,要设置环境变量。这个环境变量 LD_LIBRARY_PATH
必须要设置指向共享库所在位置的路径。作为一个示例,如果共享库与可执行程序在同一个目录,可以象这样:
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH |
现在完成了编译所需要的设置以及执行客户所用的刚构建的共享库。使用共享库很简单。:)
清单 5:Client.c;使用库中 printstring() 函数的样本客户
#include "libstring.h" void main() } printf("In Main!/n"); printstring("In Main!"); } |
用以下命令把该程序编译成可执行程序:
$ gcc -o client client.c -L. -lprint |
这生成一个称之为 client 的可执行程序,这其中假定库与代码在同一个目录(由 -L. -lprint
决定)。一旦执行,产生如下的输出:
$ client Inside _init() In Main! String: In Main! Inside _fini() $ |
现在我们已经创建出一个共享库,那就开始安装它,使用它!让我们来研究其内部机制,观察当执行 client 时所发生的情况。当启动该程序时,系统识别出该程序要依靠动态库。所以,系统调用加载器, /lib/ld-linux.so.X
(X 是版本号),来载入所需库。可以用 ldd
来决定可执行文件所依赖的库。
$ ldd client libprint.so.1 => ./libprint.so.1 libc.so.6 => /lib/libc.so.6 /lib/ld-linux.so.2=> /lib/ld-linux.so.2 |
这显示出文件 client 所依赖的库。然后加载器载入这些库,并调用相应的 _init()
部分。加载器首先在 LD_LIBRARY_PATH
环境变量所记录的路径中查找库,然后再 /etc/ld.so.conf
记录的标准路径中查找。如果找不到库,那么会报错。在正常情况下,加载器载入程序所需的库和执行文件。所有这些都是透明的且在屏幕后面进行的。
我们简要地看一下 file、nm 和 objdump 工具,这三个非常有用的二进制实用程序。
file 程序可以用来查找文件类型。被检测的文件可以是文本文件(例如,libprint.c)、可执行文件(例如,client)或数据(例如,/dev/hda5)。在查看某个特定文件编译所运行的平台、它是否是可执行的以及其它一些东西方面,file 是很有用的。用下列命令:
$ file client client: ELF 32-bit LSB executable, Intel 80386, version 1, dynamically linked (uses shared libs), not stripped |
nm 列出对象中的所有符号。(对于对象,我们一般指的是对象文件或库。)通过 nm 传递一个对象显示所使用的或由该对象导出的函数名、对象的各个部分、符号和对象类型。符号可以是未定义的或者外部的;也可以是全局的或一些其它标识符。在 libprint.so
上运行 nm
,产生下列输出:
$ nm libprint.so 00001490 A _DYNAMIC 00001480 A _GLOBAL_OFFSET_TABLE 00001510 A __bss_start 00001510 A _edata 00001510 A _end 00000452 A _etext 00000400 T _fini 000003d8 T _init 000003d8 t gcc2_compiled U printf@@GLIBC_2.0 00000428 T printstring |
Objdump 显示有关对象文件的信息,这些可以在命令中指定。传递给 objdump 的选项控制显示什么样的信息。如果想要仔细查看对象文件内部的详细信息,这些是很好的实用程序。
好,我希望您有兴趣了解共享对象。别走开,还有好多呢!