Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

8.6 共享库的创建和安装

8.6.1 共享库的创建

创建共享库非常简单,我们在前面已经演示了如何创建一个".so"共享对象。创建共享库的过程跟创建一般的共享对象的过程基本一致,最关键的是使用GCC的两个参数,即"-shared"和"-fPIC"。"-shared"表示输出结果是共享库类型的;"-fPIC"表示使用地址无关代码(Position Independent Code)技术来生产输出文件。另外还有一个参数是"-Wl"参数,这个参数可以将指定的参数传递给链接器,比如当我们使用"-Wl、-soname、my_soname"时,GCC会将"-soname my_soname"传递给链接器,用来指定输出共享库的SO-NAME。所以我们可以使用如下命令行来生成一个共享库:

$gcc -shared -Wl,-soname,my_soname -o library_name source_files library_files

注意

如果我们不使用-soname来指定共享库的SO-NAME,那么该共享库默认就没有SO-NAME,即使用ldconfig更新SO-NAME的软链接时,对该共享库也没有效果。

比如我们有libfoo1.c和libfoo2.c两个源代码文件,希望产生一个libfoo.so.1.0.0的共享库,这个共享库依赖于libbar1.so和 libbar2.so这两个共享库,我们可以使用如下命令行:

$gcc –shared -fPIC –Wl,-soname,libfoo.so.1 –o libfoo.so.1.0.0 \
libfoo1.c libfoo2.c \
-lbar1 -lbar2

当然我们也可以把编译和链接的步骤分开,分多步进行:

$gcc –c –g –Wall –o libfoo1.o libfoo1.c
$gcc –c –g –Wall –o libfoo2.o libfoo2.c
$ld –shared –soname libfoo.so.1 –o libfoo.so.1.0.0 \
libfoo1.o libfoo2.o –lbar1 –lbar2

几个值得注意的事项:

  • 不要把输出共享库中的符号和调试信息去掉,也不要使用GCC的"-fomit-frame-pointer"选项,这样做虽然不会导致共享库停止运行,但是会影响调试共享库,给后面的工作带来很多麻烦。关于"-fomit-frame-pointer"请参照后面的"函数调用和堆栈"这一节。

  • 在开发过程中,你可能要测试新的共享库,但是你又不希望影响现有的程序正常运行。我们前面提到的LD_LIBRARY_PATH是一个很好的方法,用它可以指定共享库的查找路径。还有一种方法是使用链接器的"-rpath"选项(或者GCC的-Wl,-rpath),这种方法可以指定链接产生的目标程序的共享库查找路径。比如我们用如下命令行产生一个可执行文件:

    $ld -rpath /home/mylib -o program.out program.o -lsomelib
    

    这样产生的输出可执行文件program.out在被动态链接器装载时,动态链接器会首先在"/home/mylib"查找共享库。

  • 默认情况下,链接器在产生可执行文件时,只会将那些链接时被其他共享模块引用到的符号放到动态符号表,这样可以减少动态符号表的大小。也就是说,在共享模块中反向引用主模块中的符号时,只有那些在链接时被共享模块引用到的符号才会被导出。有一种情况是,当程序使用dlopen()动态加载某个共享模块,而该共享模块须反向引用主模块的符号时,有可能主模块的某些符号因为在链接时没有被其他共享模块引用而没有被放到动态符号表里面,导致了反向引用失败。ld链接器提供了一个"-export-dynamic"的参数,这个参数表示链接器在生产可执行文件时,将所有全局符号导出到动态符号表,以防止出现上述问题。我们也可以在GCC中使用"-Wl,-export-dynamic"将该参数传递给链接器。

8.6.2 清除符号信息

正常情况下编译出来的共享库或可执行文件里面带有符号信息和调试信息,这些信息在调试时非常有用,但是对于最终发布的版本来说,这些符号信息用处并不大,并且使得文件尺寸变大。我们可以使用一个叫"strip"的工具清除掉共享库或可执行文件的所有符号和调试信息("strip"是binutils的一部分):

$strip libfoo.so

去除符号和调试信息以后的文件往往比之前要小很多,一般只有原来的一半大小,甚至不到一半。除了使用"strip"工具,我们还可以使用ld的"-s"和"-S"参数,使得链接器生成输出文件时就不产生符号信息。"-s"和"-S"的区别是:"-S"消除调试符号信息,而"-s"消除所有符号信息。我们也可以在gcc中通过"-Wl,-s"和"-Wl,-S"给ld传递这两个参数。

8.6.3 共享库的安装

创建共享库以后我们须将它安装在系统中,以便于各种程序都可以共享它。最简单的办法就是将共享库复制到某个标准的共享库目录,如/lib、/usr/lib等,然后运行ldconfig即可。

不过上述方法往往需要系统的root权限,如果没有,则无法往/lib、/usr/lib等目录添加文件,也无法运行ldconfig程序。当然我们也有其他办法安装共享库,只不过步骤稍微麻烦一些,无非是建立相应的SO-NAME软链接,并告诉编译器和程序如何查找该共享库等,以便于编译器和程序都能够正常运行。建立SO-NAME的办法也是使用ldconfig,只不过需要指定共享库所在的目录:

$ldconfig -n shared_library_directory

在编译程序时,也需要指定共享库的位置,GCC提供了两个参数"-L"和"-l",分别用于指定共享库搜索目录和共享库的路径。当然也可以使用前面提到过的"-rpath"参数,这几个参数之间有些细微的区别,我们这里不详细解释了,它们的作用都是用来指定共享库的位置,具体可以参照GCC的手册。前面提到过的LD_LIBRARY_PATH的方法也可以用来指定某个共享库的位置。

8.6.4 共享库构造和析构函数

很多时候你希望共享库在被装载时能够进行一些初始化工作,比如打开文件、网络连接等,使得共享库里面的函数接口能够正常工作。GCC提供了一种共享库的构造函数,只要在函数声明时加上"__attribute__((constructor))"的属性,即指定该函数为共享库构造函数,拥有这种属性的函数会在共享库加载时被执行,即在程序的main函数之前执行。如果我们使用dlopen()打开共享库,共享库构造函数会在dlopen()返回之前被执行。

与共享库构造函数相对应的是析构函数,我们可以使用在函数声明时加上"__attribute__((destructor))"的属性,这种函数会在main()函数执行完毕之后执行(或者是程序调用exit()时执行)。如果共享库是运行时加载的,那么我们使用dlclose()来卸载共享库时,析构函数将会在dlclose()返回之前执行。声明构造和析构函数的格式如下:

void __attribute__((constructor)) init_function(void);
void __attribute__((destructor))  fini_function (void);

当然,这种__attribute__的语法是GCC对C和C++语言的扩展,在其他编译器上这种语法并不通用。

值得注意的是,如果我们使用了这种析构或构造函数,那么必须使用系统默认的标准运行库和启动文件,即不可以使用GCC的"-nostartfiles"或"-nostdlib"这两个参数。因为这些构造和析构函数是在系统默认的标准运行库或启动文件里面被运行的,如果没有这些辅助结构,它们可能不会被运行。我们将在后面的关于系统库和启动文件的章节更加详细介绍相关的机制。

另外还有一个问题是,如果我们有多个构造函数,那么默认情况下,它们被执行的顺序是没有规定的。如果我们希望构造和析构函数能够按照一定的顺序执行,GCC为我们提供了一个参数叫做优先级,我们可以指定某个构造或析构函数的优先级:

void __attribute__((constructor(5))) init_function1(void);
void __attribute__((constructor(10))) init_function2(void);

对于构造函数来说,属性中优先级数字越小的函数将会在优先级大的函数之前运行;而对于析构函数来讲,则刚好相反。这种安排有利于构造函数和析构函数能够匹配,比如某一对构造函数和析构函数分别用来申请和释放某个资源,那么它们可以拥有一样的优先级。这样做的结果往往是先申请的资源后释放,符合资源释放的一般规则。

8.6.5 共享库脚本

我们前面所提到的共享库都是动态链接的ELF共享对象文件(.so),事实上,共享库还可以是符合一定格式的链接脚本文件。通过这种脚本文件,我们可以把几个现有的共享库通过一定的方式组合起来,从用户的角度看就是一个新的共享库。比如我们可以把C运行库和数学库组合成一个新的库libfoo.so,那么libfoo.so的内容可以如下:

GROUP( /lib/libc.so.6 /lib/libm.so.2)

我们在前面也介绍过LD的链接脚本,这里的脚本与LD的脚本从语法和命令上来讲没什么区别,它们的作用也相似,即将一个或多个输入文件以一定的格式经过变换以后形成一个输出文件。我们也可以将这种共享库脚本叫做动态链接脚本,因为这个链接过程是动态完成的,也就是运行时完成的。