第5 章• 应用程序二进制接口与版本控制149
$ 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 是定义用来提供共享库公共接口的唯一全局符号。特殊的自动缩减指令"*" 可使所
有其他全局符号缩减,以在要生成的目标文件中具有本地绑定。第48 页中的“定义其他符
号”中介绍了此指令。关联的版本名称SUNW_1.1 将生成版本定义。因此,共享库的公共接
口包含与全局符号foo1 关联的内部版本定义SUNW_1.1。
每次使用版本定义或自动缩减指令生成目标文件时,还会创建基本版本定义。此基本版本
是使用文件本身的名称定义的,用于将链接编辑器生成的所有保留符号关联在一起。有关
这些保留符号的列表,请参见第62 页中的“生成输出文件”。
可以使用带有-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);
内部版本控制
150 链接程序和库指南• 2006 年10 月
}
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。
新接口的创建非常重要,因为它标识目标文件的演变,并且使用户可以检验和选择要绑定
到的接口。第156 页中的“绑定到版本定义”和第161 页中的“指定版本绑定”中更详细
地介绍了这些概念。
以下示例说明了创建这两个接口的mapfile 指令。
$ cat mapfile
SUNW_1.1 { # Release X
global:
foo1;
local:
*;
};
SUNW_1.2 { # Release X+1
global:
内部版本控制
第5 章• 应用程序二进制接口与版本控制151
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;
内部版本控制
152 链接程序和库指南• 2006 年10 月
SUNW_1.2
版本定义SUNW_1.2 依赖于版本定义SUNW_1.1。
不同版本定义之间的继承是一项很有用的技术,可以减少任何绑定到版本依赖项的目标文
件最终记录的版本信息。第156 页中的“绑定到版本定义”一节中更加详细地介绍了版本
继承。
任何内部版本定义都会创建一个关联的版本定义符号。如上述pvs(1) 示例中所示,使用-v
选项时将显示这些符号。
创建弱版本定义(weakversion definition)
可以通过创建弱版本定义来定义目标文件的不需要引入新接口定义的内部更改。此类更改
的示例有错误修复或性能改善。
此类版本定义为空,因为它没有与之关联的全局接口符号。
例如,假设对上述示例中使用的数据文件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:
内部版本控制
第5 章• 应用程序二进制接口与版本控制153
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),应用程序
可以通过绑定到与此功能关联的版本定义来检验是否存在特定的实现。第156 页中的“绑
定到版本定义”一节更详细地说明了如何使用这些定义。
定义不相关接口
上述示例说明了添加到目标文件中的新版本定义如何继承任何现有的版本定义。还可以创
建唯一且独立的版本定义。以下示例将两个新文件bar1.c 和bar2.c 添加到目标文件
libfoo.so.1 中。这两个文件分别提供新符号bar1 和bar2:
$ cat bar1.c
extern void foo1();
void bar1()
{
foo1();
}
$ cat bar2.c
内部版本控制
154 链接程序和库指南• 2006 年10 月
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
内部版本控制
第5 章• 应用程序二进制接口与版本控制155
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};
以下各节介绍如何使用这些版本定义记录来检验运行时绑定要求以及在创建目标文件过程
中控制其绑定。
绑定到版本定义
根据其他共享库生成动态可执行文件或共享库时,会在生成的目标文件中记录这些依赖
项。有关更多详细信息,请参见第31 页中的“共享库处理”和第116 页中的“记录共享库
名称”。如果这些共享库依赖项还包含版本定义,则会在正在生成的目标文件中记录关联
的版本依赖项。
以下示例采用上一节中的数据文件并生成适用于编译时环境的共享库。在随后的绑定示例
中将使用此共享库libfoo.so.1。
内部版本控制
156 链接程序和库指南• 2006 年10 月
$ 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
内部版本控制
第5 章• 应用程序二进制接口与版本控制157
实际上,此共享库提供六个公共接口。其中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) 的方式来标准化这些定义。
内部版本控制
158 链接程序和库指南• 2006 年10 月
注– 可以使用链接编辑器的-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 不存在,
则可能表明本身在执行应用程序期间的某个时刻出现致命的重定位错误。此重定位错误可
内部版本控制
第5 章• 应用程序二进制接口与版本控制159
能会在进程初始化时或进程执行过程中发生,也可能根本不会发生(如果应用程序的执行
路径未调用函数foo2)。请参见第82 页中的“重定位错误”。
记录版本定义依赖项也可以即时指出应用程序所需接口的可用性。
如果应用程序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 是否可
用。
内部版本控制
160 链接程序和库指南• 2006 年10 月
#include
#include
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);
}
....
指定版本绑定
根据包含版本定义的共享库创建动态库时,可以指示链接编辑器将绑定仅限于特定版本定
义。实际上,利用链接编辑器可以控制目标文件到特定接口的绑定。
内部版本控制
第5 章• 应用程序二进制接口与版本控制161
可以使用文件控制指令来控制目标文件的绑定需求。此指令通过链接编辑器的-M 选项以及
关联的mapfile 提供。可以使用以下文件控制指令语法:
name - version [ version ... ] [ $ADDVERS=version ];
name-表示共享库依赖项的名称。此名称应该与链接编辑器所使用的共享库的编译环境
名称相匹配。请参见第32 页中的“库命名约定”。
version-表示应该可用于绑定的共享库内的版本定义名称。可以指定多个版本定义。
$ADDVERS-允许记录其他版本定义。
此绑定控制在下面的情况下很有用:
当共享库定义独立的、唯一的版本时。定义不同的标准接口时可以进行此版本控制。可
以通过绑定控制生成目标文件,以确保该目标文件只绑定到特定接口。
已经在多个软件发行版中对共享库进行版本化时。可以通过绑定控制生成目标文件,从
而将该目标文件限制为绑定到上一个软件发行版中提供的接口。这样,使用共享库的最
新发行版生成的目标文件仍可以与共享库依赖项的旧发行版一起运行。
以下示例说明了版本控制机制的用法。此示例使用包含以下版本接口定义的共享库
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;
内部版本控制
162 链接程序和库指南• 2006 年10 月
版本定义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 的实现。
内部版本控制
第5 章• 应用程序二进制接口与版本控制163
注– 缺省情况下,还会根据任意文件控制指令来检验链接编辑过程中遇到的共享库依赖项。
使用环境变量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;
内部版本控制
164 链接程序和库指南• 2006 年10 月
如果应用程序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);
此显式依赖性足以涵盖实际的依赖性要求。此依赖性可满足与旧发行版的兼容性。
第153 页中的“创建弱版本定义(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_;
内部版本控制
第5 章• 应用程序二进制接口与版本控制165
_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();
内部版本控制
166 链接程序和库指南• 2006 年10 月
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 选项显式绑定到某个版本定义。使
用此选项可以引用版本定义符号。但是,符号引用是不可选择的。如果是处理多个依赖项
(包含多个类似命名的版本定义)时,则此方法可能不足以创建显式绑定。
版本稳定性
只有当单个版本定义在目标文件的生命周期内保持不变时,绑定到该目标文件内版本的各
种模型才会保持不变。
创建目标文件的版本定义并将其公开后,此定义就必须在该目标文件的后续发行版中保持
未更改状态。版本名称以及关联的符号必须保持不变。因此,不支持对版本定义内定义的
符号名称进行通配符扩展。在目标文件演变过程中,与通配符匹配的符号数可能会有所不
同。
可重定位目标文件
可以在动态库内记录并使用版本信息。可重定位目标文件可用类似的方式维护版本控制信
息。但是,在如何使用此信息方面存在一些细微差异。
记录提供给可重定位目标文件链接编辑的任何版本定义时采用的格式与生成动态可执行文
件或共享库时采用的格式相同。但是,缺省情况下,不会对正在创建的目标文件执行符号
缩减。相反,当最终使用可重定位目标文件作为生成动态库的输入时,将使用版本记录来
确定要应用的符号缩减。
内部版本控制
第5 章• 应用程序二进制接口与版本控制167
此外,在可重定位目标文件中找到的所有版本定义都会传播到该动态库。有关可重定位目
标文件中版本处理的示例,请参见第56 页中的“缩减符号范围”。
外部版本控制
对共享库的运行时引用应该始终引用文件的版本文件名。版本文件名通常表示为带有版本
号后缀的文件名。由于共享库的接口以不兼容的方式变化,这种变化会造成无法继续使用
旧应用程序,因此,应该使用新的版本化文件名来分发新的共享库。此外,仍必须使用原
来的版本化文件名分发旧的共享库,以提供旧应用程序所需的接口。
针对多个软件发行版生成应用程序时,应当在运行时环境中通过单独的版本化文件名来提
供共享库。这样可以保证生成应用程序所依据的接口可用,以使应用程序在其执行过程中
可以绑定到此接口。
下面一节介绍如何协调编译环境和运行时环境之间的接口绑定。
协调版本化文件名
在链接编辑过程中,输入共享库的最常见方法是使用-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。
外部版本控制
168 链接程序和库指南• 2006 年10 月
针对多个软件发行版,可以分发具有已更改接口的此共享库的新版本。可以将编译环境构
建为通过更改符号链接即可使用适用的接口。例如:
$ ls -l libfoo*
lrwxrwxrwx 1 usr grp 11 1993libfoo.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
外部版本控制
第5 章• 应用程序二进制接口与版本控制169
$ 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 版本的唯一原因是标记不兼容的更改,因此在一个进
程中具有该目标文件的两个版本可能会导致错误的符号绑定并由此导致不需要的交互。
外部版本控制
170 链接程序和库指南• 2006 年10 月
支持接口
链接编辑器提供了许多支持接口,用于实现链接编辑器的监视和修改功能以及运行时链接
程序处理功能。要使用这些接口,除了了解前几章中所介绍的概念之外,通常还需要对链
接编辑的概念有更深入的了解。本章介绍了以下接口:
ld-support – 第171 页中的“链接编辑器支持接口”
rtld-audit – 第178 页中的“运行时链接程序审计接口”
rtld-debugger – 第188 页中的“运行时链接程序调试器接口”
链接编辑器支持接口
链接编辑器可执行许多操作,其中包括打开文件以及串联这些文件中的各节。监视并不时
修改这些操作通常会对编译系统的各组件有利。
本节介绍了ld-support 接口,通过此接口可以检查输入文件,并在某种程度上还可以修改链
接编辑过程中所用到的那些文件的输入文件数据。使用此接口的两个应用程序分别为链接
编辑器本身(使用此接口处理可重定位目标文件内的调试信息)以及make(1S) 实用程序
(使用此接口保存状态信息)。
ld-support 接口由提供一个或多个支持接口例程的支持库组成。该库是在链接编辑过程中装
入的。在链接编辑的不同阶段会调用该库中的某些支持例程。
使用此接口时,应该熟悉elf(3ELF) 结构和文件格式。
调用支持接口
链接编辑器可接受一个或多个通过SGS_SUPPORT 环境变量或链接编辑器的-S 选项提供的支
持库。此环境变量由冒号分隔的支持库列表组成:
$ SGS_SUPPORT=./support.so.1:libldstab.so.1 cc ...
-S 选项用于指定单个支持库。可以指定多个-S 选项:
6第6 章
171
$ LD_OPTIONS="-S./support.so.1 -Slibldstab.so.1" cc ...
支持库是共享库。链接编辑器会使用dlopen(3C) 按照指定库的顺序打开每个支持库。如果
遇到环境变量和-S 选项,则首先处理通过此环境变量指定的支持库。然后,使用dlsym(3C)
搜索每个支持库以查找所有支持接口例程。这些支持例程随后会在链接编辑的不同阶段调
用。
支持库必须与所调用的链接编辑器的ELF 类保持一致,可以是32 位或64 位。有关更多详
细信息,请参见第172 页中的“32 位环境和64 位环境”。
注– 缺省情况下,链接编辑器使用Solaris 支持库libldstab.so.1 来处理和压缩输入可重定位
目标文件内提供的编译器生成的调试信息。如果对使用-S 选项指定的任何支持库调用链接
编辑器,则抑制此缺省处理。如果除支持库服务之外还要求libldstab.so.1 的缺省处理,
请将libldstab.so.1 显式添加到提供给链接编辑器的支持库列表中。
32 位环境和64 位环境
如第24 页中的“32 位环境和64 位环境”中所述,64 位链接编辑器ld(1) 可以生成32 位目
标文件,32 位链接编辑器可以生成64 位目标文件。对于其中每个目标文件,都定义关联支
持接口。
64 位目标文件的支持接口类似于32 位目标文件的接口,但是以64 为后缀结尾。例如
ld_start() 和ld_start64()。通过此约定,两种方式实现的支持接口可以分别位于32 位类
和64 位类的单个共享库libldstab.so.1 中。
可以为SGS_SUPPORT 环境变量指定_32 或_64 后缀,并且可以使用链接编辑器选项-z ld32
和-z ld64 定义-S 选项的要求。这些定义只能分别通过链接编辑器的32 位或64 位类来解
释。通过此操作,可在可能不知道链接编辑器的类的情况下指定两类支持库。
支持接口函数
所有ld-support 接口均在头文件link.h 中定义。所有接口参数均为基本的C 类型或ELF 类
型。可以通过ELF 访问库libelf 检查ELF 数据类型。有关libelf 内容的说明,请参见
elf(3ELF)。以下接口函数由ld-support 接口提供,并且按照预期的使用顺序进行了说明。
ld_version()
此函数提供了链接编辑器与支持库之间的初次握手。
uint_t ld_version(uint_t version);
链接编辑器使用其可以支持的最高版本的ld-support 接口来调用此接口。支持库可以检验
此版本是否达到使用的最低要求,并返回支持库要求使用的版本。此版本通常为
LD_SUP_VCURRENT。
如果支持库没有提供此接口,则采用初始支持级别LD_SUP_VERSION1。
如果支持库返回版本零,或者返回的版本值高于链接编辑器所支持的ld-support 接口的版
本,则无法使用支持库。
链接编辑器支持接口
172 链接程序和库指南• 2006 年10 月
ld_start()
此函数在初始验证链接编辑器命令行之后调用,表示开始处理输入文件。
void ld_start(const char * name, const Elf32_Half type,
const char * caller);
void ld_start64(const char * name, const Elf64_Half type,
const char * caller);
name 是所创建的输出文件名。type 是输出文件类型,可以为ET_DYN、ET_REL 或
ET_EXEC,如sys/elf.h 中所定义。caller 是调用接口的应用程序,通常为
/usr/ccs/bin/ld。
ld_file()
执行任何文件数据处理之前,会针对每个输入文件调用此函数。
void ld_file(const char * name, const Elf_Kind kind, int flags,
Elf * elf);
void ld_file64(const char * name, const Elf_Kind kind, int flags,
Elf * elf);
name 是要处理的输入文件。kind 表示输入文件类型,可以是ELF_K_AR 或ELF_K_ELF,如
libelf.h 中所定义。flags 表示链接编辑器获取文件的方式,可以是以下一个或多个定义
:
LD_SUP_DERIVED-文件名不是在命令行中显式指定的。文件是从-l 扩展派生而来,或
者文件标识提取的归档成员。
LD_SUP_EXTRACTED-文件提取自归档。
LD_SUP_INHERITED-文件作为命令行共享库的依赖项获取。
如果未指定flags 值,则表明已在命令行中显式指定了输入文件。elf 是指向文件的ELF 描
述符的指针。
ld_input_section()
此函数会针对输入文件的每一节调用,并且在链接编辑器确定是否应将节传播给输出文
件之前即会调用此函数。此函数不同于ld_section() 处理,后者仅针对组成输出文件的
各节进行调用。
void ld_input_section(const char * name, Elf32_Shdr ** shdr,
链接编辑器支持接口