链接程序和库指南

符号处理

在输入文件处理期间,输入可重定位目标文件中的所有局部符号都将传递到输出文件映像。所有全局符号都在链接编辑器内部累积。在此内部符号表中搜索可重定位目标文件提供的每个全局符号。 如果遇到了名称与上一个输入文件中某个符号的名称相同的符号,则调用符号解析过程。符号解析过程决定这两项中哪一项会被保留。

完成输入文件处理并且没有发生致命符号解析错误时,链接编辑器将确定是否存在任何未解析的符号引用。未解析的符号引用可能导致链接编辑终止。

最后,将链接编辑器的内部符号表添加到要创建的映像的符号表中。

以下各小节详细说明了符号解析和未定义符号处理过程。

符号解析

符号解析的方式很广,有简单直观的,也有错综复杂的。大多数解析由链接编辑器执行,且没有任何提示。但是,某些重定位可能伴随有警告诊断,而其他重定位可能导致致命错误状态。

这两种符号的解析取决于符号的属性、提供符号的文件的类型以及要生成的文件的类型。有关符号属性的完整说明,请参见符号表节。但是,对于以下论述,标识了三种基本符号类型:

符号解析最简单的形式涉及使用优先级关系。此关系中,已定义符号优先于暂定符号,而暂定符号又优先于未定义符号。

以下 C 代码示例说明如何生成这些符号类型。未定义符号使用 u_ 作为前缀。暂定符号使用 t_ 作为前缀。已定义符号使用 d_ 作为前缀。


$ cat main.c

extern int      u_bar;

extern int      u_foo();



int             t_bar;

int             d_bar = 1;



d_foo()

{

        return (u_foo(u_bar, t_bar, d_bar));

}

$ cc -o main.o -c main.c

$ nm -x main.o



[Index]   Value      Size      Type  Bind  Other Shndx   Name

...............

[8]     |0x00000000|0x00000000|NOTY |GLOB |0x0  |UNDEF  |u_foo

[9]     |0x00000000|0x00000040|FUNC |GLOB |0x0  |2      |d_foo

[10]    |0x00000004|0x00000004|OBJT |GLOB |0x0  |COMMON |t_bar

[11]    |0x00000000|0x00000000|NOTY |GLOB |0x0  |UNDEF  |u_bar

[12]    |0x00000000|0x00000004|OBJT |GLOB |0x0  |3      |d_bar

简单解析

到目前为止,简单符号解析是最常见的一种解析形式。在这种情况下,将检测两个具有类似特征的符号,一个符号优先于另一个符号。此符号解析由链接编辑器执行,且没有任何提示。例如,对于具有相同绑定的符号,一个文件中的符号引用将绑定到另一个文件中已定义或暂定符号定义。或者,一个文件中的暂定符号定义将绑定到另一个文件中的已定义符号定义。

要解析的符号可以具有全局绑定或弱绑定。弱绑定的优先级低于全局绑定,因此,将根据略有改变的基本规则解析具有不同绑定的符号。

通常可以通过编译器单独定义弱符号或将它们定义为全局符号的别名。一种机制使用 #pragma 定义:


$ cat main.c

#pragma weak    bar

#pragma weak    foo = _foo



int             bar = 1;



_foo()

{

        return (bar);

}

$ cc -o main.o -c main.c

$ nm -x main.o

[Index]   Value      Size      Type  Bind  Other Shndx   Name

...............

[7]     |0x00000000|0x00000004|OBJT |WEAK |0x0  |3      |bar

[8]     |0x00000000|0x00000028|FUNC |WEAK |0x0  |2      |foo

[9]     |0x00000000|0x00000028|FUNC |GLOB |0x0  |2      |_foo

请注意,对弱别名 foo 指定了与全局符号 _foo 相同的属性。此关系由链接编辑器维护,并将导致在输出映像中对符号指定相同的值。在符号解析过程中,定义的弱符号会被名称相同的任何全局定义覆盖,且没有任何提示。

在可重定位目标文件与共享库之间或者多个共享库之间进行的另一种形式的简单符号解析是插入。在这些情况下,如果多重定义了某个符号,则链接编辑器会采用可重定位目标文件或多个共享库之间的第一个定义,且不作任何提示。可重定位目标文件的定义或第一个共享库的定义插入进来取代所有其他定义。这种插入可用于覆盖其他共享库提供的功能。

弱符号和符号插入的组合可以提供有用的编程方法。例如,标准 C 库提供了多个允许您重新定义的服务。但是,ANSI C 定义了一组必须出现在系统中的标准服务。在严格遵循规则的程序中不能替换这些服务。

例如,函数 fread(3C) 是一个 ANSI C 库函数,而系统函数 read(2) 不是。遵循 ANSI 的C 程序必须可以重新定义 read(2),并且仍以可预测的方法使用 fread(3C)

此处的问题是,在标准 C 库中 read(2)fread(3C) 实现的基础。因此,重新定义 read(2) 的程序可能会混淆 fread(3C) 实现。为了避免出现这种情况,ANSI C 声明实现只能使用为该实现保留的名称。使用以下 #pragma 指令可定义这种保留名称。使用此名称可生成函数 read(2) 的别名。


#pragma weak read = _read

因此,您可以非常自由地定义自己的 read() 函数,而不会破坏 fread(3C) 实现,而 fread(3C) 是使用 _read() 函数实现的。

链接编辑器在链接共享库或标准 C 库的归档版本时,可以轻松重新定义 read()。在前一种情况下,可以执行插入操作。在后一种情况下,由于 C 库中 read(2) 的定义较弱,所以允许默认覆盖该定义。

使用链接编辑器的 -m 选项可将所有插入的符号引用列表和节装入地址信息写入标准输出中。

复杂解析

如果发现两个符号的名称相同,但属性不同,则可以进行复杂解析。在这些情况下,链接编辑器将选择最适合的符号并同时生成一条警告消息。此消息指出符号、发生冲突的属性以及包含符号定义的文件的标识。在以下示例中,包含数据项数组定义的两个文件有不同的大小要求。


$ cat foo.c

int array[1];



$ cat bar.c

int array[2] = { 1, 2 };



$ cc -dn -r -o temp.o foo.c bar.c

ld: warning: symbol `array' has differing sizes:

        (file foo.o value=0x4; file bar.o value=0x8);

        bar.o definition taken

如果符号的对齐要求不同,则会生成一个类似的诊断。在这两种情况下,使用链接编辑器的 -t 选项可以不进行诊断。

另一种属性差异是符号的类型。在以下示例中,符号 bar() 已同时定义为数据项和函数。


$ cat foo.c

bar()

{

        return (0);

}

$ cc -o libfoo.so -G -K pic foo.c

$ cat main.c

int     bar = 1;



main()

{

        return (bar);

}

$ cc -o main main.c -L. -lfoo

ld: warning: symbol `bar' has differing types:

        (file main.o type=OBJT; file ./libfoo.so type=FUNC);

        main.o definition taken

注 –

此上下文中的符号类型是可以用 ELF 表示的类别。除非编程语言以最原始的方式使用数据类型,否则这些符号类型与数据类型无关。


在类似以上示例的情况下,在可重定位目标文件与共享库之间进行解析时将采用可重定位目标文件定义。或者,在两个共享库之间进行解析时采用第一个定义。在弱绑定符号或全局绑定符号之间进行这种解析时,还会生成警告。

链接编辑器的 -t 选项不抑制符号类型之间的不一致性。

致命解析

无法解析的符号冲突会导致致命错误状态,并生成相应的错误消息。此消息指出符号名称和提供这些符号的文件的名称。不生成输出文件。虽然致命状态足以导致链接编辑终止,但会先完成所有输入文件的处理。在此方式下,可以标识所有致命解析错误。

当两个可重定位目标文件都定义相同名称的非弱符号时,就会出现最常见的致命错误状态:


$ cat foo.c

int bar = 1;



$ cat bar.c

bar()

{ 

        return (0);

}



$ cc -dn -r -o temp.o foo.c bar.c

ld: fatal: symbol `bar' is multiply-defined:

        (file foo.o and file bar.o);

ld: fatal: File processing errors. No output written to int.o

对于符号 bar 来说,foo.cbar.c 具有相冲突的定义。因为链接编辑器无法确定哪个符号优先,所以链接编辑通常终止,并生成一条错误消息。可以使用链接编辑器的 -z muldefs 选项抑制出现此错误状态。此选项允许采用第一个符号定义。

未定义符号

在读取所有输入文件并完成所有符号解析后,链接编辑器将搜索内部符号表,以查找尚未绑定到符号定义的任何符号引用。这些符号引用称为未定义符号。这些未定义符号在链接编辑过程中的效果根据要生成的输出文件类型而不同,也可能根据符号类型而不同。

生成可执行的输出文件

生成可执行的输出文件时,如果有任何未定义符号,则链接编辑器的缺省行为是终止并生成相应的错误消息。如果可重定位目标文件中的符号引用从未与符号定义匹配,则表示此符号未定义:


$ cat main.c

extern int foo();



main()

{

        return (foo());

}

$ cc -o prog main.c

Undefined           first referenced

 symbol                 in file

foo                     main.o

ld: fatal: Symbol referencing errors. No output written to prog

同样,如果使用共享库创建动态可执行文件,并且该库中的符号引用始终未解析,则会产生未定义符号错误。


$ cat foo.c

extern int bar;

foo()

{

        return (bar);

}



$ cc -o libfoo.so -G -K pic foo.c

$ cc -o prog main.c -L. -lfoo

Undefined           first referenced

 symbol                 in file

bar                     ./libfoo.so

ld: fatal: Symbol referencing errors. No output written to prog

要允许未定义符号(与上一个示例一样),可使用链接编辑器的 -z nodefs 选项抑制出现缺省错误状态。


注 –

使用 -z nodefs 选项时应谨慎。如果在执行进程期间需要不可用的符号引用,则会发生致命的运行时重定位错误。在初始执行和测试应用程序期间可能会检测到此错误。然而,执行路径越复杂,检测此错误状态需要的时间就越长,这将非常耗时且开销很大。


将可重定位目标文件中的符号引用绑定到隐式定义的共享库中的符号定义时,符号也可以保持未定义。例如,继续使用上一个示例中使用的文件 main.cfoo.c


$ cat bar.c

int bar = 1;



$ cc -o libbar.so -R. -G -K pic bar.c -L. -lfoo

$ ldd libbar.so

        libfoo.so =>     ./libfoo.so



$ cc -o prog main.c -L. -lbar

Undefined           first referenced

 symbol                 in file

foo                     main.o  (symbol belongs to implicit \

                        dependency ./libfoo.so)

ld: fatal: Symbol referencing errors. No output written to prog

prog 是使用对 libbar.so显式引用生成的。libbar.so 依赖于 libfoo.so。因此,将从 prog 建立对 libfoo.so 的隐式引用。

因为 main.clibfoo.so 提供的接口进行特定引用,所以 prog 依赖于 libfoo.so。但是,在要生成的输出文件中将仅记录显式共享库的依赖项。因此,如果开发一种不再依赖于 libfoo.so 的新版本 libbar.so,则 prog 将无法运行。

因此,此类型的绑定被认为是致命错误。必须通过在链接编辑 prog 期间直接引用库来使隐式引用变为显式引用。前面示例中显示的致命错误消息中会提示需要的引用。

生成共享库输出文件

链接编辑器生成共享库输出文件时,允许在链接编辑结束时仍存在未定义符号。此缺省行为允许共享库从将其定义为依赖性的动态可执行文件导入符号。

可以使用链接编辑器的 -z defs 选项强制在存在任何未定义符号的情况下生成致命错误。建议在创建任何共享库时使用此选项。引用应用程序中的符号的共享库可以使用 -z defs 选项,并可以使用 extern mapfile 指令定义符号。 请参见定义其他符号

自包含的共享库(通过指定的依赖性来满足对外部符号的所有引用)可提供最大的灵活性。此共享库可由许多用户使用,并且这些用户无需确定和建立依赖性来满足共享库的要求。

弱符号

无论要生成哪种类型的输出文件,未解析的弱符号引用不会产生致命错误状态。

如果要生成静态可执行文件,则可将此符号转换为绝对符号,并指定值零。

如果要生成动态可执行文件或共享库,则可将此符号保留为未定义弱引用,并指定值零。在进程执行期间,运行时链接程序将搜索此符号。如果运行时链接程序未找到匹配项,则将此引用绑定到地址零,而不是生成致命重定位错误。

以前,这些未定义弱引用符号被用作一种机制,用于测试功能是否存在。例如,在共享库 libfoo.so.1 中可能使用了以下 C 代码段:


#pragma weak    foo



extern  void    foo(char *);



void bar(char * path)

{

        void (* fptr)(char *);



        if ((fptr = foo) != 0)

                (* fptr)(path);

}

生成引用 libfoo.so.1 的应用程序时,无论是否找到符号 foo 的定义,链接编辑都将成功完成。如果在执行此应用程序时函数地址测试为非零,则将调用此函数。但是,如果未找到符号定义,则函数地址测试将为零,因此不调用此函数。

编译系统将此地址比较方法视为未定义语义,这将导致在优化时删除测试语句。此外,运行时符号绑定机制会对使用此方法设定其他限制。这些限制防止所有动态库使用一致的模型。


注 –

建议不要按照此方式使用未定义弱引用。相反,应将 dlsym(3C)RTLD_DEFAULTRTLD_PROBE 句柄配合使用,以测试符号是否存在。 请参见测试功能


输出文件中暂定符号的顺序

构成输入文件的符号通常以这些符号的顺序出现在输出文件中。处理暂定符号及其关联的存储空间时,情况却有所不同。完成这些符号的解析后才会完全定义这些符号。如果解析可重定位目标文件中的已定义符号,则此符号出现在定义后面。

如果需要控制一组符号的顺序,则应将所有暂定定义重新定义为初始化为零的数据项。例如,与源文件 foo.c 中说明的原始顺序相比,以下暂定定义将导致在输出文件中重新排序数据项:


$ cat foo.c

char A_array[0x10];

char B_array[0x20];

char C_array[0x30];



$ cc -o prog main.c foo.c

$ nm -vx prog | grep array

[32]    |0x00020754|0x00000010|OBJT |GLOB |0x0  |15  |A_array

[34]    |0x00020764|0x00000030|OBJT |GLOB |0x0  |15  |C_array

[42]    |0x00020794|0x00000020|OBJT |GLOB |0x0  |15  |B_array

通过将这些符号定义为已初始化的数据项,这些符号在输入文件中的相对顺序将被传递到输出文件:


$ cat foo.c

char A_array[0x10] = { 0 };

char B_array[0x20] = { 0 };

char C_array[0x30] = { 0 };



$ cc -o prog main.c foo.c

$ nm -vx prog | grep array

[32]    |0x000206bc|0x00000010|OBJT |GLOB |0x0  |12  |A_array

[42]    |0x000206cc|0x00000020|OBJT |GLOB |0x0  |12  |B_array

[34]    |0x000206ec|0x00000030|OBJT |GLOB |0x0  |12  |C_array

定义其他符号

除输入文件中提供的符号外,还可以为链接编辑提供其他符号引用或定义。使用链接编辑器的 -u 选项可以生成符号引用的最简单形式。链接编辑器的 -M 选项和关联的 mapfile 可以提供更大的灵活性。使用此 mapfile,可以定义符号引用和各种符号定义。

-u 选项提供一种在链接编辑命令行中生成符号引用的机制。可以使用此选项完全从归档执行链接编辑。选择要从多个归档中提取的目标文件时,此选项还可以提供更多灵活性。有关归档提取的概述,请参见归档处理一节。

例如,您可能要从可重定位目标文件 main.o 生成动态可执行文件,此目标文件引用符号 foobar。您要从 lib1.a 中包含的可重定位目标文件 foo.o 获取符号定义 foo,并从 lib2.a 中包含的可重定位目标文件 bar.o 获取符号定义 bar

但是,归档 lib1.a 还包含定义符号 bar 的可重定位目标文件。对于 lib2.a 中提供的可重定位目标文件,此可重定位目标文件的功能可能不同。要指定需要的归档提取,可以使用以下链接编辑:


$ cc -o prog -L. -u foo -l1 main.o -l2

-u 选项生成对符号 foo 的引用。此引用导致从归档 lib1.a 中提取可重定位目标文件 foo.o。对符号 bar 的第一次引用出现在 main.o 中,这是在处理了 lib1.a 之后遇到的。因此,将从归档 lib2.a 中获取可重定位目标文件 bar.o


注 –

此简单示例假定 lib1.a 中的可重定位目标文件 foo.o 既没有直接引用也没有间接引用符号 bar。如果 lib1.a 引用 bar,则在处理 lib1.a 期间还会从中提取可重定位目标文件 bar.o。有关链接编辑器处理归档多遍的论述,请参见归档处理


可以使用链接编辑器的 -M 选项和关联的 mapfile 提供一组更丰富的符号定义。符号定义 mapfile 的项使用以下语法。


[ name ] {

      scope:

            symbol [ = [ type ] [ value ] [ size ] [ attribute ] ];

} [ dependency ];
name

此组符号定义的标签(如果存在)标识映像中的版本定义。 请参见第 5 章,应用程序二进制接口与版本控制

scope

指示要生成的输出文件中符号绑定的可见性。在链接编辑过程中,使用 mapfile 定义的所有符号都将视为全局符号。将针对任何其他具有相同名称的符号(从所有输入文件中获取)解析这些符号。以下定义和别名定义在要创建的目标文件中符号的可见性:

default / global

此范围的符号对所有外部目标文件都可见。在运行时绑定从目标文件内部对这种符号的引用,从而允许进行插入。

protected / symbolic

此范围的符号对所有外部目标文件都可见。在链接编辑时绑定从目标文件内部对这种符号的引用,从而防止在运行时插入。此范围定义与具有 STV_PROTECTED 可见性的符号产生相同的效果。 请参见表 7–20

hidden / local

此范围的符号缩减为具有本地绑定的符号。此范围的符号对其他外部目标文件不可见。此范围定义与具有 STV_HIDDEN 可见性的符号产生相同的效果。 请参见表 7–20

eliminate

此范围的符号是 hidden。删除这些符号的符号表项。

symbol

所需符号的名称。如果该名称未后跟符号属性 typevaluesizeextern 之一,则将创建符号引用。此引用与本节前面所述的使用 -u 选项生成的引用完全相同。如果符号名称后跟任何符号属性,则将使用关联的属性生成符号定义。

当符号属于 local 范围时,可以将此符号名称定义为特殊自动缩减指令 "*"。此指令将任何 mapfile 中未显式定义为 global 的所有全局符号降级为要生成的动态库中的本地绑定。

type

指示符号类型属性。此属性可以为 datafunctionCOMMON。前两种类型的属性会产生绝对符号定义。 请参见符号表节。后一种类型的属性会产生暂定符号定义。

value

指示值属性。此属性的格式为 Vnumber

size

指示大小属性。此属性的格式为 Snumber

attribute

此关键字提供符号的其他属性:

EXTERN

指示在外部对要创建的目标文件定义符号。 此属性可以与 DIRECTNODIRECT 属性配合使用,以建立单独的直接或非直接引用。还可以使用此选项抑制将使用 -z defs 选项标记的未定义符号。

DIRECT

指示应直接绑定到此符号。此属性可以与 EXTERN 属性配合使用以控制绑定到外部符号。 请参见直接绑定

NODIRECT

指示不应直接绑定到此符号。此状态适用于要创建的目标文件中的引用或外部引用中的引用。此属性可以与 EXTERN 属性配合使用以控制绑定到外部符号。 请参见直接绑定

FILTER name

指示此符号在共享库 name 中是一个过滤器。 请参见生成标准过滤器。过滤器符号不需要输入可重定位目标文件提供任何后备实现。因此,使用此指令并定义符号的类型可以创建绝对符号表项。

AUXILIARY name

指示此符号在共享库 name 中是一个辅助过滤器。 请参见生成辅助过滤器

dependency

表示此定义继承的版本定义。 请参见第 5 章,应用程序二进制接口与版本控制

如果指定了版本定义或自动缩减指令,则将在创建的映像中记录版本控制信息。如果此映像是可执行文件或共享库,则还会应用任何符号缩减。

如果要创建的映像是可重定位目标文件,则缺省情况下不会应用符号缩减。在这种情况下,任何符号缩减都将记录在版本控制信息中。当最终使用可重定位目标文件来生成可执行文件或共享库时,将应用这些符号缩减。在生成可重定位目标文件时,可以使用链接编辑器的 -B reduce 选项强制执行符号缩减。

第 5 章,应用程序二进制接口与版本控制中提供了版本控制信息的更详细说明。


注 –

为了确保接口定义的稳定性,定义符号名称时不提供通配符扩展功能。


以下各小节提供了多个使用 mapfile 语法的示例。

定义符号引用

以下示例说明如何定义三种符号引用。然后,使用这些引用提取归档成员。虽然可以通过对链接编辑指定多个 -u 选项来实现归档提取,但此示例还说明了如何将符号的最终范围缩减到局部


$ cat foo.c

foo()

{

        (void) printf("foo: called from lib.a\n");

}

$ cat bar.c

bar()

{

        (void) printf("bar: called from lib.a\n");

}

$ cat main.c

extern  void    foo(), bar();



main()

{

        foo();

        bar();

}

$ ar -rc lib.a foo.o bar.o main.o

$ cat mapfile

{

        local:

                foo;

                bar;

        global:

                main;

};

$ cc -o prog -M mapfile lib.a

$ prog

foo: called from lib.a

bar: called from lib.a

$ nm -x prog | egrep "main$|foo$|bar$"

[28]    |0x00010604|0x00000024|FUNC |LOCL |0x0  |7      |foo

[30]    |0x00010628|0x00000024|FUNC |LOCL |0x0  |7      |bar

[49]    |0x0001064c|0x00000024|FUNC |GLOB |0x0  |7      |main

缩减符号范围一节中更详细地说明了将符号范围从全局缩减为局部的重要性。

定义绝对符号

以下示例说明如何定义两种绝对符号定义。然后,使用这些定义解析输入文件 main.c 中的引用。


$ cat main.c

extern  int     foo();

extern  int     bar;



main()

{

        (void) printf("&foo = %x\n", &foo);

        (void) printf("&bar = %x\n", &bar);

}

$ cat mapfile

{

        global:

                foo = FUNCTION V0x400;

                bar = DATA V0x800;

};

$ cc -o prog -M mapfile main.c

$ prog

&foo = 400 &bar = 800

$ nm -x prog | egrep "foo$|bar$"

[37]    |0x00000800|0x00000000|OBJT |GLOB |0x0  |ABS    |bar

[42]    |0x00000400|0x00000000|FUNC |GLOB |0x0  |ABS    |foo

从输入文件获取函数或数据项的符号定义时,这些符号定义通常与数据存储元素关联。mapfile 定义不足以构造此数据存储,因此,这些符号必须保持为绝对值。如果在共享库中定义符号,则应避免绝对定义。 请参见扩充符号定义

定义暂定符号

还可以使用 mapfile 定义 COMMON 或暂定符号。与其他类型的符号定义不同,暂定符号在文件中不占用存储空间,而定义在运行时必须分配的存储空间。因此,定义此类型的符号有助于要生成的输出文件的存储分配。

暂定符号与其他类型的符号的一个不同特征在于,暂定符号的 value 属性指示其对齐要求。因此,可以使用 mapfile 定义重新对齐从链接编辑的输入文件中获取的暂定定义。

以下示例给出了两个暂定符号的定义。符号 foo 定义新的存储区域,而符号 bar 实际上用于更改文件 main.c 中相同暂定定义的对齐方式。


$ cat main.c

extern  int     foo;

int             bar[0x10];



main()

{

        (void) printf("&foo = %x\n", &foo);

        (void) printf("&bar = %x\n", &bar);

}

$ cat mapfile

{

        global:

                foo = COMMON V0x4 S0x200;

                bar = COMMON V0x100 S0x40;

};

$ cc -o prog -M mapfile main.c

ld: warning: symbol `bar' has differing alignments:

        (file mapfile value=0x100; file main.o value=0x4);

        largest value applied

$ prog

&foo = 20940

&bar = 20900

$ nm -x prog | egrep "foo$|bar$"

[37]    |0x00020900|0x00000040|OBJT |GLOB |0x0  |16     |bar

[42]    |0x00020940|0x00000200|OBJT |GLOB |0x0  |16     |foo

注 –

使用链接编辑器的 -t 选项可以不诊断此符号解析。


扩充符号定义

应避免在共享库中创建绝对数据符号。从动态可执行文件对共享库中数据项的外部引用通常需要创建复制重定位。 请参见复制重定位。要提供此重定位,应该在目标文件中定义数据项,以便将符号定义与数据存储关联。

可以过滤数据符号。 请参见作为过滤器的共享库。要提供此过滤,可以使用 mapfile 定义扩充目标文件定义。以下示例创建包含函数和数据定义的过滤器。虽然可以在 mapfile 中显式创建函数定义,但数据定义将扩充输入可重定位目标文件提供的定义。


$ cat bar.c

int bar = 0;

$ cat mapfile

{

        global:

                foo = FUNCTION FILTER filtee.so.1;

                bar = FILTER filtee.so.1;

        local:

                *;

};

$ cc -o filter.so.1 -G -Kpic -h filter.so.1 -M mapfile -R. bar.c

$ nm -x filter.so.1 | egrep "foo|bar"

[39]    |0x000102b0|0x00000004|OBJT |GLOB |0    |12     |bar

[45]    |0x00000000|0x00000000|FUNC |GLOB |0    |ABS    |foo

$ elfdump -y filter.so.1 | egrep "foo|bar"

       [1]  F        [0] filtee.so.1        bar

       [7]  F        [0] filtee.so.1        foo

运行时,从外部目标文件对任一符号的引用将解析为 filtee 中的定义。

缩减符号范围

可以使用在 mapfile 中定义为具有局部范围的符号定义来缩减符号的最终绑定。对于将来使用生成的文件作为其输入一部分的链接编辑,此机制删除对这些链接编辑的符号可见性。事实上,此机制可以提供准确的文件接口定义,从而限制其他目标文件可以使用的功能。

例如,假设要从文件 foo.cbar.c 生成一个简单的共享库。文件 foo.c 包含全局符号 foo,此符号提供其他目标文件可以使用的服务。文件 bar.c 包含符号 barstr,这两个符号提供共享库的基础实现。使用这些文件创建的共享库通常导致创建三个具有全局范围的符号。


$ cat foo.c

extern  const char *    bar();



const char * foo()

{

        return (bar());

}

$ cat bar.c

const char * str = "returned from bar.c";



const char * bar()

{

        return (str);

}

$ cc -o lib.so.1 -G foo.c bar.c

$ nm -x lib.so.1 | egrep "foo$|bar$|str$"

[29]    |0x000104d0|0x00000004|OBJT |GLOB |0x0  |12     |str

[32]    |0x00000418|0x00000028|FUNC |GLOB |0x0  |6      |bar

[33]    |0x000003f0|0x00000028|FUNC |GLOB |0x0  |6      |foo

现在,可以在另一个应用程序的链接编辑过程中使用 lib.so.1 提供的功能。将对符号 foo 的引用绑定到共享库提供的实现。

由于符号 barstr 具有全局绑定,因此还可以直接引用这些符号。此可见性会产生严重后果,因为您可能会在以后更改作为函数 foo 的基础的实现。这样做可能会无意中导致已绑定到 barstr 的现有应用程序失败或行为异常。

全局绑定符号 barstr 的另一个后果是,可以在这些符号中插入相同名称的符号。简单解析一节中说明了在共享库中插入符号。此插入可能是有意的,可以用来禁用共享库提供的预期功能。另一方面,此插入可能是无意的,是将相同通用符号名称同时用于应用程序和共享库的结果。

在开发共享库时,可以通过将符号 barstr 的范围缩减为本地绑定来防止出现这种情况。在以下示例中,不能再将符号 barstr 用作共享库接口。因此,外部目标文件不能引用或插入这些符号。您已经有效地定义了共享库的接口。可以在隐藏基础实现详细信息的同时管理此接口。


$ cat mapfile

{

        local:

                bar;

                str;

};

$ cc -o lib.so.1 -M mapfile -G foo.c bar.c

$ nm -x lib.so.1 | egrep "foo$|bar$|str$"

[27]    |0x000003dc|0x00000028|FUNC |LOCL |0x0  |6      |bar

[28]    |0x00010494|0x00000004|OBJT |LOCL |0x0  |12     |str

[33]    |0x000003b4|0x00000028|FUNC |GLOB |0x0  |6      |foo

缩减符号范围还具有其他性能方面的优点。现在,运行时必需的针对符号 barstr 的符号重定位已缩减为相对重定位。有关符号重定位开销的详细信息,请参见何时执行重定位

随着链接编辑期间处理的符号数的增加,在 mapfile 中定义局部范围缩减将变得越来越难维护。有一种更灵活的替代机制,可以根据应维护的全局符号定义共享库的接口。全局符号定义允许链接编辑器将所有其他符号缩减为本地绑定。可使用特殊的自动缩减指令 "*" 实现此机制。例如,可以重新编写上面的 mapfile 定义,以便将 foo 定义为生成的输出文件中需要的唯一全局符号:


$ cat mapfile

lib.so.1.1

{

        global:

                foo;

        local:

                *;

};

$ cc -o lib.so.1 -M mapfile -G foo.c bar.c

$ nm -x lib.so.1 | egrep "foo$|bar$|str$"

[30]    |0x00000370|0x00000028|FUNC |LOCL |0x0  |6      |bar

[31]    |0x00010428|0x00000004|OBJT |LOCL |0x0  |12     |str

[35]    |0x00000348|0x00000028|FUNC |GLOB |0x0  |6      |foo

此示例还将版本名称 lib.so.1.1 定义为 mapfile 指令的一部分。此版本名称建立一个内部版本定义,用于定义文件的符号接口。建议创建版本定义。此定义将成为可在文件演变过程中使用的内部版本控制机制的基础。 请参见第 5 章,应用程序二进制接口与版本控制


注 –

如果未提供版本名称,则将使用输出文件名标记版本定义。可以使用链接编辑器的 -z noversion 选项抑制在输出文件中创建的版本控制信息。


每次指定版本名称时,必须将所有全局符号指定给版本定义。如果有任何全局符号未指定给版本定义,则链接编辑器将生成致命错误状态:


$ cat mapfile

lib.so.1.1 {

        global:

                foo;

};

$ cc -o lib.so.1 -M mapfile -G foo.c bar.c

Undefined           first referenced

 symbol                 in file

str                     bar.o  (symbol has no version assigned)

bar                     bar.o  (symbol has no version assigned)

ld: fatal: Symbol referencing errors. No output written to lib.so.1

可以使用 -B local 选项在命令行中声明自动缩减指令 "*"。使用以下指令可以成功编译上一个示例:


$ cc -o lib.so.1 -M mapfile -B local -G foo.c bar.c

生成可执行文件或共享库时,缩减任何符号都会导致在输出映像中记录版本定义。生成可重定位目标文件时,将创建版本定义,但不会处理符号缩减。结果是所有符号缩减的符号项仍保持全局。例如,将上一个 mapfile 与自动缩减指令和关联的可重定位目标文件配合使用时,会创建一个中间可重定位目标文件,但不缩减任何符号。


$ cat mapfile

lib.so.1.1 {

        global:

                foo;

        local:

                *;

};

$ ld -o lib.o -M mapfile -r foo.o bar.o

$ nm -x lib.o | egrep "foo$|bar$|str$"

[17]    |0x00000000|0x00000004|OBJT |GLOB |0x0  |3      |str

[19]    |0x00000028|0x00000028|FUNC |GLOB |0x0  |1      |bar

[20]    |0x00000000|0x00000028|FUNC |GLOB |0x0  |1      |foo

此映像中创建的版本定义显示需要缩减符号。最终使用可重定位目标文件生成可执行文件或共享库时,将会缩减符号。也就是说,链接编辑器将按照与在 mapfile 中处理版本控制数据相同的方式,来读取并解释可重定位目标文件中包含的符号缩减信息。

因此,现在可以使用上一个示例中产生的中间可重定位目标文件来生成共享库:


$ ld -o lib.so.1 -G lib.o

$ nm -x lib.so.1 | egrep "foo$|bar$|str$"

[22]    |0x000104a4|0x00000004|OBJT |LOCL |0x0  |14     |str

[24]    |0x000003dc|0x00000028|FUNC |LOCL |0x0  |8      |bar

[36]    |0x000003b4|0x00000028|FUNC |GLOB |0x0  |8      |foo

创建可执行文件或共享库时缩减符号通常是最常见的要求。不过,使用链接编辑器的 -B reduce 选项可以强制在创建可重定位目标文件时缩减符号。


$ ld -o lib.o -M mapfile -B reduce -r foo.o bar.o

$ nm -x lib.o | egrep "foo$|bar$|str$"

[15]    |0x00000000|0x00000004|OBJT |LOCL |0x0  |3      |str

[16]    |0x00000028|0x00000028|FUNC |LOCL |0x0  |1      |bar

[20]    |0x00000000|0x00000028|FUNC |GLOB |0x0  |1      |foo

删除符号

对符号缩减的扩展是指从目标文件的符号表中删除符号项。局部符号仅在目标文件的 .symtab 符号表中维护。可以使用链接编辑器的 -s 选项或 strip(1) 从目标文件中删除整个表。有时,您可能需要保留 .symtab 符号表,但删除选择的局部符号定义。

可以使用 mapfile 指令 eliminate 删除符号。与 local 指令一样,可以单独定义符号。或者,可以将符号名称定义为特殊的自动删除指令 "*"。以下示例说明如何删除上一个符号缩减示例中的符号 bar


$ cat mapfile

lib.so.1.1

{

        global:

                foo;

        local:

                str;

        eliminate:

                *;

};

$ cc -o lib.so.1 -M mapfile -G foo.c bar.c

$ nm -x lib.so.1 | egrep "foo$|bar$|str$"

[31]    |0x00010428|0x00000004|OBJT |LOCL |0x0  |12     |str

[35]    |0x00000348|0x00000028|FUNC |GLOB |0x0  |6      |foo

可以使用 -B eliminate 选项在命令行中声明自动删除指令 "*"。

外部绑定

共享库中的定义满足要创建的目标文件中的符号引用时,符号将保持未定义状态。可以在运行时查找与此符号关联的重定位信息。提供定义的共享库通常具有依赖性。

运行时链接程序在运行时使用缺省搜索模型来查找此定义。通常,将搜索每个目标文件(从动态可执行文件开始),并按装入目标文件的顺序处理每个依赖性。

还可以创建目标文件以使用直接绑定。使用此方法时,可以在要创建的目标文件中维护符号引用与提供符号定义的目标文件之间的关系。运行时链接程序使用此信息将引用直接绑定到定义符号的目标文件,从而绕过缺省符号搜索模型。 请参见直接绑定

字符串表压缩

链接编辑器可以通过删除重复项和尾部子串来压缩字符串表。此压缩可显著减小任何字符串表的大小。压缩的 .dynstr 表的文本段较小,因此可以减少运行时换页活动。由于这些优点,缺省情况下将启用字符串表压缩。

由于字符串表压缩,提供大量符号的目标文件可能会增加链接编辑时间。为了避免开发期间产生此成本,应使用链接编辑器的 -z nocompstrtab 选项。可以使用链接编辑器的调试标记 -D strtab,detail 显示链接编辑期间执行的任何字符串表压缩。