由链接编辑器处理的 ELF 目标文件提供了许多其他目标文件可以绑定到的全局符号。这些符号描述了目标文件的应用程序二进制接口 (application binary interface, ABI)。在目标文件演变过程中,此接口可能会由于添加或删除全局符号而发生更改。此外,目标文件演变还可能涉及内部实现更改。
版本控制是一些可以应用于某个目标文件以指示接口与实现的更改的技术。这些技术使目标文件的演变过程受到控制,同时又维护向下兼容性。
本章介绍如何定义目标文件的 ABI,并对此接口的更改影响向下兼容性的方式进行分类。此外,还介绍了用于将接口与实现的更改引入目标文件新发行版的模型。
本章重点介绍动态可执行文件与共享库的运行时接口。对说明和管理这些动态库内更改的方法只作一般性介绍。附录 B,版本控制快速参考 中提供了应用于共享库的一组通用命名约定和版本控制方案。
动态库的开发者必须注意接口更改的结果,并了解管理此类更改的方法,尤其是关于维护与以前所发布的目标文件的向下兼容性。
由任何动态库实现可用的全局符号都代表目标文件的公共接口。通常,在链接编辑结束时,目标文件中剩余的全局符号数多于希望公开的符号数。这些全局符号是从用于创建该目标文件的可重定位目标文件之间所需的关系中产生的。它们代表目标文件自身的专用接口。
定义目标文件的二进制接口之前,应首先确定要创建的目标文件中那些希望使其公开可用的全局符号。可以使用链接编辑器的 -M 选项以及作为最终链接编辑一部分的关联 mapfile 来建立这些公共符号。缩减符号范围中介绍了此技术。此公共接口可在正在创建的目标文件内建立一个或多个版本定义。这些定义构成目标文件演变时添加新接口的基础。
以下各节基于此初始公共接口。不过,首先应了解如何对接口的各种更改进行分类,以便对其进行适当管理。
可以对目标文件进行许多类型的更改。可以用最简单的术语将这些更改分类为以下两组之一:
兼容更新。这些更新中新增了接口,所有先前可用的接口仍保持不变。
不兼容更新。这些更新更改了现有的接口,使此接口的现有用户操作失败或不正确地执行操作。
下表对一些常见的目标文件更改进行了分类。
表 5–1 接口兼容性示例
目标文件更改 |
更新类型 |
---|---|
添加符号 |
兼容 |
删除符号 |
不兼容 |
向非 varargs(3EXT) 函数中添加参数 |
不兼容 |
从函数中删除参数 |
不兼容 |
函数数据项或外部定义数据项的大小或内容更改 |
不兼容 |
目标文件的语义属性保持不变时,对函数进行的错误修复或内部增强 |
兼容 |
目标文件的语义属性发生更改时对函数进行的错误修复或内部增强 |
不兼容 |
添加符号(实质上是插入)可构成不兼容更新,使得新符号可能与应用程序对此符号的使用产生冲突。但是,由于通常使用源级名称空间管理,因此实际上这种情况非常少见。
可以通过维护要生成的目标文件的内部版本定义来适应兼容更新。可以通过生成具有新的外部版本化名称的新目标文件来适应不兼容更新。通过以上两种版本控制技术,可以选择应用程序的绑定,还可以在运行时验证正确版本绑定。以下各节中更详细地介绍了这两种技术。
一个动态库可以具有一个或多个与之关联的内部版本定义。每个版本定义通常与一个或多个符号名称关联。符号名称则只能与一个版本定义关联。但是,一个版本定义可以继承其他版本定义的符号。这样,便存在一个结构,用于定义正在创建的目标文件内部的一个或多个独立或者相关的版本定义。对目标文件进行新的更改后,可以添加新版本定义来反映这些更改。
提供共享库内的版本定义有两种结果:
根据版本化共享库生成的动态库可以记录它们与所绑定的版本定义之间的依赖性。运行时将检验这些版本依赖项,以确保提供相应的接口或功能来正确执行应用程序。
动态库可以在其链接编辑过程中选择要绑定的版本定义。通过此机制,开发者可以针对接口或功能控制其与共享库的依赖性,以提供最大的灵活性。
版本定义通常由符号名称与唯一版本名称关联组成。这些关联在 mapfile 内建立,并且在使用链接编辑器的 -M 选项最终链接编辑目标文件时应用它们。缩减符号范围一节中介绍了此技术。
版本定义是在将版本名称指定为 mapfile 指令的一部分时定义的。在以下示例中,将两个源文件与 mapfile 指令组合在一起,以生成具有已定义公共接口的目标文件:
$ cat foo.c extern const char * _foo1; void foo1() { (void) printf(_foo1); } $ cat data.c const char * _foo1 = "string used by foo1()\n"; $ cat mapfile SUNW_1.1 { # Release X global: foo1; local: *; }; $ cc -o libfoo.so.1 -M mapfile -G foo.o data.o $ nm -x libfoo.so.1 | grep "foo.$" [33] |0x0001058c|0x00000004|OBJT |LOCL |0x0 |17 |_foo1 [35] |0x00000454|0x00000034|FUNC |GLOB |0x0 |9 |foo1 |
符号 foo1 是定义用来提供共享库公共接口的唯一全局符号。特殊的自动缩减指令 "*" 可使所有其他全局符号缩减,以在要生成的目标文件中具有本地绑定。定义其他符号中介绍了此指令。关联的版本名称 SUNW_1.1 将生成版本定义。因此,共享库的公共接口包含与全局符号 foo1 关联的内部版本定义 SUNW_1.1。
每次使用版本定义或自动缩减指令生成目标文件时,还会创建基本版本定义。此基本版本是使用文件本身的名称定义的,用于将链接编辑器生成的所有保留符号关联在一起。有关这些保留符号的列表,请参见生成输出文件。
可以使用带有 -d 选项的 pvs(1) 来显示目标文件内包含的版本定义:
$ pvs -d libfoo.so.1 libfoo.so.1; SUNW_1.1; |
目标文件 libfoo.so.1 具有名为 SUNW_1.1 的内部版本定义以及基本版本定义 libfoo.so.1。
使用链接编辑器的 -z noversion 选项,可以由 mapfile 来定向符号缩减,但是会抑制创建版本定义。
从此初始版本定义开始,可以通过添加新接口和已更新功能来演变目标文件。例如,可以通过更新源文件 foo.c 和 data.c,将新函数 foo2 及其支持数据结构添加到目标文件中:
$ cat foo.c extern const char * _foo1; extern const char * _foo2; void foo1() { (void) printf(_foo1); } void foo2() { (void) printf(_foo2); } $ cat data.c const char * _foo1 = "string used by foo1()\n"; const char * _foo2 = "string used by foo2()\n"; |
可以通过创建新版本定义 SUNW_1.2 来定义表示符号 foo2 的新接口。此外,还可以将此新接口定义为继承原始版本定义 SUNW_1.1。
新接口的创建非常重要,因为它标识目标文件的演变,并且使用户可以检验和选择要绑定到的接口。绑定到版本定义和指定版本绑定中更详细地介绍了这些概念。
以下示例说明了创建这两个接口的 mapfile 指令。
$ cat mapfile SUNW_1.1 { # Release X global: foo1; local: *; }; SUNW_1.2 { # Release X+1 global: foo2; } SUNW_1.1; $ cc -o libfoo.so.1 -M mapfile -G foo.o data.o $ nm -x libfoo.so.1 | grep "foo.$" [33] |0x00010644|0x00000004|OBJT |LOCL |0x0 |17 |_foo1 [34] |0x00010648|0x00000004|OBJT |LOCL |0x0 |17 |_foo2 [36] |0x000004bc|0x00000034|FUNC |GLOB |0x0 |9 |foo1 [37] |0x000004f0|0x00000034|FUNC |GLOB |0x0 |9 |foo2 |
符号 foo1 和 foo2 都定义为共享库公共接口的一部分。但是,其中每个符号都指定给不同的版本定义;foo1 指定给 SUNW_1.1,foo2 指定给 SUNW_1.2。
可以使用同时带有 -d、-v 和 -s 选项的 pvs(1) 来显示这些版本定义、版本继承和符号关联。
$ pvs -dsv libfoo.so.1 libfoo.so.1: _end; _GLOBAL_OFFSET_TABLE_; _DYNAMIC; _edata; _PROCEDURE_LINKAGE_TABLE_; _etext; SUNW_1.1: foo1; SUNW_1.1; SUNW_1.2: {SUNW_1.1}: foo2; SUNW_1.2 |
版本定义 SUNW_1.2 依赖于版本定义 SUNW_1.1。
不同版本定义之间的继承是一项很有用的技术,可以减少任何绑定到版本依赖项的目标文件最终记录的版本信息。绑定到版本定义一节中更加详细地介绍了版本继承。
任何内部版本定义都会创建一个关联的版本定义符号。如上述 pvs(1) 示例中所示,使用 -v 选项时将显示这些符号。
可以通过创建弱版本定义来定义目标文件的不需要引入新接口定义的内部更改。此类更改的示例有错误修复或性能改善。
此类版本定义为空,因为它没有与之关联的全局接口符号。
例如,假设对上述示例中使用的数据文件 data.c 进行更新,以提供更详细的字符串定义:
$ cat data.c const char * _foo1 = "string used by function foo1()\n"; const char * _foo2 = "string used by function foo2()\n"; |
可以引入弱版本定义 (weak version definition) 来标识此更改:
$ cat mapfile SUNW_1.1 { # Release X global: foo1; local: *; }; SUNW_1.2 { # Release X+1 global: foo2; } SUNW_1.1; SUNW_1.2.1 { } SUNW_1.2; # Release X+2 $ cc -o libfoo.so.1 -M mapfile -G foo.o data.o $ pvs -dv libfoo.so.1 libfoo.so.1; SUNW_1.1; SUNW_1.2: {SUNW_1.1}; SUNW_1.2.1 [WEAK]: {SUNW_1.2}; |
空版本定义通过弱标志指示出来。通过这些弱版本定义 (weak version definition),应用程序可以通过绑定到与此功能关联的版本定义来检验是否存在特定的实现。绑定到版本定义一节更详细地说明了如何使用这些定义。
上述示例说明了添加到目标文件中的新版本定义如何继承任何现有的版本定义。还可以创建唯一且独立的版本定义。以下示例将两个新文件 bar1.c 和 bar2.c 添加到目标文件 libfoo.so.1 中。这两个文件分别提供新符号 bar1 和 bar2:
$ cat bar1.c extern void foo1(); void bar1() { foo1(); } $ cat bar2.c extern void foo2(); void bar2() { foo2(); } |
这两个符号旨在定义两个新的公共接口。这些新接口互不相关。但是,每个接口都依赖于原始的 SUNW_1.2 接口。
以下 mapfile 定义将创建这种所需的关联:
$ cat mapfile SUNW_1.1 { # Release X global: foo1; local: *; }; SUNW_1.2 { # Release X+1 global: foo2; } SUNW_1.1; SUNW_1.2.1 { } SUNW_1.2; # Release X+2 SUNW_1.3a { # Release X+3 global: bar1; } SUNW_1.2; SUNW_1.3b { # Release X+3 global: bar2; } SUNW_1.2; |
同样,可以使用 pvs(1) 检查使用此 mapfile 时在 libfoo.so.1 中创建的版本定义及其依赖项:
$ cc -o libfoo.so.1 -M mapfile -G foo.o bar1.o bar2.o data.o $ pvs -dv libfoo.so.1 libfoo.so.1; SUNW_1.1; SUNW_1.2: {SUNW_1.1}; SUNW_1.2.1 [WEAK]: {SUNW_1.2}; SUNW_1.3a: {SUNW_1.2}; SUNW_1.3b: {SUNW_1.2}; |
以下各节介绍如何使用这些版本定义记录来检验运行时绑定要求以及在创建目标文件过程中控制其绑定。
根据其他共享库生成动态可执行文件或共享库时,会在生成的目标文件中记录这些依赖项。有关更多详细信息,请参见共享库处理和记录共享库名称。如果这些共享库依赖项还包含版本定义,则会在正在生成的目标文件中记录关联的版本依赖项。
以下示例采用上一节中的数据文件并生成适用于编译时环境的共享库。在随后的绑定示例中将使用此共享库 libfoo.so.1。
$ cc -o libfoo.so.1 -h libfoo.so.1 -M mapfile -G foo.o bar.o \ data.o $ ln -s libfoo.so.1 libfoo.so $ pvs -dsv libfoo.so.1 libfoo.so.1: _end; _GLOBAL_OFFSET_TABLE_; _DYNAMIC; _edata; _PROCEDURE_LINKAGE_TABLE_; _etext; SUNW_1.1: foo1; SUNW_1.1; SUNW_1.2: {SUNW_1.1}: foo2; SUNW_1.2; SUNW_1.2.1 [WEAK]: {SUNW_1.2}: SUNW_1.2.1; SUNW_1.3a: {SUNW_1.2}: bar1; SUNW_1.3a; SUNW_1.3b: {SUNW_1.2}: bar2; SUNW_1.3b |
实际上,此共享库提供六个公共接口。其中 SUNW_1.1、SUNW_1.2、SUNW_1.3a 以及 SUNW_1.3b 这四个接口定义导出的符号名称。还有一个接口 SUNW_1.2.1 介绍共享库的内部实现更改,另一个接口 libfoo.so.1 定义一些保留标号。使用此共享库作为依赖项而创建的动态库将记录其绑定到的接口的版本名称。
以下示例将创建引用符号 foo1 和 foo2 的应用程序。可以使用带有 -r 选项的 pvs(1) 来检查此应用程序中记录的版本控制依赖项信息。
$ cat prog.c extern void foo1(); extern void foo2(); main() { foo1(); foo2(); } $ cc -o prog prog.c -L. -R. -lfoo $ pvs -r prog libfoo.so.1 (SUNW_1.2, SUNW_1.2.1); |
在本示例中,应用程序 prog 绑定到两个接口 SUNW_1.1 和 SUNW_1.2。这两个接口分别提供全局符号 foo1 和 foo2。
由于在 libfoo.so.1 内定义的版本定义 SUNW_1.1 由版本定义 SUNW_1.2 继承,因此还需要记录 SUNW_1.2 的版本依赖项。通过对版本定义依赖项进行这种标准化,可以减少目标文件内维护的版本信息量,并减少运行时需要进行的处理。
由于应用程序 prog 是根据包含弱版本定义 (weak version definition) SUNW_1.2.1 的共享库的实现而生成的,因此还会记录此依赖项。尽管此版本定义被定义为继承版本定义 SUNW_1.2,但此版本的弱性质不会包括通过 SUNW_1.1 进行的标准化,并且会生成单独的依赖项记录。
如果存在多个相互继承的弱版本定义 (weak version definition),则会按照标准化非弱版本定义 (non-weak version definition) 的方式来标准化这些定义。
可以使用链接编辑器的 -z noversion 选项来抑制记录版本依赖项。
记录了这些版本定义依赖项之后,运行时链接程序将验证执行应用程序时绑定到的目标文件中是否存在所需的版本定义。可以使用带有 -v 选项的 ldd(1) 来显示此验证。例如,通过对应用程序 prog 运行 ldd(1),便会显示在共享库 libfoo.so.1 中正确地找到了版本定义依赖项:
$ ldd -v prog find object=libfoo.so.1; required by prog libfoo.so.1 => ./libfoo.so.1 find version=libfoo.so.1; libfoo.so.1 (SUNW_1.2) => ./libfoo.so.1 libfoo.so.1 (SUNW_1.2.1) => ./libfoo.so.1 .... |
带有 -v 选项的 ldd(1) 意味着详细输出,将生成所有依赖项以及所有版本控制需求的递归列表。
如果找不到非弱版本定义 (non-weak version definition) 依赖项,则应用程序初始化过程中将发生致命错误。所有找不到的弱版本定义 (weak version definition) 依赖项将被忽略而无任何提示。例如,如果应用程序 prog 在 libfoo.so.1 只包含版本定义 SUNW_1.1 的环境中运行,则会发生以下致命错误:
$ pvs -dv libfoo.so.1 libfoo.so.1; SUNW_1.1; $ prog ld.so.1: prog: fatal: libfoo.so.1: version `SUNW_1.2' not \ found (required by file prog) |
在应用程序 prog 未记录任何版本定义依赖项的情况下,如果必需的接口符号 foo2 不存在,则可能表明本身在执行应用程序期间的某个时刻出现致命的重定位错误。此重定位错误可能会在进程初始化时或进程执行过程中发生,也可能根本不会发生(如果应用程序的执行路径未调用函数 foo2)。 请参见重定位错误。
记录版本定义依赖项也可以即时指出应用程序所需接口的可用性。
如果应用程序 prog 在 libfoo.so.1 只包含版本定义 SUNW_1.1 和 SUNW_1.2 的环境中运行,则会满足所有非弱版本定义 (non-weak version definition) 需求。缺少弱版本定义 (weak version definition) SUNW_1.2.1 被认为不具有致命性,因此不生成任何运行时错误状态。但是,可以使用 ldd(1) 来显示所有找不到的版本定义:
$ pvs -dv libfoo.so.1 libfoo.so.1; SUNW_1.1; SUNW_1.2: {SUNW_1.1}; $ prog string used by foo1() string used by foo2() $ ldd prog libfoo.so.1 => ./libfoo.so.1 libfoo.so.1 (SUNW_1.2.1) => (version not found) ........... |
如果目标文件需要给定依赖项中的版本定义,并且在运行时找到此依赖项的实现,但此依赖项不包含版本定义信息,则会忽略此依赖项的版本验证而无任何提示。此策略提供从非版本化共享库转换到版本化共享库时的向下兼容性级别。但是,仍可使用 ldd(1) 来显示任何版本需求差异。可以使用环境变量 LD_NOVERSION 来抑制所有运行时版本验证。
版本定义符号还提供了一种机制,可检验通过 dlopen(3C) 获取的目标文件的版本需求。使用此函数添加到进程地址空间的任何目标文件都不会使运行时链接程序执行自动版本依赖项验证。这样,此函数的调用程序将负责检验是否满足所有版本控制需求。
可以通过使用 dlsym(3C) 查找关联的版本定义符号来检验是否存在所需版本定义。在以下示例中,使用 dlopen(3C) 将共享库 libfoo.so.1 添加到进程中,并检验接口 SUNW_1.2 是否可用。
#include <stdio.h> #include <dlfcn.h> main() { void * handle; const char * file = "libfoo.so.1"; const char * vers = "SUNW_1.2"; .... if ((handle = dlopen(file, (RTLD_LAZY | RTLD_FIRST))) == NULL) { (void) printf("dlopen: %s\n", dlerror()); exit (1); } if (dlsym(handle, vers) == NULL) { (void) printf("fatal: %s: version `%s' not found\n", file, vers); exit (1); } .... |
根据包含版本定义的共享库创建动态库时,可以指示链接编辑器将绑定仅限于特定版本定义。实际上,利用链接编辑器可以控制目标文件到特定接口的绑定。
可以使用文件控制指令来控制目标文件的绑定需求。此指令通过链接编辑器的 -M 选项以及关联的 mapfile 提供。可以使用以下文件控制指令语法:
name - version [ version ... ] [ $ADDVERS=version ]; |
name-表示共享库依赖项的名称。此名称应该与链接编辑器所使用的共享库的编译环境名称相匹配。请参见库命名约定。
version-表示应该可用于绑定的共享库内的版本定义名称。可以指定多个版本定义。
此绑定控制在下面的情况下很有用:
当共享库定义独立的、唯一的版本时。定义不同的标准接口时可以进行此版本控制。可以通过绑定控制生成目标文件,以确保该目标文件只绑定到特定接口。
已经在多个软件发行版中对共享库进行版本化时。可以通过绑定控制生成目标文件,从而将该目标文件限制为绑定到上一个软件发行版中提供的接口。这样,使用共享库的最新发行版生成的目标文件仍可以与共享库依赖项的旧发行版一起运行。
以下示例说明了版本控制机制的用法。此示例使用包含以下版本接口定义的共享库 libfoo.so.1:
$ pvs -dsv libfoo.so.1 libfoo.so.1: _end; _GLOBAL_OFFSET_TABLE_; _DYNAMIC; _edata; _PROCEDURE_LINKAGE_TABLE_; _etext; SUNW_1.1: foo1; foo2; SUNW_1.1; SUNW_1.2: {SUNW_1.1}: bar; |
版本定义 SUNW_1.1 和 SUNW_1.2 表示 libfoo.so.1 内的接口,这些接口分别在软件 Release X 和 Release X+1 中提供。
可以使用以下版本控制 mapfile 指令来生成应用程序,使其只绑定到 Release X 中提供的接口:
$ cat mapfile libfoo.so - SUNW_1.1; |
例如,假设您开发一个应用程序 prog,并且要确保此应用程序可以在 Release X 中运行。这样此应用程序只能使用此发行版中提供的接口。如果应用程序错误地引用了符号 bar,便会与所需接口不兼容。链接编辑器会将此情形报告为未定义的符号错误:
$ cat prog.c extern void foo1(); extern void bar(); main() { foo1(); bar(); } $ cc -o prog prog.c -M mapfile -L. -R. -lfoo Undefined first referenced symbol in file bar prog.o (symbol belongs to unavailable \ version ./libfoo.so (SUNW_1.2)) ld: fatal: Symbol referencing errors. No output written to prog |
为了与 SUNW_1.1 接口兼容,必须删除对 bar 的引用。可以对应用程序重新进行处理以删除对 bar 的需求,也可以在创建应用程序时添加 bar 的实现。
缺省情况下,还会根据任意文件控制指令来检验链接编辑过程中遇到的共享库依赖项。使用环境变量 LD_NOVERSION 可抑制任何共享库依赖项的版本验证。
要使记录的版本依赖项多于从目标文件的正常符号绑定中生成的版本依赖项,请使用 $ADDVERS 文件控制指令。本节介绍此附加绑定可能有用的情况。
在一个 libfoo.so.1 示例中,假设在 Release X+2, 中,版本定义 SUNW_1.1 分为两个标准发行版:STAND_A 和 STAND_B。要保持兼容性,必须维护 SUNW_1.1 版本定义。在本示例中,此版本定义表示为继承两个标准定义:
$ pvs -dsv libfoo.so.1 libfoo.so.1: _end; _GLOBAL_OFFSET_TABLE_; _DYNAMIC; _edata; _PROCEDURE_LINKAGE_TABLE_; _etext; SUNW_1.1: {STAND_A, STAND_B}: SUNW_1.1; SUNW_1.2: {SUNW_1.1}: bar; STAND_A: foo1; STAND_A; STAND_B: foo2; STAND_B; |
如果应用程序 prog 的唯一需求是接口符号 foo1,则此应用程序将仅依赖于版本定义 STAND_A。这将阻止在 libfoo.so.1 小于 Release X+2 的系统上运行 prog。尽管早期的发行版具有接口 foo1,但没有版本定义 STAND_A。
生成应用程序 prog 时,可以通过创建对 SUNW_1.1 的依赖性来使其要求与早期发行版一致:
$ cat mapfile libfoo.so - SUNW_1.1 $ADDVERS=SUNW_1.1; $ cat prog extern void foo1(); main() { foo1(); } $ cc -M mapfile -o prog prog.c -L. -R. -lfoo $ pvs -r prog libfoo.so.1 (SUNW_1.1); |
此显式依赖性足以涵盖实际的依赖性要求。此依赖性可满足与旧发行版的兼容性。
创建弱版本定义 (weak version definition)介绍了如何使用弱版本定义 (weak version definition) 标记内部实现更改。这些版本定义非常适用于指示针对目标文件所做的错误修复以及性能改善。如果需要弱版本,可以生成对此版本定义的显式依赖性。当错误修复或性能改善对于目标文件的正常工作至关重要时,创建此类依赖性非常重要。
在上一个 libfoo.so.1 示例中,假设在软件 Release X+3 中以弱版本定义 (weak version definition) SUNW_1.2.1 引入了错误修复:
$ pvs -dsv libfoo.so.1 libfoo.so.1: _end; _GLOBAL_OFFSET_TABLE_; _DYNAMIC; _edata; _PROCEDURE_LINKAGE_TABLE_; _etext; SUNW_1.1: {STAND_A, STAND_B}: SUNW_1.1; SUNW_1.2: {SUNW_1.1}: bar; STAND_A: foo1; STAND_A; STAND_B: foo2; STAND_B; SUNW_1.2.1 [WEAK]: {SUNW_1.2}: SUNW_1.2.1; |
通常,如果根据此共享库生成应用程序,则生成的应用程序将记录与版本定义 SUNW_1.2.1 的弱依赖性。此依赖性仅用于提供信息。如果运行时使用的 libfoo.so.1 中不存在此版本定义,则此依赖性不会导致终止应用程序。
文件控制指令 $ADDVERS 可用于生成版本定义的显式依赖性。如果此定义为弱定义,则此显式引用还会导致版本定义提升为强依赖性。
可以使用以下文件控制指令生成应用程序 prog,以强制满足 SUNW_1.2.1 接口在运行时可用的需求:
$ cat mapfile libfoo.so - SUNW_1.1 $ADDVERS=SUNW_1.2.1; $ cat prog extern void foo1(); main() { foo1(); } $ cc -M mapfile -o prog prog.c -L. -R. -lfoo $ pvs -r prog libfoo.so.1 (SUNW_1.2.1); |
生成 prog 时创建了与接口 STAND_A 的显式依赖性。由于版本定义 SUNW_1.2.1 提升为强版本,因此它也会通过依赖性 STAND_A 进行标准化。在运行时,如果找不到版本定义 SUNW_1.2.1,则会产生致命错误。
如果处理少量的依赖项,可以使用链接编辑器的 -u 选项显式绑定到某个版本定义。使用此选项可以引用版本定义符号。但是,符号引用是不可选择的。如果是处理多个依赖项(包含多个类似命名的版本定义)时,则此方法可能不足以创建显式绑定。
只有当单个版本定义在目标文件的生命周期内保持不变时,绑定到该目标文件内版本的各种模型才会保持不变。
创建目标文件的版本定义并将其公开后,此定义就必须在该目标文件的后续发行版中保持未更改状态。版本名称以及关联的符号必须保持不变。因此,不支持对版本定义内定义的符号名称进行通配符扩展。在目标文件演变过程中,与通配符匹配的符号数可能会有所不同。
可以在动态库内记录并使用版本信息。可重定位目标文件可用类似的方式维护版本控制信息。但是,在如何使用此信息方面存在一些细微差异。
记录提供给可重定位目标文件链接编辑的任何版本定义时采用的格式与生成动态可执行文件或共享库时采用的格式相同。但是,缺省情况下,不会对正在创建的目标文件执行符号缩减。相反,当最终使用可重定位目标文件作为生成动态库的输入时,将使用版本记录来确定要应用的符号缩减。
此外,在可重定位目标文件中找到的所有版本定义都会传播到该动态库。有关可重定位目标文件中版本处理的示例,请参见缩减符号范围。
对共享库的运行时引用应该始终引用文件的版本文件名。版本文件名通常表示为带有版本号后缀的文件名。由于共享库的接口以不兼容的方式变化,这种变化会造成无法继续使用旧应用程序,因此,应该使用新的版本化文件名来分发新的共享库。此外,仍必须使用原来的版本化文件名分发旧的共享库,以提供旧应用程序所需的接口。
针对多个软件发行版生成应用程序时,应当在运行时环境中通过单独的版本化文件名来提供共享库。这样可以保证生成应用程序所依据的接口可用,以使应用程序在其执行过程中可以绑定到此接口。
下面一节介绍如何协调编译环境和运行时环境之间的接口绑定。
在链接编辑过程中,输入共享库的最常见方法是使用 -l 选项。此选项使用链接编辑器的库搜索机制来查找带有 lib 前缀以及 .so 后缀的共享库。
但是,在运行时,所有共享库依赖项都应该以其版本化名称形式存在。可以创建两个文件名之间的文件系统链接,而不是维护遵循这些命名约定的两个不同的共享库。
要使运行时共享库 libfoo.so.1 可用于编译环境,请提供从编译文件名到运行时文件名的符号链接。 例如:
$ cc -o libfoo.so.1 -G -K pic foo.c $ ln -s libfoo.so.1 libfoo.so $ ls -l libfoo* lrwxrwxrwx 1 usr grp 11 1991 libfoo.so -> libfoo.so.1 -rwxrwxr-x 1 usr grp 3136 1991 libfoo.so.1 |
符号链接和硬链接都可使用。但是,符号链接在文档和诊断辅助方面更有用。
已经针对运行时环境生成共享库 libfoo.so.1。通过生成符号链接 libfoo.so,可以在编译环境中使用此文件。 例如:
$ cc -o prog main.o -L. -lfoo |
链接编辑器处理具有共享库 libfoo.so.1(通过符号链接 libfoo.so 即可找到)所描述的接口的可重定位目标文件 main.o。
针对多个软件发行版,可以分发具有已更改接口的此共享库的新版本。可以将编译环境构建为通过更改符号链接即可使用适用的接口。 例如:
$ ls -l libfoo* lrwxrwxrwx 1 usr grp 11 1993 libfoo.so -> libfoo.so.3 -rwxrwxr-x 1 usr grp 3136 1991 libfoo.so.1 -rwxrwxr-x 1 usr grp 3237 1992 libfoo.so.2 -rwxrwxr-x 1 usr grp 3554 1993 libfoo.so.3 |
此共享库提供三个主要的版本。其中两个共享库 libfoo.so.1 和 libfoo.so.2 为现有应用程序提供依赖项。libfoo.so.3 则为创建和运行新应用程序提供最新主发行版。
使用此符号链接机制本身并不足以保证在编译环境中使用了正确的共享库绑定,就能满足运行时环境对此绑定的需求。如示例目前所示,链接编辑器将在动态可执行文件 prog 中记录其已处理的共享库的文件名。在这种情况下,此文件名即是编译环境文件名。
$ dump -Lv prog prog: **** DYNAMIC SECTION INFORMATION **** .dynamic: [INDEX] Tag Value [1] NEEDED libfoo.so ......... |
执行应用程序 prog 后,运行时链接程序将搜索依赖项 libfoo.so。prog 将绑定到此符号链接所指向的文件。
要提供记录为依赖项的正确运行时名称,应该生成共享库 libfoo.so.1(通过 soname 定义生成)。此定义标识共享库的运行时名称。链接此共享库的任何目标文件将此名称用作依赖项的名称。在共享库本身的链接编辑过程中,可以使用 -h 选项提供此定义。 例如:
$ cc -o libfoo.so.1 -G -K pic -h libfoo.so.1 foo.c $ ln -s libfoo.so.1 libfoo.so $ cc -o prog main.o -L. -lfoo $ dump -Lv prog prog: **** DYNAMIC SECTION INFORMATION **** .dynamic: [INDEX] Tag Value [1] NEEDED libfoo.so.1 ......... |
此符号链接和 soname 机制已经在编译和运行时环境的共享库命名约定之间建立了很强的协调。将在生成的输出文件中准确记录链接编辑过程中处理的接口。此记录可确保在运行时提供目标接口。
创建新的外部版本化共享库是一项主要的更改。请务必了解使用此共享库的所有进程的全部依赖项。
例如,应用程序可能依赖于 libfoo.so.1 以及从外部传送的目标文件 libISV.so.1。后者也可能依赖于 libfoo.so.1。如果重新设计应用程序,使其使用 libfoo.so.2 中的新接口,而不对外部目标文件 libISV.so.1 的使用做任何更改,则 libfoo.so 的两个主要版本都会引入正在运行的进程。由于更改 libfoo.so 版本的唯一原因是标记不兼容的更改,因此在一个进程中具有该目标文件的两个版本可能会导致错误的符号绑定并由此导致不需要的交互。