在输入文件处理期间,输入可重定位目标文件中的所有局部符号都将传递到输出文件映像。输入可重定位目标文件的所有全局符号以及共享目标文件依赖项的所有全局符号都在链接编辑器内部累积。
可以在此内部符号表中搜索输入文件提供的每个全局符号。如果遇到与上一个输入文件中名称相同的符号,则将调用符号解析过程。此符号解析过程决定保留可重定位目标文件两项中的哪一项。此解析过程还会决定共享目标文件依赖项外部引用的建立方式。
完成输入文件处理后,如果没有发生符号解析致命错误,链接编辑器会决定是否保留任何未解析的符号引用。未解析的符号引用可能导致链接编辑终止。
最后,将链接编辑器的内部符号表添加到要创建的映像的符号表中。
以下各小节详细说明了符号解析和未定义符号处理过程。
符号解析的方式很广,有简单直观的,也有错综复杂的。大多数解析由链接编辑器执行,且没有任何提示。但是,某些重定位可能伴随有警告诊断,而某些可能导致致命错误状态。
最常见的简单解析方式需要将一个目标文件中的符号引用与另一个目标文件中的符号定义绑定起来。此种绑定可用于两个可重定位目标文件之间,也可用于一个可重定位目标文件与在共享目标文件依赖项中找到的第一个定义之间。复杂解析方式通常用于两个或多个可重定位目标文件之间。
这两种符号解析方式取决于符号的属性、提供符号的文件类型以及要生成的文件类型。有关符号属性的完整说明,请参见符号表节。但是,对于以下论述,标识了三种基本符号类型。
形式最简单的符号解析需要使用优先级关系。此关系中,已定义符号优先于暂定符号,而暂定符号又优先于未定义符号。
以下 C 代码示例说明如何生成这些符号类型。未定义符号使用 u_ 作为前缀。暂定符号使用 t_ 作为前缀。已定义符号使用 d_ 作为前缀。
$ cat main.c extern int u_bar; extern int u_foo(); int t_bar; int d_bar = 1; int d_foo() { return (u_foo(u_bar, t_bar, d_bar)); } $ cc -o main.o -c main.c $ elfdump -s main.o Symbol Table Section: .symtab index value size type bind oth ver shndx name .... [7] 0x00000000 0x00000000 FUNC GLOB D 0 UNDEF u_foo [8] 0x00000010 0x00000040 FUNC GLOB D 0 .text d_foo [9] 0x00000004 0x00000004 OBJT GLOB D 0 COMMON t_bar [10] 0x00000000 0x00000004 NOTY GLOB D 0 UNDEF u_bar [11] 0x00000000 0x00000004 OBJT GLOB D 0 .data d_bar
简单符号解析是迄今最常见的解析方式。在这种情况下,将检测两个具有类似特征的符号,一个符号优先于另一个符号。此符号解析由链接编辑器执行,且没有任何提示。例如,对于具有相同绑定的符号,一个文件中的符号引用将绑定到另一个文件中的已定义或暂定符号定义。或者,一个文件中的暂定符号定义将绑定到另一个文件中的已定义符号定义。此种解析方式可用于两个可重定位目标文件之间,也可用于一个可重定位目标文件与在共享目标文件依赖项中找到的第一个定义之间。
要解析的符号可以具有全局绑定或弱绑定。处理可重定位目标文件时,弱绑定的优先级低于全局绑定。弱符号定义将在不给出任何提示的情况下被同一名称的全局定义所覆盖。
在可重定位目标文件与共享目标文件之间或者多个共享目标文件之间,还有一种简单符号解析方式,即插入。在这些情况下,如果多重定义了某个符号,链接编辑器会采用可重定位目标文件或多个共享目标文件之间的第一个定义,且不作任何提示。可重定位目标文件的定义或第一个共享目标文件的定义会在其他所有定义上插入。这种插入可用于覆盖其他共享目标文件提供的功能。可重定位目标文件与共享目标文件之间或多个共享目标文件之间的多重定义符号采用同样的处理方式。符号是弱绑定还是全局绑定无关紧要。无论哪种符号绑定,链接编辑器和运行时链接程序都行为一致,将其解析为第一个定义。
使用链接编辑器的 -m 选项可将所有插入的符号引用列表和节装入地址信息写入到标准输出中。
如果发现两个符号的名称相同,但属性不同,则将进行复杂解析。在这些情况下,链接编辑器将选择最适合的符号同时生成一条警告消息。此消息指出符号、发生冲突的属性以及包含符号定义的文件的标识。在以下示例中,包含数据项数组定义的两个文件有不同的大小要求。
$ cat foo.c int array[1]; $ cat bar.c int array[2] = { 1, 2 }; $ ld -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 int bar() { return (0); } $ cc -o libfoo.so -G -K pic foo.c $ cat main.c int bar = 1; int 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 表示的分类。除非编程语言以最原始的方式使用数据类型,否则这些符号类型与数据类型无关。
在类似以上示例的情况下,在可重定位目标文件与共享目标文件之间进行解析时将采用可重定位目标文件定义。或者,在两个共享目标文件之间进行解析时采用第一个定义。在弱绑定符号或全局绑定符号之间进行这种解析时,还会生成警告。
无法解析的符号冲突会导致致命错误状态,并生成相应的错误消息。此消息会指示符号名称以及提供符号的文件的名称。不会生成任何输出文件。虽然致命状态足以导致链接编辑终止,但会先完成所有输入文件处理。通过这种方式,所有致命解析错误都可识别。
当两个可重定位目标文件都定义相同名称的非弱符号时,就会出现最常见的致命错误状态。
$ cat foo.c int bar = 1; $ cat bar.c int bar() { return (0); } $ ld -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.c 和 bar.c 具有相冲突的定义。因为链接编辑器无法确定哪个符号优先,所以链接编辑通常会终止,并生成一条错误消息。可以使用链接编辑器的 -z muldefs 选项抑制出现此错误状态。此选项允许采用第一个符号定义。
在读取了所有输入文件并完成了所有符号解析后,链接编辑器将搜索内部符号表,以查找尚未绑定到符号定义的任何符号引用。这些符号引用称为未定义符号。未定义符号对链接编辑过程的影响因要生成的输出文件类型以及符号类型而异。
生成可执行输出文件时,如果有任何未定义符号,链接编辑器的缺省行为是终止并生成相应的错误消息。如果可重定位目标文件中的符号引用从未与符号定义匹配,则符号将保持未定义状态。
$ cat main.c extern int foo(); int 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; int 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.c 和 foo.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.c 对 libfoo.so 提供的接口进行特定引用,所以 prog 确实依赖于 libfoo.so。但是,在要生成的输出文件中将仅记录显式共享目标文件的依赖项。因此,如果开发一种不再依赖于 libfoo.so 的新版本 libbar.so,prog 将无法运行。
因此,此类型的绑定被认为是致命错误。必须通过在链接编辑 prog 期间直接引用库来使隐式引用变为显式引用。前面示例中显示的致命错误消息中会提示需要的引用。
链接编辑器在生成共享目标文件输出文件时,允许在链接编辑结束时仍存在未定义符号。此缺省行为允许共享目标文件从将其定义为依赖项的动态可执行文件导入符号。
可以使用链接编辑器的 -z defs 选项在存在任何未定义符号的情况下强制生成致命错误。建议在创建任何共享目标文件时使用此选项。引用应用程序中符号的共享目标文件可以使用 -z defs 选项,并可以使用 extern mapfile 指令定义符号。请参见SYMBOL_SCOPE / SYMBOL_VERSION 指令。
自包含的共享目标文件(其中的所有对外部符号的引用都通过指定的依赖项得到满足)可提供最大的灵活性。此共享目标文件可由许多用户使用,并且这些用户无需确定和建立依赖项来满足共享目标文件的要求。
以前,使用弱符号来禁用插入,或对可选功能进行测试。但是,经验表明,弱符号在当前编程环境中易变化且不可靠,建议不要使用它们。
在系统共享目标文件中经常使用弱符号别名。目的在于提供替代接口名称,通常符号名称使用 "_" 字符作为前缀。可从其他系统共享目标文件中引用该别名,以避免由于应用程序导出其自己的符号名称实现而导致的插入问题。在实践中,证明了该技术过于复杂且使用不一致。Oracle Solaris 的最新版本可在使用直接绑定的系统目标文件之间建立显式绑定。请参见第 9 章。
经常使用弱符号引用,以在运行时测试接口是否存在。该技术对生成环境、运行时环境进行限制,可通过编译器优化禁用该技术。使用具有 RTLD_DEFAULT 或 RTLD_PROBE 句柄的 dlsym(3C),可提供一致、强健的方式来测试符号是否存在。请参见测试功能。
构成输入文件的符号通常以这些符号的顺序出现在输出文件中。但暂定符号例外,因为完成这些符号的解析后才会完全定义这些符号。输出文件中暂定符号的顺序可能不遵循其原始顺序。
如果需要控制一组符号的顺序,应将所有暂定定义重新定义为初始化为零的数据项。例如,与源文件 foo.c 中说明的原始顺序相比,以下暂定定义将导致对输出文件中的数据项重新排序。
$ cat foo.c char One_array[0x10]; char Two_array[0x20]; char Three_array[0x30]; $ cc -o libfoo.so -G -Kpic foo.c $ elfdump -sN.dynsym libfoo.so | grep array | sort -k 2,2 [11] 0x00010614 0x00000020 OBJT GLOB D 0 .bss Two_array [3] 0x00010634 0x00000030 OBJT GLOB D 0 .bss Three_array [4] 0x00010664 0x00000010 OBJT GLOB D 0 .bss One_array
根据符号地址对符号排序将导致符号的输出顺序与其在源文件中定义的顺序不同。相反,通过将这些符号定义为已初始化的数据项,可确保这些符号在输入文件中的相对顺序传递到输出文件。
$ cat foo.c char A_array[0x10] = { 0 }; char B_array[0x20] = { 0 }; char C_array[0x30] = { 0 }; $ cc -o libfoo.so -G -Kpic foo.c $ elfdump -sN.dynsym libfoo.so | grep array | sort -k 2,2 [4] 0x00010614 0x00000010 OBJT GLOB D 0 .data One_array [11] 0x00010624 0x00000020 OBJT GLOB D 0 .data Two_array [3] 0x00010644 0x00000030 OBJT GLOB D 0 .data Three_array
除输入文件中提供的符号外,还可以为链接编辑提供其他全局符号引用或全局符号定义。生成符号引用的最简单形式是使用链接编辑器的 -u 选项。使用链接编辑器的 -M 选项和关联的 mapfile 的灵活性更大。使用此 mapfile,可以定义全局符号引用和各种全局符号定义。可以指定符号可见性和类型之类的符号属性。有关可用选项的完整说明,请参见SYMBOL_SCOPE / SYMBOL_VERSION 指令。
-u 选项提供了一种在链接编辑命令行中生成全局符号引用的机制。可以使用此选项完全从归档执行链接编辑。选择要从多个归档中提取的目标文件时,此选项还可以提供更多灵活性。有关归档提取的概述,请参见归档处理一节。
例如,可能要从可重定位目标文件 main.o 生成动态可执行文件,此目标文件引用符号 foo 和 bar。您需要从 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。有关链接编辑器处理归档多遍的介绍,请参见归档处理。
以下示例说明如何定义三种符号引用。然后,使用这些引用提取归档成员。虽然可以通过对链接编辑指定多个 -u 选项来实现归档提取,但此示例还说明了如何将符号的最终作用域缩减到局部。
$ cat foo.c #include <stdio.h> void foo() { (void) printf("foo: called from lib.a\n"); } $ cat bar.c #include <stdio.h> void bar() { (void) printf("bar: called from lib.a\n"); } $ cat main.c extern void foo(), bar(); void main() { foo(); bar(); } $ cc -c foo.c bar.c main.c $ ar -rc lib.a foo.o bar.o main.o $ cat mapfile $mapfile_version 2 SYMBOL_SCOPE { local: foo; bar; global: main; }; $ cc -o prog -M mapfile lib.a $ prog foo: called from lib.a bar: called from lib.a $ elfdump -sN.symtab prog | egrep 'main$|foo$|bar$' [29] 0x00010f30 0x00000024 FUNC LOCL H 0 .text bar [30] 0x00010ef8 0x00000024 FUNC LOCL H 0 .text foo [55] 0x00010f68 0x00000024 FUNC GLOB D 0 .text main
在缩减符号作用域一节中更详细地说明了将符号作用域从全局缩减为局部的重要性。
以下示例说明如何定义两种绝对符号定义。然后,使用这些定义解析输入文件 main.c 中的引用。
$ cat main.c #include <stdio.h> extern int foo(); extern int bar; void main() { (void) printf("&foo = 0x%p\n", &foo); (void) printf("&bar = 0x%p\n", &bar); } $ cat mapfile $mapfile_version 2 SYMBOL_SCOPE { global: foo { TYPE=FUNCTION; VALUE=0x400 }; bar { TYPE=DATA; VALUE=0x800 }; }; $ cc -o prog -M mapfile main.c $ prog &foo = 0x400 &bar = 0x800 $ elfdump -sN.symtab prog | egrep 'foo$|bar$' [45] 0x00000800 0x00000000 OBJT GLOB D 0 ABS bar [69] 0x00000400 0x00000000 FUNC GLOB D 0 ABS foo
从输入文件获取函数或数据项的符号定义时,这些符号定义通常与数据存储元素关联。mapfile 定义不足以构造此数据存储,因此,这些符号必须保持为绝对值。与 size 及 no value 关联的简单 mapfile 定义会导致创建数据存储。这种情况下,符号定义将带有节索引。但是,带有 value 的 mapfile 定义会导致创建绝对符号。如果在共享目标文件中定义符号,应当避免绝对定义。请参见扩充符号定义。
还可以使用 mapfile 定义 COMMON(即暂定)符号。与其他类型的符号定义不同,暂定符号在文件中不占用存储空间,而定义在运行时必须分配的存储空间。因此,定义此类型的符号有助于要生成的输出文件的存储分配。
暂定符号与其他符号类型的一个不同特性在于,暂定符号的 value 属性会指示其对齐要求。因此,可以使用 mapfile 定义重新对齐从链接编辑的输入文件中获取的暂定定义。
以下示例给出了两个暂定符号的定义。符号 foo 定义新的存储区域,而符号 bar 实际上用于更改文件 main.c 中相同暂定定义的对齐方式。
$ cat main.c #include <stdio.h> extern int foo; int bar[0x10]; void main() { (void) printf("&foo = 0x%p\n", &foo); (void) printf("&bar = 0x%p\n", &bar); } $ cat mapfile $mapfile_version 2 SYMBOL_SCOPE { global: foo { TYPE=COMMON; VALUE=0x4; SIZE=0x200 }; bar { TYPE=COMMON; VALUE=0x102; SIZE=0x40 }; }; $ cc -o prog -M mapfile main.c ld: warning: symbol 'bar' has differing alignments: (file mapfile value=0x102; file main.o value=0x4); largest value applied $ prog &foo = 0x21264 &bar = 0x21224 $ elfdump -sN.symtab prog | egrep 'foo$|bar$' [45] 0x00021224 0x00000040 OBJT GLOB D 0 .bss bar [69] 0x00021264 0x00000200 OBJT GLOB D 0 .bss foo
注 - 可以使用链接编辑器的 -t 选项抑制此符号解析诊断。
应避免在共享目标文件中创建绝对数据符号。从动态可执行文件对共享目标文件中数据项的外部引用通常需要创建复制重定位。请参见复制重定位。要提供此重定位,应当将数据项与数据存储关联。可通过在可重定位目标文件中定义符号来生成此关联。也可以通过在 mapfile 中定义符号并使用 size 声明和 no value 声明来生成此关联。请参见SYMBOL_SCOPE / SYMBOL_VERSION 指令。
可以过滤数据符号。请参见作为过滤器的共享目标文件。要提供此过滤,可以通过 mapfile 定义扩充目标文件定义。以下示例创建包含函数和数据定义的过滤器。
$ cat mapfile $mapfile_version 2 SYMBOL_SCOPE { global: foo { TYPE=FUNCTION; FILTER=filtee.so.1 }; bar { TYPE=DATA; SIZE=0x4; FILTER=filtee.so.1 }; local: *; }; $ cc -o filter.so.1 -G -Kpic -h filter.so.1 -M mapfile -R. $ elfdump -sN.dynsym filter.so.1 | egrep 'foo|bar' [1] 0x000105f8 0x00000004 OBJT GLOB D 1 .data bar [7] 0x00000000 0x00000000 FUNC GLOB D 1 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.c 和 bar.c 生成一个简单的共享目标文件。文件 foo.c 包含全局符号 foo,此符号可提供您希望提供给其他文件的服务。文件 bar.c 包含符号 bar 和 str,这两个符号可提供共享目标文件的底层实现。使用这些文件创建的共享目标文件通常导致创建三个具有全局作用域的符号。
$ 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 libfoo.so.1 -G foo.c bar.c $ elfdump -sN.symtab libfoo.so.1 | egrep 'foo$|bar$|str$' [41] 0x00000560 0x00000018 FUNC GLOB D 0 .text bar [44] 0x00000520 0x0000002c FUNC GLOB D 0 .text foo [45] 0x000106b8 0x00000004 OBJT GLOB D 0 .data str
现在,可以在另一个应用程序的链接编辑过程中使用 libfoo.so.1 提供的功能。对符号 foo 的引用会绑定到共享目标文件提供的实现。
由于符号 bar 和 str 具有全局绑定,因此还可以直接引用这些符号。此可见性会产生严重后果,因为以后可能会更改作为函数 foo 基础的实现。这样做可能会无意中导致已绑定到 bar 或 str 的现有应用程序失败或行为异常。
全局绑定符号 bar 和 str 的另一个后果是,可以在这些符号中插入相同名称的符号。简单解析一节中将介绍在共享目标文件中插入符号。此插入可以是有意的,用于避开共享目标文件提供的预期功能。另一方面,此插入可能是无意的,是将相同通用符号名称同时用于应用程序和共享目标文件的结果。
在开发共享目标文件时,可以通过将符号 bar 和 str 的作用域缩减为局部绑定来防止出现这种情况。在以下示例中,不能再在共享目标文件的接口中提供符号 bar 和 str。因此,外部目标文件不能引用或插入这些符号。您已经有效地定义了共享目标文件的接口。可以在隐藏底层实现详细信息的同时管理此接口。
$ cat mapfile $mapfile_version 2 SYMBOL_SCOPE { local: bar; str; }; $ cc -o libfoo.so.1 -M mapfile -G foo.c bar.c $ elfdump -sN.symtab libfoo.so.1 | egrep 'foo$|bar$|str$' [24] 0x00000548 0x00000018 FUNC LOCL H 0 .text bar [25] 0x000106a0 0x00000004 OBJT LOCL H 0 .data str [45] 0x00000508 0x0000002c FUNC GLOB D 0 .text foo
缩减符号作用域还具有其他性能方面的优点。现在,运行时必需的针对符号 bar 和 str 的符号重定位已缩减为相对重定位。有关符号重定位开销的详细信息,请参见执行重定位的时间。
随着链接编辑期间处理的符号数的增加,在 mapfile 中定义局部作用域缩减将变得越来越难维护。有一种更灵活的替代机制,可以根据应维护的全局符号来定义共享目标文件的接口。全局符号定义允许链接编辑器将所有其他符号缩减为局部绑定。可使用特殊的自动缩减指令 "*" 实现此机制。例如,可以重新编写上面的 mapfile 定义,将 foo 定义为生成的输出文件中需要的唯一全局符号。
$ cat mapfile $mapfile_version 2 SYMBOL_VERSION ISV_1.1 { global: foo; local: *; }; $ cc -o libfoo.so.1 -M mapfile -G foo.c bar.c $ elfdump -sN.symtab libfoo.so.1 | egrep 'foo$|bar$|str$' [26] 0x00000570 0x00000018 FUNC LOCL H 0 .text bar [27] 0x000106d8 0x00000004 OBJT LOCL H 0 .data str [50] 0x00000530 0x0000002c FUNC GLOB D 0 .text foo
此示例还会在 mapfile 指令中定义一个版本名称 libfoo.so.1.1。此版本名称建立一个内部版本定义,用于定义文件的符号接口。建议创建版本定义。此定义将成为可在文件演变过程中使用的内部更新控制机制的基础。请参见第 5 章。
每次指定版本名称时,必须将所有全局符号指定给版本定义。如果有任何全局符号未指定给版本定义,链接编辑器将生成致命错误状态。
$ cat mapfile $mapfile_version 2 SYMBOL_VERSION ISV_1.1 { global: foo; }; $ cc -o libfoo.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 libfoo.so.1
可以使用 -B local 选项在命令行中声明自动缩减指令 "*"。可按照以下方式成功编译上一个示例。
$ cc -o libfoo.so.1 -M mapfile -B local -G foo.c bar.c
生成可执行文件或共享目标文件时,缩减任何符号都会导致在输出映像中记录版本定义。生成可重定位目标文件时,将创建版本定义,但不会处理符号缩减。结果是所有符号缩减的符号项仍保持全局。例如,将上一个 mapfile 与自动缩减指令和关联的可重定位目标文件配合使用时,会创建一个中间可重定位目标文件,但不缩减任何符号。
$ cat mapfile $mapfile_version 2 SYMBOL_VERSION ISV_1.1 { global: foo; local: *; }; $ ld -o libfoo.o -M mapfile -r foo.o bar.o $ elfdump -s libfoo.o | egrep 'foo$|bar$|str$' [28] 0x00000050 0x00000018 FUNC GLOB H 0 .text bar [29] 0x00000010 0x0000002c FUNC GLOB D 2 .text foo [30] 0x00000000 0x00000004 OBJT GLOB H 0 .data str
此映像中创建的版本定义显示需要缩减符号。最终使用可重定位目标文件生成可执行文件或共享目标文件时,将会缩减符号。也就是说,链接编辑器将按照与在 mapfile 中处理版本更新数据相同的方式,来读取并解释可重定位目标文件中包含的符号缩减信息。
因此,现在可以使用上一个示例中产生的中间可重定位目标文件来生成共享目标文件。
$ ld -o libfoo.so.1 -G libfoo.o $ elfdump -sN.symtab libfoo.so.1 | egrep 'foo$|bar$|str$' [24] 0x00000508 0x00000018 FUNC LOCL H 0 .text bar [25] 0x00010644 0x00000004 OBJT LOCL H 0 .data str [42] 0x000004c8 0x0000002c FUNC GLOB D 0 .text foo
创建可执行文件或共享目标文件时缩减符号通常是最常见的要求。不过,使用链接编辑器的 -B reduce 选项可以强制在创建可重定位目标文件时缩减符号。
$ ld -o libfoo.o -M mapfile -B reduce -r foo.o bar.o $ elfdump -sN.symtab libfoo.o | egrep 'foo$|bar$|str$' [20] 0x00000050 0x00000018 FUNC LOCL H 0 .text bar [21] 0x00000000 0x00000004 OBJT LOCL H 0 .data str [30] 0x00000010 0x0000002c FUNC GLOB D 2 .text foo
对符号缩减的扩展是指从目标文件的符号表中删除符号项。局部符号仅在目标文件的 .symtab 符号表中维护。使用链接编辑器的 -z strip-class 选项,或者在链接编辑之后使用 strip(1),可以将此整个表从目标文件中删除。有时,可能需要保留 .symtab 符号表,但要删除选择的局部符号定义。
可以使用 mapfile 关键字 ELIMINATE 删除符号。与 local 指令一样,可以单独定义符号,也可以将符号名称定义为特殊的自动删除指令 "*"。以下示例说明如何删除上一个符号缩减示例中的符号 bar。
$ cat mapfile $mapfile_version 2 SYMBOL_VERSION ISV_1.1 { global: foo; local: str; eliminate: *; }; $ cc -o libfoo.so.1 -M mapfile -G foo.c bar.c $ elfdump -sN.symtab libfoo.so.1 | egrep 'foo$|bar$|str$' [26] 0x00010690 0x00000004 OBJT LOCL H 0 .data str [44] 0x000004e8 0x0000002c FUNC GLOB D 0 .text foo
可以使用 -B eliminate 选项在命令行中声明自动删除指令 "*"。
共享目标文件中的定义满足要创建的目标文件中的符号引用时,符号将保持未定义状态。可以在运行时查找与此符号关联的重定位信息。提供定义的共享目标文件通常会成为依赖项。
运行时链接程序在运行时使用缺省搜索模型来查找此定义。通常,将搜索每个目标文件(从动态可执行文件开始),并按装入目标文件的顺序处理每个依赖项。
还可以创建目标文件以使用直接绑定。使用此方法时,可以在要创建的目标文件中维护符号引用与提供符号定义的目标文件之间的关系。运行时链接程序使用此信息将引用直接绑定到定义符号的目标文件,从而绕过缺省符号搜索模型。请参见第 9 章。
链接编辑器可以通过删除重复项和尾部子串来压缩字符串表。此压缩可显著减小任何字符串表的大小。例如,.dynstr 表压缩后可缩小文本段,从而减少运行时分页活动。由于这些优点,缺省情况下将启用字符串表压缩。
由于字符串表压缩,提供大量符号的目标文件可能会增加链接编辑时间。为了避免开发期间产生此开销,应使用链接编辑器的 -z nocompstrtab 选项。可以使用链接编辑器的调试标记 -D strtab,detail 显示链接编辑期间执行的任何字符串表压缩。