Book-Linux UNIX 系统编程手册-下册-41章 共享库基础

Book-Linux UNIX 系统编程手册-下册-41章 共享库基础

共享库是一种将库函数打包成一个单元使之能够在运行时被多个进程共享的技术。这种技术能
够节省磁盘空间和RAM。

41.1目标库

构建程序的一种方式是简单地将每一个源文件编译成目标文件,然后将这些目标文件链接在一起组成一个可执行程序,如下所示。

$ cc -g -c 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并将程序与正确的库文件链接起来。

在很多情况下,源代码文件也可以被多个程序共享。因此要降低工作量的第一步就是将这些源代码文件只编译一次,然后在需要的时候将它们链接进不同的可执行文件中。虽然这项技术能够节省编译时间,但其缺点是在链接的时候仍然需要为所有目标文件命名。此外,大量的目标文件会散落在系统上的各个目录中,从而造成目录中内容的混乱。
为解决这个问题,可以将一组目标文件组织成一个被称为对象库的单元。对象库分为两种:静态的和共享的。共享库是一种更加现代化的对象库,它比静态库更具优势。

题外话:在编译程序时包含调试器信息

在上面的cc命令中使用了-g选项以在编译过的程序中包含调试信息。一般来讲,创建允许调试的程序和库是一种比较好的做法。(在早期,有时候会忽略调试信息,这样产生的可执行文件会占用更少的磁盘和RAM,但现在磁盘和RAM已经非常便宜了。)
此外,在一些架构上,如x86-32,不应该指定-fomit-frame-pointer 选项,因为这会使得无法调试。(在一些架构上,如x86-64,这个选项是默认启用的,因为它不会防止调试。)出于同样的原因,可执行文件和库不应该使用strip(1)删除调试信息。


41.2静态库

静态库也被称为归档文件,它是UNIX系统提供的第一种库。静态库能带来下列好处。

  • 可以将一组经常被用到的目标文件组织进单个库文件,这样就可以使用它来构建多个可执行程序并且在构建各个应用程序的时候无需重新编译原来的源代码文件。
  • 链接命令变得更加简单了。在链接命令行中只需要指定静态库的名称即可,而无需一个个地列出目标文件了。链接器知道如何搜素静态库并将可执行程序需要的对象抽取出来。

创建和维护静态库

从结果上来看,静态库多际上就是一个保存所有被添加到其中的目标文件的副本的文件。这个归档文件还记录着每个目标文件的各种特性,包括文件权限、数字用户和组ID以及最后修改时间。根据惯例,静态库的名称的形式为libname.a.
使用ar(1)命令能够创建和维护静态库,其通用形式如下所示.
$ ar options archive object-file..
opions 参数由一系列的字母构成,其中一个是操作代码,其他是能够影响操作的执行的修饰符。下面是一些常用的操作代码。
r(替换):将一个目标文件插入到归档文件中并取代同名的目标文件。这个创建和更新归档文件的标准方法,使用下面的命令可以构建一个归档文件。

$ cc -g -c mod1.c mod2.c mod3.c
$ ar r libdemo.a mod1.o modz.omod3.o
$ rm mod1.o mod2.o mod3.o

从上面可以看出,在构建完库之后可以根据需要删除原始的目标文件,因为已经不再需要它们了。

t(目录表):显示归档中的目录表。在默认情况下只会列出归档文件中目标文件的名称。添加v(verbose)修饰符之后可以看到记录在归档文件中的各个目标文件的其他所有特性,如下面的例子所示。

$ ar tv libdemo.a
rw-r--r-- 1000/100 1001016 Nov 15 12:26 2009 mod1.o
rw-r--r- 1000/100 406668. Nov 15 12:21 2009 mod2.o
rw-r--r-
 1000/100 46672 Nov 15 12:21 2009 mod3.o

从左至右每个目标文件的特性为被添加到归档文件中时的权限、用户ID和组ID、大小以及上次修改的日志和时间。
d(删除):从归档文件中删除一个模块,如下面的例子所示。
$ ar d libdemo.a mod3.o

使用静态库

将程序与静态库链接起来存在两种方式。
第一种是在链接命令中指定静态库的名称,如下所示。

$cc -g -c prog.c
$cc -g -o prog prog.o libdemo.a

或者将静态库放在链接器搜索的其中一个标准目录中(如/usr/lib),然后使用-l选项指定库名(即库的文件名去除了lib前缀和a后缀)。
$ cc -g -o prog prog.o -Idemo
如果库不位于链接器拽索的目录中,那么可以只用-L选项指定链接器应该搜索这个额外的目录。
$ cc -g -o prog prog.o -Lmylibdir -ldemo
虽然一个静态库可以包含很多目标模块,但链接器只会包含那些程序需要的模块。

在链接完程序之后可以按照通常的方式运行这个程序。

$./prog
Called mod1-x1
Called mod2-x2

41.3 共享库概述

共享库的关键思想是目标模块的单个副本由所有需要这些模块的程序共享。目标模块不会被复制到链接过的可执行文件中,相反,当第一个需要共享库中的模块的程序启动时,库的单个副本就会在运行时被加载进内存。当后面使用同一共享库的其他程序启动时,它们会使用已经被加载进内存的库的副本。使用共享库意味着可执行程序需要的磁盘空间和虚拟内存(在运行的时候)更少了。

虽然共享库的代码是由多个进程共享的,但其中的变量却不是的。每个使用库的进程会拥有自己的在库中定义的全局和静态变量的副本。

共享库优势

共享库还具备下列优势。

  • 由于整个程序的大小变得更小了,因此在一些情况下,程序可以完全被加载进内存中,从而能够更快地启动程序。这一点只有在大型共享库正在被其他程序使用的情况下才成立。第一个加载共享库的程序实际上在启动时会花费更长的时间,因为必须要先找到共享库并将其加载到内存中。
  • 由于目标模块没有被复制进可执行文件中,而是在共享库中集中维护的,因此在修改目标模块时(需遵循41.8节中介绍的限制)无需重新链接程序就能够看到变更,甚至在运行着的程序正在使用共享库的现有版本的时候也能够进行这样的变更。
    这项新增功能的主要开销如下所述。
  • 在概念上以及创建共享库和构建使用共享库的程序的实践上,共享库比静态库更复杂。
  • 共享库在编译时必须要使用位置独立的代码(在41.4.2节中予以介绍),这在大多数架构上都会带来性能开销,因为它需要使用额外的一个寄存器([Hubicka,2003])。
  • 在运行时必须要执行符号重定位。在符号重定位期间,需要将对共享库中每个符号(变量或函数)的引用修改成符号在虚拟内存中的实际运行时位置。由于存在这个重定位的过程,与静态链接程序相比,一个使用共享库的程序或多或少需要花费一些时间来执行这个过程。

共享库的另一种用法是作为Java Nativelnterface(JNI)中的一个构建块,它允许Java代码通过调用共享库中的C函数直接访问底层操作系统的特性,更多信息可参考[Liang,1999]和[Rochkind,2004]。

41.4 创建和使用共享库

在本章中,我们只关心Executable and Linking Format(ELF)共享库,因为现代版本的Linux以及很多其他UNIX实现的可执行文件和共享库都采用了ELF格式。
ELF取代了较早以前的a.out 和COFF格式。

41.4.1创建一个共享库

为构建之前创建的静态库的共享版本,需要执行下面的步骤。

$ gcc -g -C -fPIC -Wall modi.c mod2.c mod3.c
$ 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 modi.c mod2.c mod3.c -shared -o libfoo.so

与静态库不同,可以向之前构建的共享库中添加单个目标模块,也可以从中删除单个目标模块。与普通的可执行文件一样,共享库中的目标文件不再维护不同的身份。


41.4.2位置独立的代码 -fPIC

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 使用一个共享库

为了使用一个共享库就需要做两件事情,而使用静态库的程序则无需完成这两件事情。

  • 由于可执行文件不再包含它所需的目标文件的副本,因此它必须要通过某种机制找出在运行时所需的共享库。这是通过在链接阶段将共享库的名称嵌入可执行文件中来完成的。(在ELF中,库依赖性是记录在可执行文件的DT_NEEDED标签中的。)一个程序所依赖的所有共享库列表被称为程序的动态依赖列表。
  • 在运行时必须要存在某种机制来解析嵌入的库名——即找出与在可执行文件中指定的名称对应的共享库文件——接着如果库不在内存中的话就将库加载进内存。

将程序与共享库链接起来时自动会将库的名字嵌入可执行文件中。
$ gcc -g -Wall -o prog prog.c libfoo.so
如果现在运行这个程序,那么就会收到下面的错误消息。

$./prog
./prog: error in loading shared libraries: libfoo.so: cannot
open shared object file: No such file or directory

解决这个问题就需要做第二件事情:动态链接,即在运行时解析内嵌的库名。这个任务是由动态链接器(也称为动态链接加载器或运行时链接器)来完成的。动态链接器本身也是一个共享库,其名称为/lib/ld-linux.so.2,所有使用共享库的ELF可执行文件都会用到这个共享库。

路径名/lib/ld-linux.so.2通常是一个指向动态链接器可执行文件的符号链接。这个文件的名称为Id-version.so,其中version 表示安装在系统上的glibc的版本—如ld-2.11.so。在一些架构上,动态链接器的路径名是不同的。如在IA-64上,动态链接器符号链接的名称为/lib/ld-linux-ia64.so.2

动态链接器会检查程序所需的共享库清单并使用一组预先定义好的规则来在文件系统上找出相关的库文件。其中一些规则指定了一组存放共享库的标准目录。如很多共享库位于/lib/usr/lib中。之所以出现上面的错误消息是因为程序所需的库位于当前工作目录中,而不位于动态链接器搜索的标准目录清单中。
一些架构(如zSeries、PowerPC64以及x86-64)同时支持执行32位和64位的程序在此类系统上,32 位的库位于*/lib子目录中,64 位的库位于*/iib64子目录中。

LD_LIBRARY_PATH环境变量

通知动态链接器一个共享库位于一个非标准目录中的一种方法是将该目录添加到LD_LIBRARY_PATH环境变量中以分号分隔的目录列表中。(也可以使用分号来分隔,在使用分号时必须将列表放在引号中以防止 shell 将分号解释了其他用途。)如果定义了LD_LIBRARY_PATH,那么动态链接器在查找标准库目录之前会先找该环境变量列出的目录中的共享库。因此可以使用下面的命令来运行程序。

$ LD_LIBRARY_PATH=. ./prog
Called mod1-x1
Called mod2-x2

上面的命令中使用的shell(bash、Korn以及Bourne)语法在执行prog的进程中创建了
个环境变量定义。这个定义告诉动态链接器在.,即当前工作目录中搜索共享库。
LD_LIBRARY_PATH列表中的空目录(如 dirxa::diry中间的空目录)等价于.,即当前工作目录(但注意将LD_LIBRARY_PATH的值设置为空字符串并不能达到同样效果)。需要避免这种用法(SUSv3同样不建议在PATH环境变量中使用这种方式)。

静态链接和动态链接比较

通常,术语链接用来表示使用链接器ld将一个或多个编译过的目标文件组合成一个可执行文件。有时候会使用术语静态链接从动态链接中将在运行时加载可执行文件所需的共享库这一步骤给区分出来。(静态链接有时候也被称为链接编辑,像ld这样的静态链接器有时候被称为链接编辑器。)每个程序——包括那些使用共享库的程序——都会经历一个静态链接的阶段。在运行时,使用共享库的程序会经历额外的动态链接阶段。

41.4.4 共享库soname

到目前为止介绍的所有例子中,嵌入到可执行文件以及动态链接器在运行时搜索的名称是共享库文件的实际名称,这被称为库的真实名称(realname)。但可以——实际上经常这样做——使甩别名来创建共享库,这种别名称为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
SONAME       libbar.so
$ readelf -d libfoo.so | grep SONAME
0x0000000e (SONAME) Library soname:[libbar.so]

在使用soname创建了一个共享库之后就可以照常创建可执行文件了。
$ gcc -g -Wall -o prog prog.c libfoo.so
但这次链接器检查到库 libfoo.so包含了soname libbar.so,于是将这个soname嵌入到了可执行文件中。
现在当运行这个程序时就会看到下面的输出。

$ LD_LIBRARY_PATH=. ./prog
prog: error in loading shared libraries: libbar.so: cannot open shared object file: No such file or directory

这里的问题是动态链接器无法找到名为libbar.so共享库。当使用soname时还需要做一件事情:必须要创建一个符号链接将soname 指向库的真实名称,并且必须要将这个符号链接放在动态链接器搜索的其中一个目录中。因此可以像下面这样运行这个程序。

$ ln -s libfoo.so libbar.so  Create soname symbolic link in current directory
$ LD_LIBRARY_PATH=. ./prog 
Called mod1-x1
Called mod2-x2

要找出一个进程当前使用的共享库则可以列出相应的Linux特有的/proc/PID/maps文件中的内容。

41.5 使用共享库的特有工具

ldd命令

ldd(1)(列出动态依赖)命令显示了一个程序运行所需的共享库,如下所示。

$ ldd prog
libdemo.so.1 => /usr/lib/libdemo.so.1 (0x40019000)
libc.so.6 => /lib/tls/libc.so.6 (0x4017b000)
/lib/ld-linux.so.2 =>/lib/ld-linux.so.2(0x40000000)

ldd命令会解析出每个库引用(使用的搜索方式与动态链接器一样)并以下面的形式显示结果。
library-name => resolves-to-path

对于大多数ELF可执行文件来讲,ldd至少会列出与ld-linux.so.2、动态链接器以及标准C库libc.so.6相关的条目。

objdump和readelf命令

objdump命令能够用来获取各类信息——包括反汇编的二进制机器码——从一个可执行文件、编译过的目标以及共享库中。它还能够用来显示这些文件中各个ELF节的头部信息,当这样使用objdump时它就类似于readelf,readelf能显示类似的信息,但显示格式不同。

nm命令

nm命令会列出目标库或可执行程序中定义的一组符号。这个命令的一种用途是找出哪些库定义了一个符号。如要找出哪个库定义了crypt()函数则可以像下面这样做。

$ nm -A /usr/lib/lib*.so 2> /dev/null | grep'crypt$
/usr/lib/libcrypt.so:00007080 W crypt

nm-A选项指定了在显示符号的每一行的开头处应该列出库的名称。这样做是有必要的,因为在默认情况下,nm只列出库名一次,然后在后面会列出库中包含的所有符号,这对于像上面那样进行某种过滤的例子来讲是没有用处的。此外,这里还丢弃了标准错误输出以便隐藏与nm命令无法识别文件格式有关的错误消息。从上面的输出中可以看出,crypt()被定义在了libcrypt库中。

41.6 共享库版本和命名规则

真实名称、soname以及链接器名称
共享库的每个不兼容版本是通过一个唯一的主要版本标识符来区分的,这个主要版本标识符是共享库的真实名称的一部分。根据惯例,主要版本标识符由一个数字构成,这个数字随着库的每个不兼容版本的发布而顺序递增。除了主要版本标识符之外,真实名称还包含一个次要版本标识符,它用来区分库的主要版本中兼容的次要版本。真实名称的格式规范为libname. so.major-id.minor-id
与主要版本标识符一样,次要版本标识符可以是任意字符串。但根据惯例,它要么是一个数字,要么是两个由点分隔的数字,其中第一个数字标识出了次要版本,第二个数字表示该次要版本中的补丁号或修订号。
下面是一些共享库的真实名称。

libdemo.so.1.0.1 
libdemo.so.1.0.2   Minor version, compatible with version 1.0.1
libdemo.so.2.0.0  New major version, incompatible with version 1.*
libreadline.so.5.0

共享库的soname 包括相应的真实名称中的主要版本标识符,但不包含次要版本标识符。因此 soname的形式为libname.so.major-id
通常,会将soname 创建为包含真实名称的目录中的一个相对符号链接。下面是一些soname的例子以及它们可能通过符号链接指向的真实名称。

libdemo.so.1         ->libdemo.so.1.0.2
libdemo.so.2        -> libdemo.so.2.0.0
libreadline.so.5    -> libreadline.so.5.0

对于共享库的某个特定的主要版本来讲,可能存在几个库文件,这些库文件是通过不同的次要版本标识符来区分的。通常,每个库的主要版本的soname 会指向在主要版本中最新的次要版本(如上面的 libdemo.so例子所示)。这种配置使得在共享库的运行时操作期间版本化语义能够正确工作。由于静态链接阶段会将soname的副本(独立于次要版本)嵌入到可执行文件中并且soname符号链接后面可能会被修改指向一个更新的(次要)版本的共享库,因此可以确保可执行文件在运行时能够加载库的最新的次要版本。此外,由于一个库的不同的主要版本的soname不同,因此它们能够和平地共存并且被需要它们的程序访问。

除了真实名称和soname之外,通常还会为每个共享库定义第三个名称:链接器名称,将可执行文件与共享库链接起来时会用到这个名称。链接器名称是一个只包含库名同时不包含主要或次要版本标识符的符号链接,因此其形式为libname.so。有了链接器名称之后就可以构建能够自动使用共享库的正确版本(即最新版本)的独立于版本的链接命令了。
一般来讲,链接器名称与它所引用的文件位于同一个目录中,它既可以链接到真实名称,也可以连接到库的最新主要版本的soname.通常,最好使用指向soname的链接,因此对soname所做的变更会自动反应到链接器名称上。(在41.7节中会看到ldconfig 程序将保持soname最新的任务自动化了,因此如果使用了刚才介绍的规范的话就是隐式地维护链接器名称。)

如果需要将一个程序与共享库的一个较老的主要版本链接起来,就不能使用链接器名称。相反,在链接命令中需要通过制定具体的真实名称或soname 来标示出所需要的版本(主要版本)。
下面是一些链接器名称的例子。

libdemo.so      ->1ibdemo.so.2
libreadline.so  -> libreadline.so.5

表41-1对共享库的真实名称、soname以及链接器名称进行了总结,图41-3描绘了这些名称之间的关系。


41.7 安装共享库

一般来进,共享库及其关联的符号链接会被安装在其中一个标准库目录中,标准库目录包括

  • /usr/lib,它是大多数标准库安装的目录。
  • /lib,应该将系统启动时用到的库安装在这个目录中(因为在系统启动时可能还没有挂载/usr/lib)。
  • /usr/local/lib,应该将非标准或实验性的库安装在这个目录中(对于/usr/lib是一个由多个系统共享的网络挂载但需要只在本机安装一个库的情况则可以将库放在这个目录中)。
  • 其中一个在/etc/ld.so.conf中列出的目录。
    在大多数情况下,将文件复制到这些目录中需要具备超级用户的权限。
    安装完之后就必须要创建soname和链接器名称的符号链接了,通带它们是作为相对符号链接与库文件位于同一个目录中。
    因此要将本章的演示库安装在/usr/lib(只允许root 进行更新)中则可以使用下面的命令。
$ su
Password:
# mv libdemo.so.1.0.1 /usr/lib
# cd /usr/lib
# ln -s libdemo.so.1.0.1 libdemo.so.1
# ln -s libdemo.so.1 libdemo.so

shell会话中的最后两行创建了soname和链接器名称的符号链接。

ldconfig

ldconfig(8)解决了共享库的两个潜在问题。

  • 共享库可以位于各种目录中,如果动态链接器需要通过搜索所有这些目录来找出一个库并加载这个库,那么整个过程将非常慢。
  • 当安装了新版本的库或者删除了旧版本的库,那么soname符号链接就不是最新的。
    ldconfig 程序通过执行两个任务来解决这些问题。
    1.它搜索一组标准的目录并创建或更新一个缓存文件/etc/ld.so.cache 使之包含在所有这些目
    录中的主要库版本(每个库的主要版本的最新的次要版本)列表。动态链接器在运行时解析库名称时会轮流使用这个缓存文件。为了构建这个缓存,ldconfig 会搜索在/etc/ld.so.conf中指定的目录,然后搜索/lib/usr/lib/etc/ld.so.conf文件由一个目录路径名(应该是绝对路径名)列表构成,其中路径名之间用换行、空格、制表符、逗号或冒号分隔。在一些发行版中,/usr/local/lib目录也位于这个列表中。(如果不在这个列表中,那么就需要手工将其添加到列表中。)

命令ldconfig -p会显示/etc/ld.so.cache的当前内容

2.它检查每个库的各个主要版本的最新次要版本(即具有最大的次要版本号的版本)以找出嵌入的soname,然后在同一目录中为每个soname创建(或更新)相对符号链接。
为了能够正确执行这些动作,ldconfig要求库的名称要根据前面介绍的规范来命名(即库的真实名称包含主要和次要标识符,它们随着库的版本的更新而恰当的增长)。
在默认情况下,ldconfig会执行上面两个动作,但可以使用命令行选项来指定它执行其中一个动作:
-N选项会防止缓存的重建,
-X选项会阻止soname符号链接的创建。
此外,-v(verbose)选项会使得ldconfig输出描述其所执行的动作的信息。
每当安装了一个新的库,更新或删除了一个既有库,以及/etc/ld.so.conf中的目录列表被修改之后,都应该运行ldconfig。

41.8兼容与不兼容库比较

随着时间的流逝,可能需要修改共享库的代码。这种修改会导致产生一个新版本的库,
这个新版本可以与之前的版本兼容,也可能与之前的版本不兼容。如果是兼容的话则意味着只需要修改库的真实名称的次要版本标识符即可,如果是不兼容的话则意味着必须要定义一个库的新主要版本。
当满足下列条件时表示修改过的库与既有库版本兼容。

  • 库中所有公共方法和变量的语义保持不变。换句话说,每个函数的参数列表不变并且对全局变量和返回参数产生的影响不变,同时返回同样的结果值。因此提升性能或修复Bug(导致更加行为更加符合规定)的变更可以认为是兼容的变更。
  • 没有删除库的公共API中的函数和变量,但向公共API中添加新函数和变量不会影响兼容性。
  • 在每个函数中分配的结构以及每个函数返回的结构保持不变。类似的,由库导出的公共结构保持不变。这个规则的一个例外情况是在特定情况下,可能会向既有结构的结尾处添加新的字段,但当调用程序在分配这个结构类型的数组时会产生问题。有时候,库的设计人员会通过将导出结构的大小定义为比库的首个发行版所需的大小太来解决这个问题,即增加一些填充字段以备将来之需。
    如果所有这些条件都得到了满足,那么在更新新库名时就只需要调整既有名称中的次要版本号了,否则就需要创建库的一个新主要版本。

41.9升级共享库

共享库的优点之一是当一个运行着的程序正在使用共享库的一个既有版本时也能够安装库的新主要版本或次要版本。在安装的过程中需要做的事情包括创建新的版本、将其安装在恰当的目录中以及根据需要更新soname和链接器名称符号链接(或通常让ldconfig来完成这部分工作)。
ldconfig自动为新主要版本创建了一个soname符号链接,但必须要手工更新链接器名称的符号链接。

41.10在目标文件中指定库搜索目录

通知动态链接器共享库的位置的方式:

  • 使用LD_LIBRARY_PATH环境变量
  • 将共享库安装到其中一个标准库目录中(/lib/usr/lib或在/etc/ld.so.conf中列出的其中一个目录)
  • 在静态编辑阶段可以在可执行文件中插入一个在运行时搜索共享库的目录列表。这种方式对于库位于一个固定的但不属于动态链接器拽索的标准位置的位置中时是非常有用的。要实现这种方式需要在创建可执行文件时使用-rpath链接器选项。

$ gcc -g -Wall -W1,-rpath,/home/mtk/pdir -o prog prog.c libdemo.so
上面的命令将字符串/home/mtk/pdir 复制到了可执行文件prog的运行时库路径(rpath)列表中,因此当运行这个程序时,动态链接器在解析共享库引用时还会搜索这个目录。
如果有必要的话,可以多次指定-rpath选项;所有这些列出的目录会被连接成一个放到可执行文件中的有序rpath列表。或者,在一个rpath选项中可以指定多个分号分割开来的具录列表。在运行时,动态链接器会按照在-rpath选项中指定的目录顺序来搜索目录。

-rpath 选项的一个替代方案是LD_RUN_PATH环境变量。可以将一个由分号分隔开来的目录的字符串赋给该变量,当构建可执行文件时可以将这个变量作为rpath列表来使用。只有当构建可执行文件时不指定-rpath选项时才会使用LD_RUN_PATH变量

在构建共享库时使用-rpath链接器选项也是有用的。
使用下面的命令能够检查prog和libx1.so以便查看它们的rpath列表的内容。

$ objdump -p prog l grep PATH
RPATH   /home/mtk/pdir/d1          libx1.so will be sought here at run time
$ objdump -p d1/libx1.so | grep PATH
RPATH   /home/mtk/pdir/d2         libx2.so will be sought here at run time

还可以通过查找readelf --dynamic(或等价的readelf -d)命令的输出来查看rpath列表。

使用ldd命令能够列出prog的完整的动态依赖列表。

$ ldd prog
libx1.so =>/home/mtk/pdir/d1/libx1.so (0x40017000)
libc.so.6 => /lib/tls/libc.so.6 (0x40024000)
libx2.so => /home/mtk/pdir/d2/libx2.so (0x4014c000)
/lib/ld-linux.so.2 => /lib/ld-linux.s0.2(0x40000000)

ELF DT_RPATHDT_RUNPATH条目
在第一版ELF 规范中,只有一种rpath列表能够被嵌入到可执行文件或共享库中,它对应于ELF 文件中的DT_RPATH标签。后续的ELF 规范舍弃了DT_RPATH,同时引入了一种新标签 DT_RUNPATH 来表示rpath列表。这两种rpath列表之间的差别在于当动态链接器在运行时搜索共享库时它们相对于LD_LIBRARY_PATH环境变量的优先级:DT_RPATH的优先级更高,而DT_RUNPATH的优先级则更低(参见41.11节)。
在默认情况下,链接器会将rpath 列表创建为DT _RPATH标签。为了让链接器将rpath列表创建为DT_RUNPATH条目必须要额外使用--enable-new-dtags(启用新动态标签)链接器选项。如果使用这个选项重建程序并且使用objdump查看获得的可执行文件,那么将会看到下面这样的输出。

$ gcc -g -Wall -o prog prog.c -Wl,--enable-new-dtags \
-Wl,-rpath,/home/mtk/pdir/d1 -L/home/mtk/pdir/d1 -lx1
$ objdump -p prog | grep PATH
RPATH           /home/mtk/pdir/d1
RUNPATH      /home/mtk/pdir/d1

从上面可以看出,可执行文件包含了DT_RPATH和 DT_RUNPATH标签。链接器采用这种方式复写了rpath列表是为了让不理解DT_RUNPATH标签的老式动态链接器能够正常工作。(glibc 2.2增加了对DT_RUNPATH的支持)。理解 DT_RUNPATH标签的链接器会忽略DT_RPATH标签(参见41.11节),

在rpath 中使用$ORIGIN -> turn-key应用程序

假设需要发布一个应用程序,这个应用程序使用了自身的共享库,但同时不希望强制要求用户将这些库安装在其中一个标准目录中,相反,需要允许用户将应用程序解压到任意异目录中,然后能够立即运行这个应用程序。这里存在的问题是应用程序无法确定存放共享库的位置,除非要求用户设置LD_LIBRARY_PATH或者要求用户运行某种能够标识出所需的目录的安装脚本,但这两种方法都不是令人满意的方法。
为解决这个问题,在构建链接器的时候增加了对rpath规范中特殊字符串$ORIGIN(或等价的${ORIGIN})的支持。动态链接器将这个字符串解释成“包合应用程序的目录”。这意味着可以使用下面的命令来构建应用程序。
$ gcc -W,-rpath,'$ORIGIN'/1ib ...
上面的命令假设在运行时应用程序的共享库位于包含应用程序的可执行文件的目录的子目录lib中,这样就能向用户提供一个简单的包含应用程序及相关的库的安装包,同时允许用户将这个包安装在任意位置并运行这个应用程序了(即所谓的“turn-key应用程序”)。

41.11 在运行时找出共享库

在解析库依赖时,动态链接器首先会检查各个依赖字符串以确定它是否包含斜线(/),因为在链接可执行文件时如果指定了一个显式的库路径名的话就会发生这种情况。如果找到了一个斜线,那么依赖字符串就会被解释成一个路径名(绝对路径名或相对路径名),并且会使用该路径名加载库。否则动态链接器会使用下面的规则来搜索共享库。
1.如果可执行文件的DT_RPATH 运行时库路径列表(rpath)中包含目录并且不包含DT_RUNPATH 列表,那么就搜索这些目录(按照链接程序时指定的目录顺序)。
2.如果定义了LD_LIBRARY_PATH环境变量,那么就会轮流搜索该变量值中以冒号分隔的各个目录。如果可执行文件是一个set-user-ID或 set-group-ID程序,那么就会忽略LD_LIBRARY_PATH 变量。这项安全措施是为了防止用户欺骗动态链接器让其加载一个与可执行文件所需的库的名称一样的私有库。
3.如果可执行文件 DT_RUNPATH运行时库路径列表中包含目录,那么就会搜索这些目录(按照链接程序时指定的目录顺序)。
4.检查/etc/ld.so.cache文件以确认它是否包含了与库相关的条目。
5.搜索/lib/usr/lib目录(按照这个顺序)。

41.12 运行时符号解析

(构建共享库时)-Bsymbolic 链接器选项指定了共享库中对全局符号的引用应该优先被绑定到库中的相应定义上(如果存在的话)。(注意不管是否使用了这个选项,在主程序中调用xyz()总是会调用主程序中定义的xyz()。

41.13使用静态库取代共享库

虽然在大多数情况下都应该使用共享库,但在某些场景中静态库则更加适合。特别地,静态链接的应用程序包含了它在运行时所需的全局代码这一事实是非常有利的。如当用户不希望感者无法在运行程序的系统上安装共享库或者程序在另一个无法使用共享库的环境中运行时(如可能是一个chroot 监狱(jail)),静态链接就派上用场了。此外,即使是一个兼容的共享库升级也可能会在无意中引入一个 Bug,从而导致应用程序无法正常工作。通过静态链接应用程就能确保系统上共享库的变动不会影响到它并具它已经拥有了运行所需的全局代码(付出的代价就是程序更大了,从而会需要更多的磁盘空间和内存)。
在默认情况下,当链接器能够选择名称一样的共享库和静态库时(如在链接时使用-Lsomedir -ldemo 并且 libdemo.solibdemo.a都存在)会优先使用共享库。要强制使用库的静态版本则可以完成下列之一。

  • 在gcc 命令行中指定静态库的路径名(包括a扩展)。
  • 在gcc命令行中指定-static选项。
  • 使用-Wl,-Bstatic-Wl,-Bdynamic gcc选项来显式地指定链接器选择共享库还是静态库。在gcc命令行中可以使用-l选项来混合这些选项。链接器会按照选项被指定时的顺序来处理这些选项。

41.14总结

目标库是一组编译过的目标模块的聚合,它可以用来与程序进行链接。与其他 UNIX实现一样,Linux提供了两种目标库:一种是静态库,在早期的UNIX系统中只存在这种库,还有一种是更加现代的共享库。
由于与静态库相比,共享库存在很多优势,因此在当代 UNIX系统上共享库用得最多。共享库的优势主要源自这样一个事实,即当一个程序与库进行链接时,程序所需的目标模块的副本不会被包含进结果可执行文件中。相反,(静态)链接器将会在可执行文件中添加与程序在运行时所需的共享库相关的信息。当文件被执行时,动态链接器会使用这些信息来加载所需的共享库。在运行时,所有使用同一共享库的程序共享该库在内存中的单个副本。由于共享库不会被复制到可执行文件中,并且在运行时所有程序都使用共享库在内存中的单个副本,因此共享库能够降低系统所需的磁盘空间和内存。
共享库soname 为在运行时接续共享库引用提供了一层间接。如果一个共享库拥有一个soname,那么在由静态链接器产生的可执行文件中将会记录这个soname,而不是库的真实名称。根据共享库命名规范,其真实名称的形式为libname.so.major-id.minor-id,其soname的形式为 libname.so.major-id。这种规范使得程序能够自动使用共享库的最新次要版本(无需重新链接程序),同时也允许创建库的新的不兼容的主要版本。
为了在运行时能够找到共享库,动态链接器遵循了一组标准的搜索规则,其中包括搜索一组大多数共享库安装的目录(如/lib/usr/lib)。

posted @ 2023-05-07 12:05  Theseus‘Ship  阅读(14)  评论(0编辑  收藏  举报
Live2D