链接程序和库指南

符号解析

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

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

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

以下 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 选项抑制出现此错误状态。此选项允许采用第一个符号定义。