链接程序和库指南

内部版本控制

一个动态库可以具有一个或多个与之关联的内部版本定义。每个版本定义通常与一个或多个符号名称关联。符号名称则只能与一个版本定义关联。但是,一个版本定义可以继承其他版本定义的符号。这样,便存在一个结构,用于定义正在创建的目标文件内部的一个或多个独立或者相关的版本定义。对目标文件进行新的更改后,可以添加新版本定义来反映这些更改。

提供共享库内的版本定义有两种结果:

创建版本定义

版本定义通常由符号名称与唯一版本名称关联组成。这些关联在 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.cdata.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

符号 foo1foo2 都定义为共享库公共接口的一部分。但是,其中每个符号都指定给不同的版本定义;foo1 指定给 SUNW_1.1foo2 指定给 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 选项时将显示这些符号。

创建弱版本定义 (weak version 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:

                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.cbar2.c 添加到目标文件 libfoo.so.1 中。这两个文件分别提供新符号 bar1bar2


$ 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.1SUNW_1.2SUNW_1.3a 以及 SUNW_1.3b 这四个接口定义导出的符号名称。还有一个接口 SUNW_1.2.1 介绍共享库的内部实现更改,另一个接口 libfoo.so.1 定义一些保留标号。使用此共享库作为依赖项而创建的动态库将记录其绑定到的接口的版本名称。

以下示例将创建引用符号 foo1foo2 的应用程序。可以使用带有 -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.1SUNW_1.2。这两个接口分别提供全局符号 foo1foo2

由于在 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) 依赖项将被忽略而无任何提示。例如,如果应用程序 proglibfoo.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)。 请参见重定位错误

记录版本定义依赖项也可以即时指出应用程序所需接口的可用性。

如果应用程序 proglibfoo.so.1 只包含版本定义 SUNW_1.1SUNW_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 ];

此绑定控制在下面的情况下很有用:

以下示例说明了版本控制机制的用法。此示例使用包含以下版本接口定义的共享库 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.1SUNW_1.2 表示 libfoo.so.1 内的接口,这些接口分别在软件 Release XRelease 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_ASTAND_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 选项显式绑定到某个版本定义。使用此选项可以引用版本定义符号。但是,符号引用是不可选择的。如果是处理多个依赖项(包含多个类似命名的版本定义)时,则此方法可能不足以创建显式绑定。


版本稳定性

只有当单个版本定义在目标文件的生命周期内保持不变时,绑定到该目标文件内版本的各种模型才会保持不变。

创建目标文件的版本定义并将其公开后,此定义就必须在该目标文件的后续发行版中保持未更改状态。版本名称以及关联的符号必须保持不变。因此,不支持对版本定义内定义的符号名称进行通配符扩展。在目标文件演变过程中,与通配符匹配的符号数可能会有所不同。

可重定位目标文件

可以在动态库内记录并使用版本信息。可重定位目标文件可用类似的方式维护版本控制信息。但是,在如何使用此信息方面存在一些细微差异。

记录提供给可重定位目标文件链接编辑的任何版本定义时采用的格式与生成动态可执行文件或共享库时采用的格式相同。但是,缺省情况下,不会对正在创建的目标文件执行符号缩减。相反,当最终使用可重定位目标文件作为生成动态库的输入时,将使用版本记录来确定要应用的符号缩减。

此外,在可重定位目标文件中找到的所有版本定义都会传播到该动态库。有关可重定位目标文件中版本处理的示例,请参见缩减符号范围