动态目标文件可以包含一个或多个与其关联的内部版本定义。每个版本定义通常与一个或多个符号名称关联。一个符号名称只能与一个版本定义关联。但是,一个版本定义可以继承来自其他版本定义的符号。因此,我们有一种结构可以定义所创建的目标文件内的一个或多个独立的或相关的版本定义。当对目标文件进行了新的更改时,可以添加新的版本定义来表示这些更改。
共享目标文件内的版本定义有两项功能。
根据版本化共享目标文件生成的动态目标文件可以记录它们对所绑定的版本定义的依赖性。运行时将检验这些版本依赖项,以确保提供相应的接口或功能来正确执行应用程序。
动态目标文件可以在其链接编辑期间选择要绑定的共享目标文件版本定义。通过此机制,开发者可以针对接口或功能控制其对共享目标文件的依赖性,以提供最大的灵活性。
版本定义通常包括符号名称与唯一版本名称之间的关联。这些关联建立在 mapfile 中,并使用链接编辑器的 -M 选项提供给目标文件的最终链接编辑。缩减符号作用域一节中介绍了此技术。
任何时候作为 mapfile 指令的一部分指定一个版本名称时,都会创建一个版本定义。在以下示例中,两个源文件进行合并,并配合 mapfile 指令,生成一个定义了公共接口的目标文件。
$ cat foo.c #include <stdio.h> extern const char *_foo1; void foo1() { (void) printf(_foo1); } $ cat data.c const char *_foo1 = "string used by foo1()\n"; $ cat mapfile $mapfile_version 2 SYMBOL_VERSION SUNW_1.1 { # Release X global: foo1; local: *; }; $ cc -c -Kpic foo.c data.c $ cc -o libfoo.so.1 -M mapfile -G foo.o data.o $ elfdump -sN.symtab libfoo.so.1 | grep 'foo.$' [32] 0x0001074c 0x00000004 OBJT LOCL H 0 .data _foo1 [53] 0x00000560 0x00000038 FUNC GLOB D 0 .text foo1
符号 foo1 是为提供共享目标文件的公共接口而定义的唯一全局符号。特殊的自动缩减指令 "*" 缩减了其他所有全局符号,从而在所生成的目标文件中实现局部绑定。SYMBOL_SCOPE / SYMBOL_VERSION 指令部分对该自动缩减指令进行了介绍。关联的版本名称 SUNW_1.1 导致生成版本定义。因此,共享目标文件的公共接口包括与内部版本定义 SUNW_1.1 关联的全局符号 foo1。
任何时候使用版本定义或自动缩减指令生成目标文件时,都会同时创建一个基版本定义。此基版本使用所生成的目标文件的名称来定义。此基版本用于关联链接编辑器生成的任何保留符号。有关保留符号的列表,请参见生成输出文件。
使用 pvs(1) 和 -d 选项可以显示目标文件中包含的版本定义。
$ pvs -d libfoo.so.1 libfoo.so.1; SUNW_1.1;
目标文件 libfoo.so.1 包含一个名为 SUNW_1.1 的内部版本定义和一个名为 libfoo.so.1 的基版本定义。
目标文件可以从该初始版本定义开始,通过添加新的接口和更新功能不断演变。例如,通过更新源文件 foo.c 和 data.c,可以将新的函数 foo2 及其支持的数据结构添加到目标文件中。
$ cat foo.c #include <stdio.h> 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 $mapfile_version 2 SYMBOL_VERSION SUNW_1.1 { # Release X global: foo1; local: *; }; SYMBOL_VERSION SUNW_1.2 { # Release X+1 global: foo2; } SUNW_1.1; $ cc -o libfoo.so.1 -M mapfile -G foo.o data.o $ elfdump -sN.symtab libfoo.so.1 | grep 'foo.$' [28] 0x000107a4 0x00000004 OBJT LOCL H 0 .data _foo1 [29] 0x000107a8 0x00000004 OBJT LOCL H 0 .data _foo2 [48] 0x000005e8 0x00000020 FUNC GLOB D 0 .text foo1 [51] 0x00000618 0x00000020 FUNC GLOB D 0 .text foo2
符号 foo1 和 foo2 均定义为共享目标文件公共接口的一部分。但是,每个符号指定给了一个不同的版本定义。foo1 指定给了版本 SUNW_1.1。foo2 指定给了版本 SUNW_1.2。
使用 pvs(1) 以及 -d、-v 和 -s 选项可以显示这些版本定义及其继承性和符号关联。
$ 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";
这时便可以引入一个弱版本定义来标识此项更改。
$ cat mapfile $mapfile_version 2 SYMBOL_VERSION SUNW_1.1 { # Release X global: foo1; local: *; }; SYMBOL_VERSION SUNW_1.2 { # Release X+1 global: foo2; } SUNW_1.1; SYMBOL_VERSION 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};
空版本定义由弱标签表示。利用这些弱版本定义,应用程序可以验证某个特定的实现详细信息是否存在。应用程序可以绑定到与其要求的实现详细信息相关联的版本定义。绑定到版本定义一节更详细地说明了如何使用这些定义。
上面的示例说明了添加到目标文件的新版本定义如何继承任何现有的版本定义。您还可以创建具有唯一性的、独立的版本定义。在以下示例中,两个新文件 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 $mapfile_version 2 SYMBOL_VERSION SUNW_1.1 { # Release X global: foo1; local: *; }; SYMBOL_VERSION SUNW_1.2 { # Release X+1 global: foo2; } SUNW_1.1; SYMBOL_VERSION SUNW_1.2.1 { } SUNW_1.2; # Release X+2 SYMBOL_VERSION SUNW_1.3a { # Release X+3 global: bar1; } SUNW_1.2; SYMBOL_VERSION SUNW_1.3b { # Release X+3 global: bar2; } SUNW_1.2;
使用此 mapfile 时在 libfoo.so.1 中创建的版本定义及其相关的依赖项可以使用 pvs(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
共享目标文件 libfoo.so.1 提供六个公共接口。其中 SUNW_1.1、SUNW_1.2、SUNW_1.3a 以及 SUNW_1.3b 这四个接口定义了导出的符号名称。接口 SUNW_1.2.1 描述了目标文件的一个内部实现更改。接口 libfoo.so.1 定义了若干保留标签。使用 libfoo.so.1 作为依赖项创建的动态目标文件记录了动态目标文件绑定到的接口的版本名称。
以下示例创建了一个引用符号 foo1 和 foo2 的应用程序。使用 pvs(1) 及 -r 选项可以检查该应用程序中记录的版本控制依赖项信息。
$ 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。
因为版本定义 SUNW_1.1 在 libfoo.so.1 中定义为由版本定义 SUNW_1.2 继承,所以您只需记录一个依赖项。这种继承是为了实现版本定义依赖项的标准化。这种标准化可以减少目标文件中保留的版本信息量,并减少运行时需要的版本验证处理。
因为应用程序 prog 是根据包含弱版本定义 SUNW_1.2.1 的共享目标文件实现而生成的,所以也记录了该依赖项。虽然此版本定义被定义为继承版本定义 SUNW_1.2,但是其弱版本的性质阻止了其使用 SUNW_1.1 进行标准化。弱版本定义会导致单独记录依赖项的情况。
如果有多个弱版本定义彼此继承,则这些定义将以与非弱版本定义一样的方式进行标准化。
在执行应用程序时,运行时链接程序会验证是否存在任何来自绑定到的目标文件的已记录版本定义。使用 ldd(1) 及 -v 选项可以显示此验证。例如,通过对应用程序 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 ....
如果找不到非弱版本定义依赖项,应用程序初始化期间会发生致命错误。将在不给出任何提示的情况下忽略任何无法找到的弱版本定义依赖项。例如,如果在应用程序 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 。在这种情况下,所有非弱版本定义的要求均得到满足。缺少弱版本定义 SUNW_1.2.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(1) 可以显示所有无法找到的版本定义。
$ ldd prog libfoo.so.1 => ./libfoo.so.1 libfoo.so.1 (SUNW_1.2.1) => (version not found) ...........
在运行时,如果依赖项的实现不包含版本定义信息,则会在不给出任何提示的情况下忽略该依赖项的任何版本验证。在从非版本化的共享目标文件过渡到版本化的共享目标文件时,此策略可以提供一定程度的向下兼容性。ldd(1) 总是可以用于显示任何版本要求差异。
注 - 环境变量 LD_NOVERSION 可以用于抑制所有运行时版本控制验证。
版本定义符号还提供一种机制,用于验证dlopen(3C) 所获取的目标文件的版本要求。使用 dlopen(3C) 添加到进程地址空间的目标文件不会自动进行版本依赖项验证。因此,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()); return (1); } if (dlsym(handle, vers) == NULL) { (void) printf("fatal: %s: version `%s' not found\n", file, vers); return (1); } ....
创建动态目标文件时,如果该动态目标文件基于包含版本定义的共享目标文件进行链接,则可以指示链接编辑器将绑定仅限于特定版本定义。实际上,使用链接编辑器可以控制目标文件到特定接口的绑定。
使用 DEPEND_VERSIONS mapfile 指令可以控制目标文件的绑定要求。此指令通过链接编辑器的 -M 选项和关联的 mapfile 来提供。DEPEND_VERSIONS 指令使用以下语法。
$mapfile_version 2 DEPEND_VERSIONS objname { ALLOW = version_name; REQUIRE = version_name; ... };
objname 代表共享目标文件依赖项的名称。此名称应当与链接编辑器所使用的共享目标文件编译环境名称一致。请参见库命名约定。
ALLOW 属性用于指定共享目标文件内应设为可用于绑定的版本定义名称。可以指定多个 ALLOW 属性。
REQUIRE 属性允许记录新增的版本定义。可以指定多个 REQUIRE 属性。
在以下情况中,版本绑定的控制非常实用。
当共享目标文件定义了独立的、具有唯一性的版本时。在定义不同标准接口时可以使用这种版本控制方法。可以使用绑定控制生成目标文件,以确保目标文件仅绑定到特定的接口。
当共享目标文件经过了若干软件发行版的版本化时。可以使用绑定控制生成目标文件,以将其绑定限定到之前的软件发行版中可用的接口。这样,目标文件在使用最新发行版的共享目标文件生成之后,仍可以与旧发行版的共享目标文件依赖项一起运行。
以下示例说明了版本控制机制的使用。此示例使用共享目标文件 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 $mapfile_version 2 DEPEND_VERSIONS libfoo.so { ALLOW = SUNW_1.1; }
例如,假定您开发一个应用程序 prog,并希望确保该应用程序可以运行于 Release X。该应用程序必须仅使用 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 的实现。
要记录目标文件的正常符号绑定可能生成的更多版本依赖项,可以将 REQUIRE 属性用于 DEPEND_VERSIONS mapfiile 指令。以下各节说明了可以使用这种额外绑定的情形。
情形之一是,将特定于 ISV 的接口转换为公共标准接口。
在之前的 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 的依赖项。这将使 prog 无法运行在 libfoo.so.1 小于 Release X+2 的系统上。版本定义 STAND_A 在之前的发行版中不存在,尽管接口 foo1 在之前的发行版中存在。
通过创建对 SUNW_1.1 的依赖项,可以生成应用程序 prog,使其要求与之前的发行版一致。
$ cat mapfile $mapfile_version 2 DEPEND_VERSIONS libfoo.so { ALLOW = SUNW_1.1; REQURE = 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);
此显式依赖项足以封装真实的依赖项要求。此依赖项可以满足与旧发行板的兼容性。
创建弱版本定义描述了如何使用弱版本定义标记内部实现更改。这些版本定义非常适合指示对目标文件所做的错误修复和性能改进。如果弱版本的存在是必需的,可以生成一个对此版本定义的显式依赖项。当某项错误修复或性能改进对于目标文件的正常工作至关重要时,创建这类依赖项是非常重要的。
在之前的 libfoo.so.1 示例中,假定一项错误修复作为弱版本定义 SUNW_1.2.1 纳入软件 Release X+3 中:
$ 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;
通常,如果应用程序依据此 libfoo.so.1 进行生成,将记录一个对版本定义 SUNW_1.2.1 的弱依赖项。此依赖项仅用于提供信息。如果在运行时使用的 libfoo.so.1 的实现中不存在版本定义,此依赖项不会导致应用程序终止。
使用 DEPEND_VERSIONS mapfile 指令的 REQUIRE 属性可以生成对版本定义的显式依赖项。如果此定义是一个弱定义,则此显式引用同时也是要提升为强依赖项的版本定义。
使用以下文件控制指令,可以将应用程序 prog 生成为强制要求 SUNW_1.2.1 接口在运行时可用。
$ cat mapfile $mapfile_version 2 DEPEND_VERSIONS libfoo.so { ALLOW = SUNW_1.1; REQUIRE = 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 提升为强版本,所以版本 SUNW_1.2.1 使用依赖项 STAND_A 进行标准化。在运行时,如果无法找到版本定义 SUNW_1.2.1,将生成一条致命错误。
注 - 当处理的依赖项数量较少时,可以使用链接编辑器的 -u 选项显式绑定到版本定义。使用此选项可以引用版本定义符号。但是,符号引用是不可选择的。当处理多个包含类似命名的版本定义的依赖项时,使用此方法可能不足以创建显式绑定。
已介绍了用于绑定到目标文件中的版本定义的不同模型。这些模型允许对接口要求进行运行时验证。仅当各个版本定义在目标文件的生命周期内保持不变时,此验证才能保持有效。
可以为一个目标文件创建一个版本定义,以供其他目标文件进行绑定。此版本定义必须在后续的目标文件发行版中一直存在。版本名称及与该版本关联的符号均必须保持不变。要强制执行这些要求,版本定义中定义的符号名称的通配符扩展将不受支持。与通配符匹配的符号的数量在目标文件的演变过程中可能会有所不同。这种差异可能会导致意外的接口不稳定性。
之前的各节说明了如何在动态目标文件中记录版本信息。可重定位的目标文件可以按类似的方式保留版本控制信息。但是,在如何使用此信息方面,存在着细微的差别。
任何提供给可重定位目标文件的链接编辑的版本定义都记录在该目标文件中。这些定义采取与动态目标文件中记录的版本定义相同的格式。但是,缺省情况下,不会对所创建的可重定位目标文件执行符号缩减。当可重定位目标文件用于创建动态目标文件时,由版本控制信息定义的符号缩减将应用到该目标文件。
此外,可重定位目标文件中找到的任何版本定义都将传播到动态目标文件。有关在可重定位目标文件中进行版本处理的示例,请参见缩减符号作用域。