【链接装载与库】 Linux共享库的组织

Linux共享库的组织

由于动态链接的诸多优点,大量的程序开始使用动态链接机制,导致系统里面存在数量 极为庞大的共享对象。如果没有很好的方法将这些共享对象组织起来,整个系统中的共享对象文件则会散落在各个目录下,给长期的维护、升级造成了很大的问题。所以操作系统一般会对共享对象的目录组织和使用方法有一定的规则,我们将在这一章介绍Linux 下共享库的管理问题。

共享库版本

  • 共享库兼容性

共享库的开发者会不停地更新共享库的版本,以修正原有的Bug、 增加新的功能或改进性能等。由于动态链接的灵活性,使得程序本身和程序所依赖的共享库可以分别独立开发和更新, 但是共享库版本的更新可能会导致接口的更改或删除,这可能导致依赖于该共享库的程序无法正常运行。最简单的情况下,共享库的更新可以被分为两类。

  1. 兼容更新。 所有的更新只是在原有的共享库基础上添加一些内容,所有原有的接口都 保持不变。
  2. 不兼容更新。 共享库更新改变了原有的接口,使用该共享库原有接口的程序可能不能 运行或运行不正常。

导致 C 语言的共享库ABI 改变的行为主要有如下4个

  1. 导出函数的行为发生改变,也就是说调用这个函数以后产生的结果与以前不一样。
  2. 导出函数被删除。
  3. 导出数据的结构发生变化,比如共享库定义的结构体变量的结构发生改变:结构成员 删除、顺序改变或其他引起结构体内存布局变化的行为
  4. 导出函数的接口发生变化,如函数返回值、参数被更改。
  • 共享库版本命名

Linux 有一套规则来命名系统中的每一 个共享库,它规定共享库的文件名规则必须如下:

libname.so.x.y.z

最前面使用前缀“lib"、 中间是库的名字和后缀“.so”,最后面跟着的是三个数字组成 的版本号。“x”表示主版本号,“y”表示次版本号,“z” 表示发布版本号。 三个版本号的含义不一样。

主版本号表示库的重大升级,不同主版本号的库之间是不兼容的,依赖于旧的主版本号 的程序需要改动相应的部分,并且重新编译,才可以在新版的共享库中运行
次版本号表示库的增量升级,即增加一些新的接口符号,且保持原来的符号不变。在主 版本号相同的情况下,高的次版本号的库向后兼容低的次版本号的库。
发布版本号表示库的一些错误的修正、性能的改进等,并不添加任何新的接口,也不对接口进行更改。

  • SO-NAME

共享库的主版本号和次版本号决定了一个共享库的接口。动态链接器怎样知道程序依赖于哪些共享库,它们的版本号又是什么?

Solaris 和Linux, 普遍采用一种叫做SO-NAME的命名机制来记录共享库的依赖关系。每个共享库都有一个对应的“SO-NAME”, 这个 SO-NAME即共享库的文件名去掉次版本号和发布版本号,保留主版本号。
系统会为每个共享库在它所在的目录创建一个跟 “SO-NAME” 相同的并且指向它的软链接

那么以“SO-NAME”为名字建立软链接有什么用处呢?

实际上这个软链接会指向目录 中主版本号相同、次版本号和发布版本号最新的共享库。

建立以SO-NAME为名字的软链接目的是,使得所有依赖某个共享库的模块,在编译、 链接和运行时,都使用共享库的SO-NAME,而不使用详细的版本号。
编译输出ELF文件时,将被依赖的共享库的SO-NAME保存到“.dynamic”中,这样当动态链接器进行共享库依赖文件查找时,就会根据系统中各种共享库目录中的SO-NAME软链接自动定向到最新版本的共享库。

Linux 中提供了一个工具叫做“ldconfig”, 当系统中安装或更新一个共享库时,就需要运行这个工具,它会遍历所有的默认共享库目录,比如/ib、/usr/ib 等,然后更新所有的软 链接,使它们指向最新版的共享库;如果安装了新的共享库,那么ldconfig会为其创建相应 的软链接。

符号版本

如果某个共享库在系统中存在相同主版本号不同次版本号的多个副本,那么动态链接器会使用那个最高次版本号的副本。如果找到的共享库次版本号低于所需要的版本, SunOS 4.x系统的策略是向用户发 出一个警告信息,表示系统中仅有低次版本号的共享库,但运行程序还是继续运行。有些采取更加保守策略的系统中,对于 这种系统中没有足够高的次版本号满足依赖关系的情况,程序将会被禁止运行,以防止出现意外情况。在采取第二种策略的系统中,如果系统中 只有低次版本号的共享库,那么这些程序就不能运行。我们可以把这个问题叫做次版本号交会问题

这种次版本号交会问题并没有因为SO-NAME 的存在而得到任何改善。 对于这个问题,现代的系统通过一种更加精巧的方式来解决,那就是符号版本机制。

  • 基于符号的版本机制

Linux 下的Glibc 从版本2.1之后开始支持一种叫做基于符合的版本机制的方案。这个方案的基本思路是让每个导出和导入的符号都有一个相关联的版 本号,它的实际做法类似于名称修饰的方法。

当我们将libfoo.so.1.2升级至1.3时,仍然 保持 libfoo.so.1 这个 SO-NAME, 但是给在1.3这个新版中添加的那些全局符号打上一个标 记,比如“VERS 1.3”。那么,如果一个共享库每一次次版本号升级,我们都能给那些在新 的次版本号中添加的全局符号打上相应的标记,就可以清楚地看到共享库中的每个符号都拥 有相应的标签,比如“VERS 1.1”、“VERS 1.2”、“VERS 1.3”、“VERS 1.4”。

  • Solaris 中的符号版本机制

Solaris 的ld链接器为共享库新增了版本机制和范围机制

版本机制的想法很简单,也就是定义一些符号的集合,这些集合本身都有名字,比如叫 “VERS 1.1”、“VERS 1.2”等,每个集合都包含一些指定的符号,除了可以拥有符号以外, 一个集合还可以包含另外一个集合,比如 “VERS 1.2”可以包含集合“VERS 1.1”。

在 Solaris 中,程序员可 以在链接共享库时编写一种叫做符号版本脚本的文件。链接器在链接时根据符号版本脚本中指定的关系来产 生共享库,并且设置符号的集合与它们之间的关系。

当共享库的符号都有了版本集合之后, 一个最明显的效果就是,当我们在构建(编译和 链接)应用程序的时候,链接器可以在程序的最终输出文件中记录下它所用到的版本符号集 合。

  • GCC 对 Solaris 符号版本机制的扩展

GCC 还允许使用一个叫做“.symver”的汇编宏指令来指定符号的版本,这个汇编宏指令可以被用在GAS 汇编中,也可以在GCC的C/C++源代码中以嵌入汇编指令的模式使用。

asm(".symver add,addeVERS_1.1");
  • Linux系统中符号版本机制实践

在 Linux下,当我们使用ld 链接一个共享库时,可以使用“-version-script”参数;如 果使用GCC, 则可以使用“-Xlinker”参数加“-version-script", 相当于把“-version-script” 传递给ld链接器。如编译源代码为“lib.c”, 符号版本脚本文件为"lib.ver":

gcc -shared -fpIC lib.c -Xlinker --version-script lib.ver -o lib.so

共享库系统路径

目前大多数包括 Linux在内的开源操作系统都遵守一个叫做 FHS的标准,这个标准规定了一个系统中的系统文件应该如何存放,包括各个目录的结构、组织和作用,这有利于促进各个开源操作系统之间的兼容性。
FHS 规定, 一个系统中主要有3个存放共享库的位置,它们分别如下:

  1. /ib,这个位置主要存放系统最关键和基础的共享库,比如动态链接器、 C 语言运行库、 数学库等
  2. /usr/lib,这个目录下主要保存的是一些非系统运行时所需要的关键性的共享库,主要是 一些开发时用到的共享库
  3. /usr/local/lib, 这个目录用来放置一些跟操作系统本身并不十分相关的库,主要是一些 第三方的应用程序的库

共享库查找过程

动态链接的ELF可执行文件在启动时同时会启动动态链接器。在Linux 系统中,动态链接器是/lib/ld-linux.so.X(X是版本号)。任何一个动态链接的模块所依赖的模块路径保存在“.dynamic” 段里面,由DT_NEED类型的项表示,如果DT_NEED里面保存的是绝对路径,那么动态链接器就按照这个路径去查找;如果DT_NEED里面保存的是相对路径,那么动态链接器会在/ib、/usr/lib 和由/etc/ld.so.conf 配置文件指定 的目录中查找共享库。

ld.so.conf 是一个文本配置文件,它可能包含其他的配置文件,这些配置文件中存放着 目录信息。
/usr/local/lib
/lib/i486-linux-gnu
/usr/lib/i486-linux-gnu

Linux 系统中都有一个叫做 ldconfig的程序,这个程序的作用是为共享库目录下的各个共享 库创建、删除或更新相应的SO-NAME (即相应的符号链接),这样每个共享库的 SO-NAME就能够指向正确的共享库文件;并且这个程序还会将这些SO-NAME收集起来,集中存放到letc/ld.so.cache 文件里面,并建立一个 SO-NAME的缓存。

环境变量

  • LD_LIBRARY_PATH

Linux 系统提供了很多方法来改变动态链接器装载共享库路径的方法。改变共享库查找路径最简单的方法是使用LD_LIBRARY_PATH环境变量

  • LD_PRELOAD

系统中另外还有一个环境变量叫做LD_PRELOAD,这个文件中我们可以指定预先装载 的一些共享库甚或是目标文件。在LD_PRELOAD里面指定的文件会在动态链接器按照固定 规则搜索共享库之前装载,它比LD_LIBRARY_PATH里面所指定的目录中的共享库还要优先。

  • LD_DEBUG

另外还有一个非常有用的环境变量LD_DEBUG,这个变量可以打开动态链接器的调试功能,当我们设置这个变量时,动态链接器会在运行时打印出各种有用的信息,对于我们开发和调试共享库有很大的帮助。

LD_DEBUG=files ./HelloWorld.out

你可能感兴趣的:(编译原理,linux,服务器,c语言,c++,学习,汇编)