编写并使用程序库
差不多可以认为,每个程序都链接到一个或几个库上。任何一个使用了C函数(诸如 printf 等)都须链接到C运行时库。如果您的程序具有图形界面(GUI),它将被链接到窗口系统的库。如果您的程序使用了数据库,数据库供应商会提供给您一些简化访问数据库的库。
在这些情况中,您必须作出选择:静态(statically)还是动态(dynamically)地将程序链接到库上。如果您选择了静态链接,程序体积可能会更大,程序也会比较难以升级,但是可能相对而言比较易于部署。如果您选择动态链接,则程序体积会比较小、易于升级,但是部署的难度将会有所提高。本节中我们将介绍静态和动态两种链接方法,仔细比较它们的优劣,并提出一些在两者之间选择的简单的规则。
存档文件
存档文件(archive),也被称为静态库(static library),是一个存储了多个对象文件(object file)的单一文件。(与Windows 系统的 .LIB文件基本相当。)编译器得到一个存档文件后,会在这个存档文件中寻找需要的对象文件,将其提取出来,然后与链接一个单独的对象文件一样地将其链接到您的程序中。
您可以使用ar命令创建存档文件3。传统上,存档文件使用 .a 后缀名,以便与 .o 的对象文件区分开。下面的命令可以将 test1.o 和 test2.o 合并成一个存档文件 libtest.a :
% ar cr libtest.a test1.o test2.o
上面命令中的 cr 选项通知 ar 创建这个存档文件3。现在您可以通过为 gcc 或 g++ 指定 -ltest 参数将程序链接到这个库。这个过程在第一章“起步”1.2.2节“链接对象文件”中进行了说明。 当链接器在命令行参数中获取到一个存档文件时,它将在其中搜索所有之前已经被引用而没有被定义的符号(函数或变量)的定义。定义了这些符号的对象文件将从存档中被提取出来,链接到新程序执行文件中。因为链接器会在读取命令行参数的过程中一遇见存档文件就进行解析,通常将存档文件放在命令行参数的最后最有意义。例如,假设 test.c 包含了代码2.7中的代码而 app.c 包含了代码2.8中的代码。
代码2.7 (test.c)库内容
int f () { return 3; }
代码2.8 (app.c)一个使用库函数的程序
int main () { return f (); }
现在假设我们将test.o与其它一些对象文件合并生成了libtest.a存档文件。下面的命令行不会正常工作:
% gcc –o app –L. –ltest app.o app.o: In function ‘main’: app.o(.text+0x4): undefined reference to ‘f’ collect2: ld returned 1 exit status
错误信息指出虽然libtest.a中包含了一个 f 的定义,链接器并没有找到它。这是因为 libtest.a 在第一次出现在命令行的时候就被搜索了,而这个时候链接器并没有发现对 f 的任何引用。而如果我们稍微更改一下命令行,则不会再有错误消息出现:
% gcc –o app app.o –L. –ltest
这是因为 app.o 中对 f 的引用导致连接器将 libtest.a 中的 test.o 包含在生成的执行文件中。
共享库
共享库(shared library,也被称为共享对象或动态链接库)在某种程度上与由一组对象文件生成的打包文件相当类似。不过,两者之间的区别也是非常明显的。最本质的区别在于,当一个共享库被链接到程序中的时候,程序本身并不会包含共享库中出现的代码。程序仅包含一个对共享库的引用。当系统中有多个程序链接到同一个共享库的时候,它们都将引用这个共享库而不是将代码直接包含在自身程序中——正因为如此,我们说这个库被所有这些程序“共享”。
第二个重要的区别在于,共享库不仅仅是对象文件的简单组合。当使用的时候,链接器会从中寻找需要的部分进行链接,以匹配未定义的符号引用。而当生成共享库的时候,所有对象文件被合成为一个单独的对象文件,从而使链接到这个库的程序总能包含库中的全部代码,而不仅仅是所需要的部分。
要创建一个共享库,您必须在编译那些用于生成共享库的对象时为编译器指定 -fPIC 选项。
% gcc –c –fPIC test1.c
这里的 -fPIC 选项会通知编译器您要将得到的 test1.o 作为共享库的一部分。
位置无关代码(Position-Independent Code) 共享库中的函数在不同程序中可能被加载在不同的地址,因此共享库中的代码不能依赖特定的加载地址(或位置)。作为程序员,这并不需要您自己操心;您只需要在编译这些用于共享库的对象文件的时候,在编译器参数中指明 –fPIC。
然后您将得到的对象文件合并成一个共享库:
% gcc –shared –fPIC –o libtest.so test1.o test2.o
这里 -shared 选项通知链接器生成共享库,而不是生成普通的可执行文件。共享库文件通常使用 .so 作为后缀名,这里 so 表示共享对象(shared object)。与静态库文件相同,文件名以 lib 开头,表示这是一个程序库文件。
将程序链接到共享库与链接到静态库的方法并无二致。例如,当 libtest.so 位于当前目录或者某个系统默认搜索目录时,下面这条命令可以将程序与它进行链接:
% gcc –o app app-o –L. –ltest
假设系统中同时有 libtest.a 和 libtest.so。这时链接器必须从两者中选择一个进行链接。链接器会依次搜索每个文件夹(首先搜索 -L选项指定的路径,然后是系统默认搜索路径)。不论链接器发现了哪一个,它都会停止搜索过程。如果当时只找到了两者中的一个,链接器会选择找到的那个进行链接。如果两个版本同时存在,除非您明确指定链接静态版本,链接器会选择共享库版本进行链接。对链接器指定 -static 选项表示您希望使用静态版本。例如,当使用下面的命令进行链接的时候,即使 libtest.so 同时存在,链接器仍将选择 libtest.a 进行链接:
% gcc -static -o app app.o -L. -ltest
可以用 ldd 命令显示与一个程序建立了动态链接的库的列表。当程序运行的时候,这些库必须存在系统中。注意 ldd 命令会输出一个特殊的、叫做 ld-linux.so 的库。它是 GNU/Linux 系统动态链接机制的组成部分。
使用 LD_LIBRARY_PATH
当您将一个程序与共享库进行动态链接的时候,链接器并不会将共享库的完整路径加入得到的执行文件中,而是只记录共享库的名字。当程序实际运行的时候,系统会搜索并加载这个共享库。默认情况下,系统只搜索 /lib 和 /usr/lib。如果某个链接到程序中的共享库被安装在这些目录之外的地方,系统将无法找到这个共享库,并因此拒绝执行您的程序。 一种解决方法是在链接的时候指明 -Wl,-rpath 参数。假设您用下面的命令进行链接:
% gcc -o app app.o -L. -ltest -Wl,-rpath,/usr/local/lib
当运行app的时候,系统会在 /usr/local/lib 中寻找所需的库文件。 另外一个解决方案是在运行程序的时候设置 LD_LIBRARY_PATH 环境变量。与 PATH 变量类似,LD_LIBRARY_PATH 包含的是一组以冒号分割的目录列表。例如,假设我们将 LD_LIBRARY_PATH 设为 /usr/local/lib:/opt/lib,则系统会在搜索默认路径 /lib 和 /usr/lib 之前搜索 /usr/local/lib 和 /opt/lib 目录。需要注意的是,如果在编译程序的时候设定了 LD_LIBRARY_PATH 环境变量,链接器会在搜索 -L参数指定的路径之前搜索这个环境变
位置无关代码(Position-Independent Code)共享库中的函数在不同程序中可能被加载在不同的地址,因此共享库中的代码不能依赖特定的加载地址(或位置)。作为程序员,这并不需要您自己操心;您只需要在编译这些用于共享库的对象文件的时候,在编译器参数中指明 -fPIC变量中指定的路径以寻找库文件4。
标准库
即使您在程序链接阶段并没有指明任何库,几乎可以确信程序总会链接到某些共享库中。这是因为GCC会自动将程序链接到标准 C 库 libc。标准C库的数学函数并未被包含在 libc 中;它们位于 libm 中,而这个库要求您明确指定才会链接到程序中。例如,要编译一个使用了诸如 sin 和 cos 之类的三角函数的程序 compute.c,您需要执行这个命令:
% gcc -o compute compute.c -lm
如果您写的是一个 C++ 程序,并用 c++ 或 g++ 命令完成链接过程,您还将自动获得对标准 C++ 库 libstdc++ z的链接。
库依赖性
经常出现这样的情况:一个库依赖另一个库。例如,许多 GNU/Linux 系统提供了 libtiff,一个包含了读写 TIFF 格式图片的函数的库。这个库依次依赖 libjpeg(JPEG图像函数库)和 libz(压缩函数库)。代码2.9展示了一个非常简单的程序。它通过libtiff打开一个TIFF格式的图片。
代码2.9 (tifftest.c)使用 libtiff
#include <stdio.h> #include <tiffio.h> int main ( int argc, char** argv) { TIFF* tiff; tiff = TIFFOpen (argv[1], “r”); TIFFClose (tiff); return 0; }
将这份源码保存为tifftest.c。要在编译时将这个程序链接到libtiff则应在链接程序命令行中指定 -ltiff:
% gcc -o tifftest tifftest.c -ltiff
默认情况下,链接器会选择共享库版本的libtiff。它通常位于 /usr/lib/libtiff.so。因为libtiff会引用libjpeg和libz,这两个库的共享库版本也会被引入(共享库可以指向自己依赖的其它共享库)。可以用ldd命令验证这一点:
% ldd tifftest libtiff.so.3 => /usr/lib/libtiff.so.3 (0x4001d000) libc.so.6 => /lib/libc.so.6 (0x40060000) libjpeg.so.62 => /usr/lib/libjpeg.so.62 (0x40155000) libz.so.1 => /usr/lib/libz.so.1 (0x40174000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
而另一方面,静态库无法指向其它的库。如果您决定通过指定 -static参数,将程序与静态版本的libtiff链接时,您会得到这些关于“无法解析的符号”的错误信息:
% gcc -static -o tifftest tifftest.c -ltiff /usr/bin/../lib/libtiff.a(tif_jpeg.o): In function ‘TIFFjpeg_error_exit’: tif_jpeg.o(.text+0x2a): undefined reference to ‘jpeg_abort’ /usr/bin/../lib/libtiff.a(tif_jpeg.o): In function ‘TIFFjpeg_create_compress’: tif_jpeg.o(.text+0x8d): undefined reference to ‘jpeg_std_error’ tif_jpeg.o(.text+0xcf): undefined reference to ‘jpeg_CreateCompress’ ...
要想将这个程序静态链接,您必须手工指定另外两个库:
% gcc -static -o tifftest tifftest.c -ltiff -ljpeg -lz -lm
有时候,两个库可能互相依赖。也就是说,第一个库可能引用了第二个库中定义的符号,反之亦然。通常这种情况都是由于不良设计导致的;但是这种情况确实可能出现。在这种情况下,您可以在命令行中多次指定同一个库。链接器会在每次读取到这个库的时候重新查找库中的符号。例如,下面的命令会导致libfoo.a被多次扫描:
% gcc -o app app.o -lfoo -lbar -lfoo
因此,即使libfooo.a引用了libbar.a中定义的符号,且反之亦然,程序仍将被成功链接。
优点与缺陷
当您了解了两种类型的库的时候,您可能开始考虑实际使用哪一种。这里有一些您在选择时必须记住的注意点。
动态库的重要优点之一在于,为安装程序的系统节省了空间。假设您安装了10个程序,而它们同时会利用同一个库,则使用共享库较之使用静态库将为系统节省大量的空间。如果您选用静态库,则您将会在系统中随这十个程序保存十份静态库的副本。因此,使用共享库可以节省磁盘空间。而且如果您的程序是从网络上下载的,使用共享库可以同时节省下载时间。
共享库与此相关的一个优势在于,程序员可以选择升级这个库而不必强令用户同时升级所有依赖这个库的程序。例如,假设您写了一个用于处理HTTP连接的库。可能有许多程序依赖这个库。如果您在库的代码中发现了bug,您可以选择升级您的库。与此同时,所有使用这个库的程序中的bug都会被修复;您不必像使用静态库那样重新链接所有这些程序。
这些优点也许会让您认为应该尽量使用共享库。但是,仍然存在一些现实的理由让程序选择链接到静态库。升级共享库同时会升级所有依赖程序的特点很可能成为一个缺陷。假设您开发了一个用于处理关键性任务的程序,您可能应该选择静态链接您的程序以防止对系统的升级影响到您的程序的运行。(否则,也许用户会升级系统中的共享库,由此影响到您程序的运行,然后打电话到您的技术支持热线并责怪您的程序的错误。)
如果您可能没有将库安装到 /lib或 /usr/lib的权限,您绝对应该重新考虑是否将您的库作为共享库发布。(除非您要求您的用户具有管理员权限,您的库将无法被安装到 /lib或 /usr/lib目录。)而且,如果您不确定库最终被安装的位置,-Wl,rpath的办法也无法起作用。让您的用户去设置LD_LIBRARY_PATH对他们而言意味着额外的步骤。因为每个用户都必须为自己设置这个环境变量,这将着实成为一个负担。
每当您尝试发布一个程序的时候,您都不得不对这些有缺点进行权重并选择合适的形式发布您的程序。
动态加载与卸载
有时,您可能希望在运行时加载一些代码,而不是将这些代码直接链接进程序。例如,设想一个支持“插件”模块的程序,如一个网页浏览器。浏览器允许第三方开发者制作插件以提供额外的功能。这些开发者制作共享库,并将它放在指定的位置。浏览器在运行的时候将自动加载这些库中的代码。
在Linux系统中,这种功能可以通过使用dlopen函数获取。您可以这样通过dlopen加载一个名为dlopen的函数:
dlopen (“libtest.so”, RTLD_LAZY);
(第二个参数是一个标志,它指明了绑定库中符号的方法。您可以参考dlopen的手册页以获取更详细的信息,不过RTLD_LAZY通常就是您所需要的。)如果使用动态加载函数,您需要在程序文件中包含 <dlfcn.h> 头文件,并将程序链接到 libdl库(通过为编译器指定 -ldl参数)。
这个函数会返回一个void *指针;这个指针将被用作一个操作被加载的共享库的句柄。您可以将这个指针传递给dlsym函数以获取被加载的库中特定函数的地址。假设libtest.so中定义了一个函数my_function,则您可以这样调用这个函数:
void* handle = dlopen (“libtest.so”, RTLD_LAZY); void (*test)() = dlsym (handle, “my_function”); (*test)(); dlclose (handle);
这里,系统调用dlsym还可以用于从共享库中获取静态变量的地址。
前面提到的两个函数,dlopen 和 dlsym,均会在执行失败的时候返回NULL。这时您可以调用dlerror(不需指定任何参数)获取一个可读的信息对出现的错误进行解释。
函数dlclose可以从内存中卸载已经加载的库。技术上来说,dlopen只在库并未被加载的情况下将共享库载入内存;如果这个库已被加载,则dlopen仅增加指向这个库的引用计数。同样的,dlclose只是将库的引用计数减一;只有当引用计数到达0的时候这个函数才会真正地将库卸载。
如果您的共享库是用C++ 语言写成,则您需要将那些用于提供外界访问的函数和变量用extern “C” 链接修饰符进行修饰。假设您的共享库中有一个C++ 函数 foo,而您希望通过dlsym访问这个函数,您需要这样对它进行声明:
extern “C” void foo ();
这样就可以防止C++ 编译器对函数名称进行修饰。否则,C++ 编译器可能将函数名从foo变为另外一个看起来很可笑的名字;这个名字中包含了其它一些与这个函数相关的信息。C编译器不会对标识符进行修饰;它会直接使用任何您指定的函数或变量名。
3 还有其它一些选项,包括从存档中删除一个文件或者执行其它操作。这些操作很少用,不过在ar手册页中都有说明。4 在一些在线文档中可能您会看见对 LD_RUN_PATH环境变量的引用。不要相信它们;这个变量在GNU/Linux系统中不起任何作用。