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并将程序与正确的库文件连接起来。

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

41.2 静态库

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

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

创建和维护静态库

从结果上来看,静态库实际上就是一个保存所有被添加到其中的目标文件的副本的文件。这个归档文件还记录着每个目标文件的各种特性,包括文件权限,数字用户和组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)修饰符之后可以看到记录在归档文件中的各个目标文件的其他所有特性,如下面例子所示。
    $ ar tv libdemo.a
    rw-r–r-- 1000/100 1001016 Nov 15 12:26 2022 mod1.o
    rw-r–r-- 1000/100 400416 Nov 15 12:26 2022 mod2.o
    rw-r–r-- 1000/100 46672 Nov 15 12:26 2022 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 ldemo
如果库不在位于连接器搜索的目录中,那么可以只用-L选项指定链接器应该搜索这个额外的目录。
$ cc -g -o prog prog.o -Lmylibdir -ldemo
虽然一个静态库可以包含很多目标模块,但连接器只会包含那些程序需要的模块。在连接完程序之后,可以按照通常的方式运行这个程序
$ ./rpog
Called md1-x1
Called md2-x2

41.3 共享库概述

将程序与静态库链接起来时(或没有使用静态库),得到的可执行文件会包含所有被连接进程序的目标文件的副本。这样当几个不同的可执行程序使用了同样的目标模块时,每个可执行程序会拥有自己的目标模块的副本。这种代码的冗余存在几个缺点。

  • 存储同一个目标模块的多个副本会浪费磁盘空间,并且所浪费的空间是比较大的。
  • 如果几个使用了同一模块的程序在同一时刻运行,那么每个程序会独立的在虚拟内存中保存一份目标模块的副本,从而提高系统中虚拟内存的整体使用量。
  • 如果需要修改一个静态库中的一个目标模块(可能是因为安全性或需要修正bug)那么所有使用那个模块的可执行文件都必须重新进行链接以合并这个变更。这个缺点还会导致系统管理员要弄清哪些应用程序链接了这个库
    共享库就是设计用来解决这些缺点的。共享库的关键思想是目标模块的单个副本由所有需要这些模块的程序共享。目标模块不会被复制到连接过的可执行文件中,相反当地一个需要共享库的模块启动时,库的单个副本会在运行时被加载进内存,当后面使用共享库的其他程序启动时,他们会使用已经被加载进内存的库的副本,使用共享库意味着可执行程序需要的磁盘空间和虚拟内存(在运行的时候)更少了。
虽然共享库的代码是由多个进程共享的,但其中的变量却不是的。
每个使用库的进程会拥有自己的在库中定义的全局变量和静态变量的副本
  • 共享库还具备由于整个程序的大小变小了,因此在某些情况下程序完全可以被加载进内存中,从而能够更快的启动程序。这一点只有在大型共享库正在被其他程序使用的情况下才成立。第一个被加载共享库的程序在启动时实际上会花费更长的时间,因为必须要先找到共享库并将其加载到内存中。一下优势。

  • 由于目标文件没有被复制进可执行文件中,而是在共享库中维护的,因此在修改目标模块时无需重新链接程序就能够看到变更甚至在运行着的程序正在使用共享库的现有版本也能够进行这样的变更
    这项新增功能的主要开销如下所述:

  • 在概念上以及创建共享库和构建使用共享库的程序实践上,共享库比静态库更复杂

  • 共享库在编译时必须要使用位置独立的代码,这在大多数架构上都会带来性能开销,因为它需要额外的一个寄存器

  • 在运行时必须要之心符号重定位。在符号重定位期间,需要对共享库中每个符号(变量或者函数)的引用修改成符号在虚拟内存中的实际运行时位置。由于存在这个重定位的过程,与静态链接程序相比,一个使用共享库的成俗会多或少需要话费一些时间来执行这个过程

共享库的另一种用法就是作为Java NativeInterface(JNI)中的一个构
建块,它允许Java代码通过调用共享库中的函数直接访问底层操作系统的特性

41.4 创建和使用共享库----首回合

为了理解共享库的操作方式,下面介绍构建和使用一个共享库所需完成的最少步骤,在介绍的过程中会忽略平时使用的共享库命名规范,允许程序自动加载他们所需的共享库的最新版本同时也允许一个库的多个不兼容的版本和谐共存。
在本章中,我们只关心ELF(Executable and Link F
ormat)共享库,ELF格式取代了较早以前的a.out和COFF格式

41.4.1创建一个共享库

创建共享库版本需要执行下面步骤
$ gcc -g -fPIC -Wall mod1.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 mod1.c mod2.c mod3.c -shared -o libfoo.so
为了区分编译和构建两个步骤,本章一开始的例子中给出了两个独立的命令。
与静态库不同,可以像之前的构建的共享库添加单个目标模块,也可以从中删除单个目标模块。与普通的可执行文件一样,共享库的目标文件不再维护不同的身份。

41.4.2 为之独立的代码

cc -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 libraies:libfoo.so:can not open shared object file: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
Callled mod1-x1
Callled mod2-x2
上面的命令在执行prog的进程中创建了一个环境变量定义。这个定义告诉动态链接器在 .,即在当前目录中搜索共享库。

静态链接和动态链接的比较

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

41.4.4 共享库 soname

到目前为止介绍的所有例子中,嵌入到可执行文件以及动态链接器在运行时搜索的名称是共享库的实际名称,这被称为库的真实名称(real name).但可以–实际上经常这样做—使用别名来创建共享库,这种别名成为soname(ELF 中的DT_SONAME标签)。
如果共享库拥有一个soname,那么静态链接阶段会将soname嵌入到可执行文件中,而不会使用真实名称,同时后面的动态链接器在运行时也会使用这个soname来搜索库,提供soname的目的是为了提供一层间接,使得可执行程序在运行时使用与连接时使用的库不同(但是可兼容的)。
使用soname的第一步是在创建共享库时指定soname。
$gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
g c c − g s h a r e d − W l , − s o n a m e , l i b b a r . s o − o l i b f o o . s o m o d 1. o m o d 2. o m o d 3. o ∗ ∗ ∗ − W l − s o n a m e 以及 l i b b a r . s o 选项是传给连接器的指令以及将共享库 l i b f o o . s o 的 s o n a m e 设置为 l i b b a r . s o 如果要确定一个既有共享库的 s o n a m e , 那么可以使用下面两个命令中的任意一个。 ∗ ∗ ∗ ∗ 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,那么可以使用下面两个命令中的任意一个。 **** gccgsharedWl,soname,libbar.soolibfoo.somod1.omod2.omod3.oWlsoname以及libbar.so选项是传给连接器的指令以及将共享库libfoo.sosoname设置为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
但是这次连接器检查到库libfooo.so包含了soname libbar.so.于是将这个soname嵌入到了
可执行文件中。现在当运行这个程序时会看到以下输出,

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

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

$ln -s libfoo.so libbar.so
LD_LIBRARY_PATH=. ./prog
Called mod1-x1
Called mod2-x2
***==还要建立符号链接才能使用,目前还不太清楚这个soname的作用==***

下图给出了在使用一个内嵌的soname,将程序与共享库连接起来,以及创建运行程序所需的soname符号链接时所涉及到的编译选项和链接事项。创建一个共享库并将一个程序与该共享库连接起来
下图给出了创建程序被加载进内存以备执行时发生的事情
加载共享库的程序执行

41.5 使用共享库的有用工具

本节简要介绍对分析共享库、可执行文件以及编译过的目标文件(.o)有用的一组工具

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命令会解析出每个库引用(使用的搜索方式和动态链接器一样)
duiyu大多数ELF可执行文件来讲,ldd至少会列出与ld-linux.so.2、动态链接器以及标准C库libc.so.6相关条目

objdump与readelf命令

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

nm命令

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

$ 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必须要使用一种标准的命名规范。

真实名称、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.1.0.2
libdemo.so.2 -> libdemo.2.0.0
libreadline.so.5 -> lireadline.so.5.0

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

除了真实名称和soname之外,通常还会为每个共享库定义第三个名称:连接器名称,将可执行文件与共享库连接起来时会用到这个名称。连接器名称是一个只包含库名同时不包含主要或次要版本标识符的符号链接,因此形式为libname.so。有了连接器名称之后就可以构建能够自动使用共享库的正确版本(即最新版本)的独立于版本的链接命令了

一般来讲,连接器名称与它所引用的文件位于同一个目录中,它既可以连接到真实名称,也可以连接到库的最新主要版本的soname。通常最好使用指向soname的链接,因此对soname 所做的变更会自动反映到连接器名称上(41.7节会看到ldcnfig程序将保持soname最新的任务自动化了,因此如果使用了刚才介绍的规范的话,就是隐式地维护连接器名称。)

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

下面试一下连接器名称的例子。

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

下表对共享库的真实民称、soname以及连接器名称进行了总结
共享库名称的命名规范
共享库名称命名规范

使用标准命名规范创建一个共享库

根据上面介绍知识,下面介绍如何遵循标准规范来构建一个演示库。首先需要创建目标文件。

$ gcc -g -c fPIC Wall nmod1.c mod2.c mod3.c
接着创建共享库,真实姓名为libdemo.so.1.0.1 soname为 libdemo.so.1
$ gcc -g -shared -W1,-soname,libdemo.so.1 -o libdemo.so.1.0.1 \
mmod1.o mod2.o mod3.o

//接着为soname 和连接器名称创建切当的符号链接。
$ln -s libdemo.so.1.0.1 libdemo.so.1
ln -s lidemo.so.1 libdemo.so
//接着可以使用ls 来验证配置
$ls libdemo.so*
//接着可以使用连接器名称来构建可执行文件(注意链接命令不会用到版本号),并且照常运行这个程序
gcc  -g -Wall -o prog prog.c -L. -ldemo
LD_LIBRARY_PATH=. ./prog
Called mod1-x1
Called mod2-x2

41.7 安装共享库

在本章目前位置所介绍的例子中都是将共享库目录常见在四哟的目录中,然后使用LD_LIBRARY_PATH环境变量来确保动态链接器会搜索到该目录。特权用户和非特权用户都可以使用这种技术,但是在生产应用中不应该采用这种技术。一般来讲,共享库及其关联的符号链接会被安装在一个标准库目录中,标准库目录包括:

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

shell 回话中最后两行创建了soname和连接器的符号链接

ldconfig

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

  • 共享库可以位于各个目录中,如果如果动态连接器需要通过搜索所有这些目录来找出一个库并加载这个库,那么整个郭恒将非常慢。
  • 当安装了新版本的库或者删除了旧版本的库,那么soname的符号链接就不是最新的,

ldconfgi通过执行两个任务来解决这些问题
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
下面是一个使用ldconfig的例子假设需要安装一个库的两个不同的主要版本,那么需要执行以下步骤:

# mv libdemo.so.1.0.1 libdemo.so.2.0.0 /usr/lib
#ldconfig | grep libdemo
    libdemo.so.1->  libdemo.so.1.0.1 (changed)
    libdemo.so.2->  libdemo.so.2.0.0 (changed)
接着列出/usr/lib 目录中名为lidemo的文件来验证soname符号链接的设置。
# cd /usr/lib
ls -l libdemo* | awk '{print $1,$9,$10,$11}'
还需要为连接器民称创建符号链接如下面:
#ln -s libdemo.so.2 libdemo.so

如果安装了一个新的2.x的次要版本,那么由于连接器名称指向了最新的soname,
因此ldconfig还能取得保持连接器名称的最新效果。
#mv libdemo.so.2.0.1 /usr/lib
# ldconfig -v | grep libdemo
	libdemo.so.1 ->libdemo.so.1.0.1
	libdemo.so.2 ->libdemo.so.2.0.1(changed)

如果创建和使用的是一个私有库(即没有安装在上述标准目录中的库),那么可以通过使用==-n==选项让ldconfig创建soname符号链接。这个选项指定了ldconfig只处理在命令行中列出的目录中的库,而无需更新u缓存文件。下面的例子使用了ldconfig来处理当前工作目录中的库。

$ gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
$ gcc -g -c -shared -Wl,-soname,libdemo.so.1 -o libdemo.so.1.0.1 \
 	mod1.o mod2.o mod3.o
$ /sbin/ldconfig -nv .
.:
	libdemo.so.1 -> libdemo.so.1.0.1

41. 8 兼容与不兼容库比较

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

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

41.9 升级共享库

共享库的有点之一是当一个运行着的程序正在使用一个既有共享库时,也能够安装库的新主要版本或次要版本。在安装过程中需要做的事情包括创建新的库版本,==将其安装在恰当的目录中以及根据需要更新soname和链接器名称符号链接(通常让ldconfig完成这项工作)。==如果要创建共享库的/usr/lib/libdemo.1.01的一个新次要版本,那么需要完成:

# gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
# gcc -g -shared -Wl,-soname,libdemo.so.1 -o  libdemo.so.1.0.2\
          mod1.o mod2.o mod3.o
#mv libdemo.so.1.0.2 /usr/lib
# ldconfig -v | grep libdemo
	libdemo.so.1 -> libdemo.so.1.0.2(changed)

假设已经正确配置了链接器名称(指向库的soname),那么就无需修改链接器名称了

已经运行着的程序会继续使用共享库的上一个次要版本,只有当他们终止或重启之后才会使用共享库的新催要版本
如果后面要创建共享库的新主要版本(2.0.0),那么就需要完成:

# gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
# gcc -g -shared -W1,-soname,libdemo.so.2 -o libdemo.2.0.0 \
        mod1.o mod2.o mod3.o
#mv libdemo.so.2.0.0 /usr/lib
#ldconfig -v | grep libdemo
	libdemo.so.1 -> libdemo.so.1.0.2
	libdemo.so.2 -> libdemo.so.2.0.0(changed)
#cd /usr/lib
#ln -sf libdemo.so.2 libdemo.so

从上面的输出可以看出,ldconfig自动为新主要版本创建了一个soname 符号链接,但是必须手动更新链接器名称的符号链接。

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

到目前位置本章已经介绍了两种通知动态链接器共享库位置的方式:使用LD_LIBRARY_PATH环境变量和将共享库安装到一个标准库目录中(/lib /usr/lib 或在ld.so.conf中列出的其中一个目录)。
还存在第三种方式:在静态编译阶段可以在可执行文件中插入一个在运行时搜索共享库的目录列表。这种方式对于库位于一个固定的位置的但不属于动态链接器搜索的标准位置的位置中时是非常有用的。要实现这种方式主要在创建可执行文件时使用-rpath连接器选项。

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

在构建共享库时使用 -rpath连接器选项

在构建共享库时-rpath选项也是有用的。假设有一个依赖于另一个共享库libx2.so的共享库libx1.so,如下图所示。另外再假设这些库分别位于非标准目录d1和d2中。下面介绍构建这些库以及使用他们的程序所需完成的步骤。
依赖与另一个库的共享库
首先在pdir/d2目录中构建libx.so。(省略库版本以及soname)

$ cd /home/mtk/pdir/d2
$ gcc -g -c -fPIC -Wall modx2.c
$ gcc -g -shared -o libx2.so modx2.o

接着在pdir/d1目录中构建libx1.so.由于libx2.so并且libx2.so位于一个非标准目录中,因此在指定libx2.so的运行位置时需要使用-rpath连接器选项。这个选项的取值与链接时位置(由 -L选项指定)可以不同,尽管在这个例子中两个位置时相同的。

$ cd /home/mtk/pdir/d1
$ gcc -g -c -Wall -fPIC modx1.c
$ gcc -g -shared -o libx1.so modx1.o,-rpath,/home/mtk/pdir/d2 \
							-L/home/mtk/pdir/d2 -lx2

最后在pdir目录中构建主程序,由于主程序使用了libx1.so.并且这个库位于一个非标准目录中,因此还需要使用-rpath连接器选项。

$ cd /home/mtk/pdir
$ gcc -g -Wall -o prog prog.c -W1,-rpath,/home/mtk/pdir/d1 \
				-L/home/mtk/pdir/d1 -lx1

注意在连接时无需指定libx2.so.由于连接器能够分析libx1.so中的rpath列表,因此它能够找到libx2.so,同时在静态连接阶段解析出所有的符号。
使用下面命令能够检查prog和libx1.so以便查看他们的rpath列表的内容

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

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

ELF DT_RPATH和DT_RUNPATH条目

在第一班ELF规范中,只有一种rpath列表能够被嵌入到可执行文件或共享库中,它对应于ELF文件中的DT_RPATH标签。后续的ELF规范舍弃了DT_RPATH,同时引入了一种新标签DT_RUNPATH来表示rpath列表。这两种rpath列表之间的差别在于当动态链接器在运行时搜索共享库时他们相对于LD_LIBRARY_PATH环境变量的优先级:DT_RPATH的优先级更高,而DT_RUNPATH的优先级更低。
在默认情况下,连接器会将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标签。连接器采用这种方式复写lerpath列表是为了让不理解DT_RUNPATH标签的老式动态链接器能够正常工作。理解DT_RUNPATH标签的动态链接器会忽略DT_RPATH标签。

在rpath中使用$ORIGIN

假设需要发布一个应用程序,这个应用程序使用了自身的共享库,但是同时不希望强制要求用户将这些库安装在其中一个标准目录中,相反,需要允许用户将应用程序解压到任意目录中,然后立即运行这个程序。这里存在的问题是应用程序无法确定存放共享库 的位置除非要求用户设置LD_LIBRARY_PATH或者要求用户运行某种能够表示出所需的目录的安装脚本,但这两种方法都不是令人满意的方法。
为解决这个问题,在构建连接器的时候增加了对rpath规范字符串==$ORGIN==(或等价的$ORGIN)的支持,动态字符串将这个字符串解释成“包含应用程序的目录”。这意味着可以使用下面的命令来构建应用程序

$ gcc -Wl,-rpath,'$ORIGIN'/lib...

上面的命令假设在运行时应用程序的共享库位于包含应用程序的可执行文件的目录的子目录lib中。这样就能向用户提供一个简单的包含应用程序以及库的安装包,同时允许用户将这个安装包安装在任意位置并运行这个应用程序了。

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运行时解析符号

假设在多个地方定义了一个全局符号(即函数或变量),如在一个可执行文件和一个共享文件库中火灾多个共享库中,该如何解析指向这个符号的引用呢?
假设现在由一个程序和一个共享库,他们两个都定义了y一个全局函数xyz(),并且共享库中的另一个函数调用了xyz(),如下图所示:
解析全局符号引用
在构建可执行程序和共享库并运行这个程序之后能够看到下面的输出。

$ gcc -g -c -fPIC -Wall -c foo.c
$ gcc -g -shared -o libsoo.so  foo.o
$ gcc -g -o prog prog.c libfoo.so
$ LD_LIBRARY_PATH=. ./prog
main-xyz

从上面输出的最后一行可以看出,主程序中的xyz()定义覆盖(优先)了共享库中的定义
尽管这种处理方式在一开始看起来令人惊讶,但是这样做是有历史原因的。第一个共享库实现在设计时的目标是使符号解析的默认语义与那些和同一库等价的静态库进行连接的应用程序中的符号解析的语义完成一致。这意味着下面的而予以是正确的。

  • 主程序中全局符号的定义覆盖库中相应的定义。
  • 如果一个全局符号在多个库中进行了定义,那么对该符号的引用会被绑定到在扫描库中列出时从左至右的顺序。
    虽然这些语义使得从静态库到共享库的转变变得简单了,但这种做法会导致一些问题。其中最大的问题是在使用这些共享库实现一个自包含的子系统时会与共享库模型产生矛盾,在默认的情况下,共享库无法确保一个指向其自身的摸个全局符号的引用会被真正被绑定到该符号在库中的定义上,从而导致当该共享库被集成到一个更大的系统中时共享库的属性可能会发生改变。这会导致应用程序会出现令人意料之外的行为。
    在上面的例子中,如果想要确保在共享库中对xyz的调用确实调用了库定义中的相应函数,那么在构建共享库的时候就需要使用-Bsymbolic连接器选项。
$ gcc -g -c -fPIC -Wall -c -foo.c
$ gcc -g -shared -Wl, -Bsymbolic -o libfoo.so foo.o
$ gcc -g -o prog prog.c libfoo.so
$ LD_LIBRARY_PATH=. ./prog
foo-xyz

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

41.13 使用静态库取代共享库

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

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

41.14 总结

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值