共享库是一种将库函数打包成一个单元使之能够在运行时被多个进程共享的技术。 这种技术能够节省磁盘空间和 RAM。本章将介绍共享库的基础知识,下一章将介绍共享库的几个高级特性
41.1 目标库
构建程序的一种方式是简单地将每一个源文件编译成目标文件,然后将这些目标文件链接在一起组成一个可执行程序,如下所示
cc -c -g prog.c mod1.c mod2.c mod3.c
cc -g -o prog_nolib prog.o mod1.o mod2.o mod3.o
链接实际上是由一个单独的链接器程序 ld 来完成的。当使用 cc(或 gcc)命令链接一个程序时,编译器会在幕后调用 ld。在 Linux 上应该总是通过 gcc 间接地调用链接器,因为gcc 能够确保使用正确的选项来调用 ld 并将程序与正确的库文件链接起来
在很多情况下,源代码文件也可以被多个程序共享。因此要降低工作量的第一步就是将这些源代码文件只编译一次,然后在需要的时候将它们链接进不同的可执行文件中。虽然这项技术能够节省编译时间,但其缺点是在链接的时候仍然需要为所有目标文件命名。此外,大量的目标文件会散落在系统上的各个目录中,从而造成目录中内容的混乱。
为解决这个问题,可以将一组目标文件组织成一个被称为对象库的单元。对象库分为两种:静态的和共享的。共享库是一种更加现代化的对象库,它比静态库更具优势。
41.2 静态库
在开始讨论共享库之前首先对静态库作一个简短的介绍,这样读者就能够弄清楚共享库与静态库之间的差别以及共享库所具备的优势了。 静态库也被称为归档文件,它是 UNIX 系统提供的第一种库。静态库能带来下列好处。 1.可以将一组经常被用到的目标文件组织进单个库文件,这样就可以使用它来构建多个可执行程序并且在构建各个应用程序的时候无需重新编译原来的源代码文件。 2.链接命令变得更加简单了。在链接命令行中只需要指定静态库的名称即可,而无需一个个地列出目标文件了。链接器知道如何搜素静态库并将可执行程序需要的对象抽取出来。
创建和维护静态库
从结果上来看,静态库实际上就是一个保存所有被添加到其中的目标文件的副本的文件。 这个归档文件还记录着每个目标文件的各种特性,包括文件权限、数字用户和组 ID 以及最后修改时间。根据惯例,静态库的名称的形式为 libname.a。 使用 ar(1)命令能够创建和维护静态库,其通用形式如下所示
ar options archive object-file...
options 参数由一系列的字母构成,其中一个是操作代码,其他是能够影响操作的执行的修饰符。下面是一些常用的操作代码。
r(替换):将一个目标文件插入到归档文件中并取代同名的目标文件。这个创建和更新归档文件的标准方法,使用下面的命令可以构建一个归档文件。
cc -g -c mod1.c mod2.c mod3.c
ar r libdemo.a mod1.o mod2.o mod3.o
rm mod1.o mod2.o mod3.o
从上面可以看出,在构建完库之后可以根据需要删除原始的目标文件,因为已经不再需要它们了。 t(目录表):显示归档中的目录表。在默认情况下只会列出归档文件中目标文件的名称。添加 v( verbose)修饰符之后可以看到记录在归档文件中的各个目标文件的其他所有特性,如下面的例子所示
从左至右每个目标文件的特性为被添加到归档文件中时的权限、用户 ID 和组 ID、大小以及上次修改的日志和时间。
ar tv libdemo.a
d(删除):从归档文件中删除一个模块,如下面的例子所示
ar d libdemo.a mod3.o
使用静态库
将程序与静态库链接起来存在两种方式。第一种是在链接命令中指定静态库的名称,如下所示
cc -g -c prog.c
cc -g -o prog prog.o libdemo.a
#或者将静态库放在链接器搜索的其中一个标准目录中(如/usr/lib),然后使用-l 选项指定库名
cc -g -o prog prog.o -ldemo
#如果库不位于链接器搜索的目录中,那么可以只用-L 选项指定链接器应该搜索这个额外的目录。
cc -g -o prog prog.o -Lmylibdir -ldemo
#在链接完程序之后可以按照通常的方式运行这个程序。
./prog
41.3 共享库概述
将程序与静态库链接起来时(或没有使用静态库),得到的可执行文件会包含所有被链接进程序的目标文件的副本。这样当几个不同的可执行程序使用了同样的目标模块时,每个可执行程序会拥有自己的目标模块的副本。这种代码的冗余存在几个缺点。 1.存储同一个目标模块的多个副本会浪费磁盘空间,并且所浪费的空间是比较大的。
2.如果几个使用了同一模块的程序在同一时刻运行,那么每个程序会独立地在虚拟内存中保存一份目标模块的副本,从而提高系统中虚拟内存的整体使用量。
3.如果需要修改一个静态库中的一个目标模块(可能是因为安全性或需要修正 bug),那么所有使用那个模块的可执行文件都必须要重新进行链接以合并这个变更。这个缺点还会导致系统管理员需要弄清楚哪些应用程序链接了这个库。 共享库就是设计用来解决这些缺点的。
共享库的关键思想是目标模块的单个副本由所有需要这些模块的程序共享。目标模块不会被复制到链接过的可执行文件中,相反,当第一个需要共享库中的模块的程序启动时,库的单个副本就会在运行时被加载进内存。当后面使用同一共享库的其他程序启动时,它们会使用已经被加载进内存的库的副本。使用共享库意味着可执行程序需要的磁盘空间和虚拟内存(在运行的时候)更少了。
共享库还具备下列优势。
1.由于整个程序的大小变得更小了, 因此在一些情况下, 程序可以完全被加载进内存中,从而能够更快地启动程序。这一点只有在大型共享库正在被其他程序使用的情况下才成立。第一个加载共享库的程序实际上在启动时会花费更长的时间,因为必须要先找到共享库并将其加载到内存中。
2.由于目标模块没有被复制进可执行文件中,而是在共享库中集中维护的,因此在修改目标模块时无需重新链接程序就能够看到变更,甚至在运行着的程序正在使用共享库的现有版本的时候也能够进行这样的变更。
这项新增功能的主要开销如下所述。 1.在概念上以及创建共享库和构建使用共享库的程序的实践上,共享库比静态库更复杂。
2.共享库在编译时必须要使用位置独立的代码,这在大多数架构上都会带来性能开销,因为它需要使用额外的一个寄存器。
3.在运行时必须要执行符号重定位。在符号重定位期间,需要将对共享库中每个符号(变量或函数)的引用修改成符号在虚拟内存中的实际运行时位置。由于存在这个重定位的过程,与静态链接程序相比, 一个使用共享库的程序或多或少需要花费一些时间来执行这个过程。
41.4 创建和使用共享库
为了理解共享库的操作方式,下面开始介绍构建和使用一个共享库所需完成的最少步骤,在介绍的过程中会忽略平时使用的共享库文件命名规范。遵循第 41.6 节中介绍的惯例允许程序自动加载它们所需的共享库的最新版本,同时也允许一个库的多个相互不兼容的版本和谐地共存。 在本章中,我们只关心 Executable and Linking Format( ELF)共享库,因为现代版本的Linux 以及很多其他 UNIX 实现的可执行文件和共享库都采用了 ELF 格式
41.4.1 创建一个共享库
为构建之前创建的静态库的共享版本,需要执行下面的步骤
#创建了三个将要被放到库中的目标模块
gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
#创建了一个包含这三个目标模块的共享库,根据惯例,共享库的前缀为lib,后缀为.so,表示 shared object
gcc -g shared -o libfoo.so mod1.o mod2.o mod3.o
第一个命令创建了三个将要被放到库中的目标模块。 cc –shared 命令创建了一个包含这三个目标模块的共享库。 根据惯例,共享库的前缀为 lib,后缀为.so(表示 shared object)。 在上面的例子中使用了 gcc 命令,而并没有使用与之等价的 cc 命令,这是为了突出用来创建共享库的命令行选项是依赖于编译器的,在另一个 UNIX 实现上使用一个不同的 C 编译器可能会需要使用不同的选项。 注意可以将编译源代码文件和创建共享库放在一个命令中执行
gcc -g -fPIC -Wall mod1.c mod2.c mod3.c -shared -o libfoo.so
这里为了清楚区分编译和构建库两个步骤,所以在本章给出的例子中使用了两个独立的命令。 与静态库不同,可以向之前构建的共享库中添加单个目标模块,也可以从中删除单个目标模块。与普通的可执行文件一样,共享库中的目标文件不再维护不同的身份。
41.4.2 位置独立的代码
cc-fPIC 选项指定编译器应该生成位置独立的代码, 这会改变编译器生成执行特定操作的代码的方式,包括访问全局、静态和外部变量,访问字符串常量,以及获取函数的地址。
这些变更使得代码可以在运行时被放置在任意一个虚拟地址处。这一点对于共享库来讲是必需的,因为在链接的时候是无法知道共享库代码位于内存的何处的。 在 Linux/x86-32 上,可以使用不加–fPIC 选项编译的模块来创建共享库。但这样做的话会丢失共享库的一些优点,因为包含依赖于位置的内存引用的程序文本页面不会在进程间共享。 在一些架构上是无法在不加–fPIC 选项的情况下构建共享库的。 为了确定一个既有目标文件在编译时是否使用了–fPIC 选项,可以使用下面两个命令中的一个来检查目标文件符号表中是否存在名称GLOBAL_OFFSET_TABLE。
nm mod1.o |grep _GLOBAL_OFFSET_TABLE_
readelf -s mod1.o |grep _GLOBAL_OFFSET_TABLE_
相应地,如果下面两个相互等价的命令中的任意一个产生了任何输出,那么指定的共享库中至少存在一个目标模块在编译时没有指定–fPIC 选项。
objdump --all-headers libfoo.so |grep TEXTREL
readelf -d libfoo.so |grep TEXTREL
字符串 TEXTREL 表示存在一个目标模块,其文本段中包含需要运行时重定位的引用
41.4.3 使用一个共享库
为了使用一个共享库就需要做两件事情,而使用静态库的程序则无需完成这两件事情。 1.由于可执行文件不再包含它所需的目标文件的副本,因此它必须要通过某种机制找出在运行时所需的共享库。这是通过在链接阶段将共享库的名称嵌入可执行文件中来完成的。 (在 ELF 中,库依赖性是记录在可执行文件的 DT_NEEDED 标签中的。 )
一个程序所依赖的所有共享库列表被称为程序的动态依赖列表。
2.在运行时必须要存在某种机制来解析嵌入的库名——即找出与在可执行文件中指定的名称对应的共享库文件——接着如果库不在内存中的话就将库加载进内存。
将程序与共享库链接起来时自动会将库的名字嵌入可执行文件中
gcc -g -Wall -o prog prog.c libfoo.so
如果现在运行这个程序,那么就会收到下面的错误消息。 no such file or directory
解决这个问题就需要做第二件事情:动态链接,即在运行时解析内嵌的库名。这个任务是由动态链接器(也称为动态链接加载器或运行时链接器)来完成的。动态链接器本身也是一个共享库,其名称为/lib/ld-linux.so.2,所有使用共享库的 ELF 可执行文件都会用到这个共享库
动态链接器会检查程序所需的共享库清单并使用一组预先定义好的规则来在文件系统上找出相关的库文件。其中一些规则指定了一组存放共享库的标准目录。如很多共享库位于/lib和/usr/lib 中。之所以出现上面的错误消息是因为程序所需的库位于当前工作目录中,而不位于动态链接器搜索的标准目录清单中。
LD_LIBRARY_PATH 环境变量
通知动态链接器一个共享库位于一个非标准目录中的一种方法是将该目录添加到LD_LIBRARY_PATH 环境变量中以分号分隔的目录列表中。
如 果 定 义 了LD_LIBRARY_PATH,那么动态链接器在查找标准库目录之前会先查找该环境变量列出的目录中的共享库。 因此可以使用下面的命令来运行程序。
LD_LIBRARY_PATH =.
./prog
上面的命令中使用的 shell语法在执行 prog 的进程中创建了一个环境变量定义。这个定义告诉动态链接器在.,即当前工作目录中搜索共享库
静态链接和动态链接比较
通常,术语链接用来表示使用链接器 ld 将一个或多个编译过的目标文件组合成一个可执行文件。有时候会使用术语静态链接从动态链接中将在运行时加载可执行文件所需的共享库这一步骤给区分出来。(静态链接有时候也被称为链接编辑,像 ld 这样的静态链接器有时候被称为链接编辑器。 )每个程序——包括那些使用共享库的程序——都会经历一个静态链接的阶 段。
在运行时,使用共享库的程序会经历额外的动态链接阶段
41.4.4 共享库 soname
到目前为止介绍的所有例子中, 嵌入到可执行文件以及动态链接器在运行时搜索的名称是共享库文件的实际名称,这被称为库的真实名称。
但可以——实际上经常这样做——使用别名来创建共享库,这种别名称为 soname( ELF 中的 DT_SONAME 标签)。 如果共享库拥有一个 soname,那么在静态链接阶段会将 soname 嵌入到可执行文件中,而不会使用真实名称,同时后面的动态链接器在运行时也会使用这个 soname 来搜索库。引入soname 的目的是为了提供一层间接,使得可执行程序能够在运行时使用与链接时使用的库不同的(但兼容的)共享库。 在 41.6 节中将会介绍共享库的真实名称和 soname 的命名规则。 下面通过一个简化的例子来说明这些原则。 使用 soname 的第一步是在创建共享库时指定 soname。
gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
gcc -g -shared -Wl,-soname,libbar.so -o libfoo.so mod1.o mod2.o mod3.o
–Wl、 –soname 以及 libbar.so 选项是传给链接器的指令以将共享库 libfoo.so 的 soname 设置为 libbar.so。
如果要确定一个既有共享库的 soname,那么可以使用下面两个命令中的任意一个
objdump -p libfoo.so |grep SONAME
readelf -d libfoo.so |grep SONAME
在使用 soname 创建了一个共享库之后就可以照常创建可执行文件了。
gcc -g -Wall -o prog prog.c libfoo.so
但这次链接器检查到库 libfoo.so 包含了 soname libbar.so,于是将这个 soname 嵌入到了可执行文件中。 现在当运行这个程序时就会看到下面的输出。 no such file or directory
这里的问题是动态链接器无法找到名为 libbar.so 共享库。当使用 soname 时还需要做一件事情:必须要创建一个符号链接将 soname 指向库的真实名称,并且必须要将这个符号链接放在动态链接器搜索的其中一个目录中。因此可以像下面这样运行这个程序。
ln -s libfoo.so libbar.so
LD_LIBRARY_PATH =. ./prog
给出了当图 41-1 中创建的程序被加载进内存以备执行时发生的事情