共享库是一种将库函数打包成一个单元使之能够在运行时被多个进程共享的技术。
这种技术能够节省磁盘空间和RAM。
1.目标库
链接器实际上是由一个单独的链接器程序ld来完成的。当使用cc(或gcc)命令链接
一个程序时,编译器会在幕后调用ld.在Linux上总是应该通过gcc间接的调用链接器,
因为gcc能够确保使用正确的选项来调用ld并将程序与正确的库文件链接起来。
很多情况下,源代码文件也可以被多个程序共享。因此要降低工作量的第一步就是
将这些源码文件只编译一次,然后在需要的时候将他们链接进不同的可执行文件。其
缺点是在链接的时候仍然需要为所有目标文件命名。
为解决这一问题,可以将一组目标文件组织成一个被称为对象库的单元。对象库
分为2种:静态的和动态的。
2.静态库
静态库也被称为归档文件,好处:
1.可以将一组经常被用到的目标文件组织进单个库文件,这样就可以使用它来构建
多个可执行程序并且在构建各个应用的时候无需重新编译原来的源代码文件。
2.链接命令变得简单了。在链接命令行中只需要指定静态库的名称即可,而无需一个
个的列出目标文件了。链接器知道如何搜索静态库并将可执行程序需要的对象抽取
出来。
创建和维护静态库:
从结果上说,静态库实际上就是一个保存所有被添加到其中的目标文件的副本文件。
这个归档文件还记录着每个目标文件的各种特性,包括文件的权限,数字用户,组ID以及
最后修改时间。根据惯例,静态库的名称形式为 libname.a 。
使用 ar(1) 命令能够创建和维护静态库 :
ar options archive object-file ...
options 参数是由一些列的字母构成,其中一个是操作代码,其他是能够影响操作的执行的
修饰符。下面是常用的操作码:
1. r(替换)
2. t(目录表)
3. d(删除)
使用静态库:
将静态库与程序链接起来的方式有2种,
1.在链接命令中指定静态库的名称,如
gcc -g -o prog prog.o libdemo.a
2.将静态库放在链接器搜索的其中一个标准目录中(如 /usr/lib) ,然后使用 -l选项
指定库名(即库的文件名出去了 lib 前缀和 .a 后缀)
gcc -g -o prog prog.o -ldemo
如果库不在链接器搜索的目录中,那么可以只要 -L 选项指定链接器应该搜索额外的目录
gcc -g -o prog prog.o -L目录 -ldemo
3.共享库
将程序与静态库链接起来,得到的可执行文件会包含所有被链接进程序的目标文件的副本。
当有几个不同的可执行程序使用了同样的目标模块时,每个可执行程序都会拥有自己的目标
目标模块的副本。这种代码冗余有几个缺点:
1.多个副本会浪费磁盘空间
2.每个程序会独立的在虚拟内存中保存一份目标模块的副本,从而提高了系统在虚拟内存的整体
使用量
3.如果要修改静态库中的一个目标模块,那么所有使用那个模块的可执行程序都必须要重新进行
链接以合并这个变更。
共享库就是设计来解决这些缺点的。共享库的关键思想是目标模块的单个副本由所有需要这些
模块的程序共享。目标模块不会被链接进可执行程序中。相反,当第一个需要共享库中的模块的
程序启动的时候,库的单个副本就会在运行时被加载进内存。而后面使用同一共享库的其他程序
启动时,它们会使用已经被加载进内存的库的副本。
还有以下其他优势:
1.由于整体程序的大小变小了,因此在一些情况下,程序可以完全被加载进内存中从而更快的
启动程序。第一个加载共享库的程序实际上在启动时会花费更多的时间。
2.由于目标模块没有被复制进可执行文件中,而是在共享库中集中维护,因此修改目标模块时,
无需重新链接程序就能看到变更。
缺点:
1.共享库比静态库更复杂
2.共享库在编译时必须要使用位置独立的代码,这在大多数架构上都会有性能开销,因为它额外
需要一个寄存器。
3.在运行时必须要执符号重定位。在符号重定位期间,需要将对共享库中的每个符号(变量或者函数)
的引用修改成符号在虚拟内存中的实际运行时位置。由于存在这个重定位的过程,与静态库相比,
一个使用共享库的程序或多或少需要花费一些时间执行这个过程。
4.创建一个共性库
gcc -g -c -fPIC -Wall a.c b.c c.c
gcc -g -shared -o libfoo.so a.o b.o c.o
-shared 命令创建了一个包含目标模块的共享库。
根据惯例,共享库的前缀为 lib,后缀为 .so(shared object)
5.位置独立的代码:
-fPIC 选项指定编译器应该生成位置独立的代码,这会改变编译器生成执行特定操作的代码的方式。
包括访问全局,静态和外部变量,访问字符串常量,以及获取函数的地址。这些变更使得代码可以在运行时
被放置在任意一个虚地址处。这一点对于共享库来讲是必须的,因为在链接的时候是无法知道共享库代码
位于内存的何处的。(一个共享库在运行时所处的内存位置依赖于很多因素,如加载这个库的程序已经占用
的内存数量和这个程序已经加载的其他共享库。)
为了确定一个既有目标文件在编译时是否使用了 -fPIC 选项,可以使用如下命令:
nm mod1.o | grep _GLOBAL_OFFSET_TABLE_
readelf -s mod1.o | grep _GLOBAL_OFFSET_TABLE_
6.使用共享库
使用一个共享库需要做2件事情,而使用静态库则不用:
1.由于可执行文件不再包含它所需的目标文件的副本,因此它必须要通过某种机制找出在运行时所需的
共享库。这是通过在链接阶段将共享库的名称嵌入可执行文件中来完成的。(在 ELF中,库依赖性是记录
在可执行文件的 DT_NEEDED 标签中的)一个程序所依赖的所有共享库列表被称为程序的动态依赖列表。
在运行时必须要存在某种机制解析嵌入的库名---即找出与在可执行文件中指定的名称对应的共享库文件--
接着如果库不在内存中的话就将库加载进内存。
将程序与共享库链接起来时会自动将库的名字嵌入可执行文件中。
2.动态链接,即在运行时解析内嵌的库名。这个任务是由动态链接器(也称为动态加载器或运行时链接器)来完成
的。动态链接器本身也是一个共享库,其名称为 /lib/ld-linux.so.2, 所有使用共享库 ELF 可执行文件都会
用到这个共享库。
路径名 /lib/ld-linux.so.2 通常是一个指向动态链接器的可执行文件的符号链接。这个文件的名称为 ld-version.so
,其中 version 表示安装在系统上的 glibc 的版本---如 ld.211.so。
动态链接器会检查程序所需的共享库清单并使用一组预先定义好的规则来在文件系统上找出相关的库文件。其中
一些规则则指定了一组存放共享库的标准目录。如很多共享库位于 /lib 和 /usr/lib。
LD_LIBRARY_PATH 环境变量:
通知动态链接器一个共享库位于一个非标志的目录中的一种方法是将该目录添加到 LD_LIBRARY_PATH 环境变量中以
分号分割的目录列表中。如果定义了LD_LIBRARY_PATH,那么动态链接器在查找标准目录之前会先查找该环境变量列出
的目录中的共享库。
静态链接和动态链接比较:
通常,术语链接用来表示链接器ld将一个或者多个编译过的目标文件组成一个可执行文件。有时候会使用术语静态
链接从动态链接中将运行时加载可执行文件所需的共享库这一步骤区分开来。(静态链接有时也被称为链接编辑,像ld
这样的静态链接器有时候被称为链接编辑器)。每个程序---包括那些使用共享库的程序---都会经历一个静态链接的阶段。
在运行时,使用共享库的程序会经历额外的动态链接阶段。
7.共享库别名 soname
到目前为止介绍的所有例子中,嵌入到可执行文件以及动态链接器在运行时搜索的名称是共享库文件的实际名称,
这被称为真实名称(real name)。但可以---实际上经常这样做---使用别名来创建共享库,这种别名称为 soname(ELF
中的 DT_SONAME标签)。
如果共享库拥有一个 soname, 那么在静态链接阶段会将 soname 嵌入到可执行文件中,而不会使用真实名称,同时后面的
动态链接器子啊运行时也会使用这个 soname 来搜索库。引入 soname 的目的是为了提供一层更间接,使得可执行程序能够在
运行时使用与链接时使用的库不同(但兼容)的共享库。
gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
gcc -g -shared -W1,-soname,libbar.so -o libfoo.so mod1.o mod2.o mod3.o
-W1,-soname,以及 libbar.so 选项是传给链接器的指令将共享库 libfoo.so 的soname 设置为 libbar.so
如果要确定一个既有共享库 soname,可以使用如下命令:
objdump -p libfoo.so | grep SONAME
readelf -d libfoo.so | grep SONAME
动态链接器无法找到名为 libbar.so 共享库。当使用 soname 时,还必须做一件事:必须要创建一个符号链接
将 soname 指向库的真实名称,并且必须要将这个符号链接放在动态链接搜索的其中一个目录中。
8.使用共享库的有用工具
ldd(1) : 列出动态依赖显示了一个运行所需的共享库
objdump : 能够获取各类信息---包括反汇编的二进制机器码---从一个可执行文件,编译过的目标以及共享库中。它还能够显示这些文件
中各个 ELF 节的头部信息。
nm : 会列出模板库或者可执行程序中定义的一组符号。这个命令的一种用途是找出哪些库定义了一个符号。
9.共享库版本和命名规则
一般来说,一个共享库互相连续的两个版本是互相兼容的,这意味着每个模块中的函数对外呈现的调用接口是一致的,并且函数语义是等价的(
即它们能取得同样的结果)。这种版本号不同但互相兼容的版本称为共享库的次要版本。但有时候需要创建一个库的新主版本---即与上一个版本
不兼容的版本。
真实名称,soanme,以及链接器名称
共享库的每一个不兼容版本是通过一个唯一的主要版本标识符来区分的,这个主要版本标识符是共享库的真实名称的一部分。根据惯例,主要版本
标识符由一个数字构成,这个数字是随着库的每个不兼容版本的发布而顺序递增。除了主要版本标识符之外,真实名称还包含一个次要版本标识符,它
是用来区分库的主要版本中兼容的次要版本。真实名称的格式为 libname.so.major-id.minor-id。
与主要版本标识符一样,次要版本标识符可以是任意字符串。但根据惯例,它要么是一个数字,要么是由2个点分割的数字,其中第一个数字标识了
次要版本,第二个数字标识该次要版本中的补丁号或修正号。
libdemo.so.1.0.1
共享库的soname包括相应的真实名称中的主要版本标识符,但不包括次要版本标识符。因此,soname的形式为 libname.so.major-id。
通常,会将 soname 创建为包含真实名称的目录中的一个相对符号链接。下面是一些 soname 的例子,
libdemo.so.1 -> libdemo.so.1.0.2
对共享库的某个特定的主要版本来说,可能存在几个库文件,这些库文件是通过不同的次要版本标识符来区分的。通常,每个库的主要版本的 soname
会指向在主要版本中的最新的次要版本。这种配置使得在共享库的运行时操作期间版本化语义能够正确工作。由于静态链接阶段会将 soname 的副本嵌入
到可执行文件中并且 soname 符号链接后面可能会被修改指向一个更新的(次要)版本的共享库,因此可以确保可执行文件在运行时能够加载库的最新的次要
版本。此外,由于一个库不同的主要版本的 soname 不同,因此它们能够和平的共存并且被需要它们的程序访问。
除了真实名称和 soname 之外,通常还会为每个共享库定义第三个名称:链接器名称,将可执行文件与共享库链接起来时会用到这个名称。链接器名称是一个
只包含库名同时不包含主要或次要版本标识符的符号链接,因此其形式为 libname.so 。有了链接器名称后就可以构建能够自动使用共享库的正确版本(即最新版本)
的独立于版本的链接命令了。
一般来说,链接器名称与它所引用的文件位于同一目录中,它既可以链接到真实名称,也可以链接到库的最新主要版本的 soname。通常,最好使用指向 soname 的连接,
因此对 soname 所做的变更会自动反应到链接器名称上。
libdemo.so ->libdemo.so.2
使用标准规范创建一个共享库:
1.gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
//接着创建共享库,真实名称为 libdemo.so.1.0.1, soname 为 libdemo.so.1
2.gcc -g -shared -W1,-soname,-libdemo.so.1 -o libdemo.so.1.0.1 mod1.o mod2.o mod3.o
//接着为 soname 和链接器名称创建恰当的符号链接
3.ln -s libdemo.so.1.0.1 libdemo.so.1
ln -s libdemo.so.1 libdemo.so
//接着可以使用 ls 来验证配置
4.ls -l libmode.so* | aws '{print $1, $9, $10, $11}'
//接着可以使用链接器名称来构建可执行文件
5.gcc -g -Wall -o prog prog.c -L. -ldemo
10.安装共享库
一般来说,共享库及其关联的符号链接会被安装到其中一个标准库目录中,标准库目录包括:
1./usr/lib, 大多数标准库安装目录
2./lib, 应该将系统启动时用到的库安装在这个目录中(因为在系统启动时可能还没挂载/usr/lib)
3./usr/local/lib,应该将非标准或实验性的库安装在这个目录中(对于 /usr/lib 是一个由多个系统共享的网络挂载但需要只在本机安装一个库的情况则可以将库房在这个目录中)
4.其中一个在 /etc/ld.so.conf 中列出的目录
ldconfig:
ldconfig(8)解决了共享库的两个潜在问题:
1.共享库可以位于各种目录中,如果动态链接器需要通过搜索所有这些目录找出一个库并加载这个库,那么整个过程将非常慢
2.当按照了新版本的库或者删除了旧版本的库,那么 soname 符号链接就不是最新的。
ldconfig 通过执行2个任务来解决这些问题。
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 选项会使得 ldconfig 输出描述其所执行的动作信息
每当安装了一个新的,或者删除了一个既有的库,以及 /etc/ld.so.conf 修改后,都应该重新运行 ldconfig
11.在目标文件中指定库搜索目录
到目前为止已经介绍了2种通知动态链接器共享库的位置的方式:
1.使用 LD_LIBRARY_PATH 环境变量
2.将共享库安装到其中一个标准库目录中(/lib,/usr/lib,或者 /etc/ld.so.conf 中列出其中的一个目录)
3.在静态编译阶段可以在可执行文件中插入一个在运行时搜索共享库的目录列表.这种方式对于库位于一个固定但不属于动态链接器搜索的标准位置的位置中时,非常有用。
要实现这种方式需要在创建可执行文件时使用 -rpath 链接器选项。
gcc -g -Wall -W1,-rpaht,/home/mtk/pdir -o prog prog.c libdemo.so
命令:
ar
nm
readelf
objdump
ldd //列出动态依赖