链接编辑过程根据一个或多个输入文件创建输出文件。输出文件的创建由提供给链接编辑器的选项和输入文件提供的输入节控制。
所有文件都使用可执行链接格式 (executable and linking format, ELF) 表示。有关 ELF 格式的完整说明,请参见第 7 章,目标文件格式。本章介绍两种 ELF 结构:节和段。
节是 ELF 文件中可以处理的最小不可分割单位。段是节的集合,节表示可由 exec(2) 或运行时链接程序 ld.so.1(1) 映射到内存映像的最小独立单位。
虽然存在许多类型的 ELF 节,但就链接编辑阶段而言可将所有节都归为两种类别:
本质上,链接编辑器将程序数据节串联成输出文件。链接编辑器将解释链接编辑信息节,以便修改其他节。信息节还用于生成在后期处理输出文件时使用的新输出信息节。
以下对链接编辑器功能的简单细分介绍了本章中讨论的主题:
对提供的所有选项进行验证和一致性检查。
串联多个输入可重定位目标文件中具有相同特征的节,以便在输出文件中形成新的节。串联的节又可与输出段关联。
处理可重定位目标文件和共享库中的符号表信息,以便验证并把引用和其定义合并起来。在输出文件中生成新的符号表。
处理输入可重定位目标文件中的重定位信息,并通过更新其他输入节将此信息应用于输出文件。此外,还可以生成输出重定位节以供运行时链接程序使用。
生成用于描述创建的所有段的程序头。
必要时生成动态链接信息节,这些节为运行时链接程序提供信息,如共享库依赖项和符号绑定。
把相似的节串联起来以及关联节到段的处理是在链接编辑器中使用缺省信息完成的。对于大多数链接编辑操作来说,链接编辑器提供的缺省节和段处理通常已满足要求。不过,可将 -M 选项与关联的 mapfile 配合使用来处理这些缺省行为。 请参见第 9 章,Mapfile 选项。
可以从命令行直接运行链接编辑器,也可以让编译器驱动程序调用链接编辑器。以下两小节详细介绍了这两种方法。但是,首选使用编译器驱动程序。编译环境通常是复杂且有时会发生变化的一系列操作(仅对编译器驱动程序可识别)。
直接调用链接编辑器时,必须提供创建预期输出所需的每个目标文件和库。链接编辑器对创建输出时使用的目标文件模块或库不会作出任何假设。例如,当您发布以下命令时:
$ ld test.o |
链接编辑器仅使用输入文件 test.o 创建一个名为 a.out 的动态可执行文件。要使 a.out 成为有用的可执行文件,应该包括用于启动和退出处理的代码。此代码可以特定于语言或操作系统,并且通常通过编译器驱动程序提供的文件提供。
此外,您还可以提供自己的初始化代码和终止代码。必须正确封装和标记此代码,以便运行时链接程序可以正确识别并使用代码。也可以通过编译器驱动程序提供的文件提供此封装和标记。
创建运行时目标文件(如可执行文件和共享库)时,应使用编译器驱动程序来调用链接编辑器。建议仅在使用 -r 选项创建中间可重定位目标文件时直接调用链接编辑器。
通常通过特定于语言的编译器驱动程序来使用链接编辑器。需要为编译器驱动程序 cc(1) 和 CC(1) 等等提供组成应用程序的输入文件。编译器驱动程序将添加其他文件和缺省库以完成链接编辑。展开编译调用可以看到这些其他文件,例如:
$ cc -# -o prog main.o /usr/ccs/bin/ld -dy /opt/COMPILER/crti.o /opt/COMPILER/crt1.o \ /usr/ccs/lib/values-Xt.o -o prog main.o \ -YP,/opt/COMPILER/lib:/usr/ccs/lib:/usr/lib -Qy -lc \ /opt/COMPILER/crtn.o |
编译器驱动程序包括的实际文件和用于显示链接编辑器调用的机制可能有所不同。
大多数选项可以通过编译器驱动程序的命令行传递到链接编辑器。通常,编译器选项和链接编辑器选项不会产生冲突。如果产生冲突,编译器驱动程序通常提供一种命令行语法,您可以使用该语法将特定选项传递到链接编辑器。也可以通过设置 LD_OPTIONS 环境变量为链接编辑器提供选项。
$ LD_OPTIONS="-R /home/me/libs -L /home/me/libs" cc -o prog main.c -lfoo |
链接编辑器解释 -R 和 -L 选项。这些选项位于从编译器驱动程序接收的任何命令行选项的前面。
链接编辑器解析整个选项列表,以查找任何无效选项或具有无效关联参数的任何选项。如果发现其中任何一种情况,则生成一条适当的错误消息。如果该错误被认为是致命错误,则链接编辑将终止。在以下示例中,链接编辑器通过检查捕获到非法选项 -X 和 -z 选项的非法参数。
$ ld -X -z sillydefs main.o ld: illegal option -- X ld: fatal: option -z has illegal argument `sillydefs' |
如果指定了某个需要关联参数的选项两次,则链接编辑器将产生适当的警告并继续进行链接编辑。
$ ld -e foo ...... -e bar main.o ld: warning: option -e appears more than once, first setting taken |
链接编辑器还会检查选项列表以查看是否存在任何致命的不一致性错误。
$ ld -dy -a main.o ld: fatal: option -dy and -a are incompatible |
处理完所有选项之后,如果未检测到任何致命错误状态,则链接编辑器将继续处理输入文件。
请参见附录 A,链接编辑器快速参考 以了解最常用的链接编辑器选项,并参见 ld(1) 以了解所有链接编辑器选项的完整说明。
链接编辑器按输入文件在命令行中的出现顺序读取这些文件。将打开并检查每个文件以确定文件的 ELF 类型,从而确定文件的处理方式。作为链接编辑的输入应用的文件类型由链接编辑的绑定模式(静态或动态)确定。
在静态模式下,链接编辑器仅接受可重定位目标文件或归档库作为输入文件。在动态模式下,链接编辑器还接受共享库。
对于链接编辑过程,可重定位目标文件是最基本的输入文件类型。这些文件中的程序数据节将串联成要生成的输出文件映像。组织链接编辑信息节供以后使用。这些节不会成为输出文件映像的一部分,因为将生成新的节替代它们。符号将被收集到内部符号表中以进行验证和解析。然后,使用此表在输出映像中创建一个或多个符号表。
虽然可以在链接编辑命令行中直接指定输入文件,但通常使用 -l 选项指定归档库和共享库。 请参见与其他库链接。在链接编辑期间,归档库和共享库的解释完全不同。下面两小节详细说明了这些差别。
使用 ar(1) 生成归档。归档通常由一组可重定位目标文件和归档符号表组成。该符号表提供符号定义与提供这些定义的目标文件之间的关联关系。缺省情况下,链接编辑器有选择地提取归档成员。链接编辑器使用未解析的符号引用从归档中选择所需的目标文件以完成绑定过程。也可以显式提取归档的所有成员。
归档成员包含满足暂定符号定义(目前保存在链接编辑器的内部符号表中)的数据符号定义。例如,FORTRAN COMMON 块定义,它导致提取定义相同 DATA 符号的可重定位目标文件。
有选择地提取归档时,除非 -z weakextract 选项有效,否则弱符号引用不会从归档中提取目标文件。 有关更多信息,请参见简单解析。
使用选项 -z weakextract、-z allextract 和 -z defaultextract,可以在多个归档之间切换归档提取机制。
在有选择地提取归档的情况下,链接编辑器会检查整个归档多遍。将根据需要提取可重定位目标文件,以满足链接编辑器内部符号表中累积的符号信息。链接编辑器检查完归档一遍但未提取任何可重定位目标文件之后,将处理下个输入文件。
由于遇到归档时仅从归档中提取需要的可重定位目标文件,因此命令行中归档的位置可能很重要。 请参见命令行中归档的位置。
虽然链接编辑器检查整个归档多遍以解析符号,但此机制的开销很大。对于包含随机组织的可重定位目标文件的大型归档,更是如此。在这些情况下,应使用诸如 lorder(1) 和 tsort(1) 的工具对归档中的可重定位目标文件排序。该排序操作可减少链接编辑器必须检查归档的遍数。
共享库是一个或多个输入文件的上一次链接编辑生成的不可分割完整单元。链接编辑器处理共享库时,共享库的所有内容将成为生成的输出文件映像的逻辑部分。包含此逻辑部分意味着,链接编辑过程可以使用在共享库中定义的所有符号项。在执行进程期间实际上会复制共享库。
链接编辑器不使用共享库的程序数据节和大多数链接编辑信息节。绑定共享库以生成可运行的进程时,运行时链接程序将解释这些节。但是,会记录出现的共享库。信息存储在输出文件映像中,以便表明此目标文件是在运行时必须使用的依赖项。
缺省情况下,链接编辑过程中指定的所有共享库在要生成的目标文件中都记录为依赖项。无论要生成的目标文件实际上是否引用共享库提供的符号,都会进行此记录。为了最大程度地降低运行时链接的开销,请仅指定将解析所生成目标文件中的符号引用的那些依赖项。可以使用链接编辑器的调试功能和带有 -u 选项的 ldd(1) 确定未使用的依赖项。或者,链接编辑器的 -z ignore 选项可以抑制记录未使用的共享库的依赖项。
如果某个共享库依赖于其他共享库,则也会处理这些依赖项。处理完所有命令行输入文件后将进行此处理,以完成符号解析过程。不过,在要生成的输出文件映像中,不会将共享库名称作为依赖项进行记录。
虽然命令行中共享库的位置没有归档处理那么重要,但该位置具有全局效果。可重定位库与共享库之间以及多个共享库之间允许存在多个名称相同的符号。 请参见符号解析。
链接编辑器处理共享库的顺序由存储在输出文件映像中的依赖性信息维护。运行时链接程序读取此信息,并按相同的顺序装入指定的共享库。因此,链接编辑器和运行时链接程序选择多重定义的一系列符号中第一次出现的某个符号。
虽然编译器驱动程序通常确保对链接编辑器指定适当的库,但您经常必须提供自己的库。通过显式指定链接编辑器需要的输入文件可以指定共享库和归档。但是,更常见且更灵活的方法涉及使用链接编辑器的 -l 选项。
根据约定,通常指定共享库具有前缀 lib 和后缀 .so。指定归档具有前缀 lib 和后缀 .a。例如,libc.so 是可用于编译环境的标准 C 库的共享版本。libc.a 是库的归档版本。
这些约定可由链接编辑器的 -l 选项识别。此选项通常用于为链接编辑提供其他库。以下示例指示链接编辑器搜索 libfoo.so。如果链接编辑器未找到 libfoo.so,则在继续搜索下一个目录之前将搜索 libfoo.a。
$ cc -o prog file1.c file2.c -lfoo |
在编译环境和运行时环境中使用的共享库都遵循相应的命名约定。编译环境使用简单 .so 后缀,而运行时环境通常使用带有附加版本号的后缀。 请参见命名约定和协调版本化文件名。
当链接编辑处于动态模式时,可以选择同时链接共享库和归档。当链接编辑处于静态模式时,仅接受归档库作为输入。
在动态模式下使用 -l 选项时,链接编辑器首先搜索给定目录以查找与指定名称匹配的共享库。如果未找到任何匹配项,则链接编辑器将在相同目录中查找归档库。在静态模式下使用 -l 选项时,将仅查找归档库。
动态模式下的库搜索机制搜索给定目录以查找共享库,然后搜索归档库。使用 -B 选项可以更精确地控制搜索。
通过在命令行中指定 -B dynamic 和 -B static 选项,可以分别在共享库或归档之间切换库搜索。例如,要将应用程序与归档 libfoo.a 和共享库 libbar.so 链接,可发布以下命令:
$ cc -o prog main.o file1.c -Bstatic -lfoo -Bdynamic -lbar |
-B static 和 -B dynamic 关键字并不完全对称。指定 -B static 时,链接编辑器要等到下一次出现 -B dynamic 时才接受共享库作为输入。但是,指定 -B dynamic 时,链接编辑器首先在任何给定的目录中查找共享库,然后查找归档库。
对上一个示例的准确说明如下:链接编辑器首先搜索 libfoo.a,然后搜索 libbar.so ,如果此搜索失败,则搜索 libbar.a。最后,链接编辑器搜索 libc.so,如果此搜索失败,则搜索 libc.a。
命令行中归档的位置可以影响要生成的输出文件。链接编辑器搜索归档只是为了解析先前遇到的未定义或暂定外部引用。完成此搜索并提取所有需要的成员后,链接编辑器将继续处理命令行中的下一个输入文件。
因此,缺省情况下,不能使用归档解析命令行中归档后面的输入文件中的任何新引用。例如,以下命令指示链接编辑器搜索 libfoo.a,仅仅是为了解析从 file1.c 中获取的符号引用。不能使用 libfoo.a 归档解析 file2.c 或 file3.c 中的符号引用。
$ cc -o prog file1.c -Bstatic -lfoo file2.c file3.c -Bdynamic |
应该在命令行末尾指定任何归档,除非多重定义冲突要求采取其他方式。
归档之间可以存在相互的依赖性,这样,要从一个归档中提取成员,还必须从另一个归档中提取相应成员。如果这些依赖性构成循环,则必须在命令行中重复指定归档以满足前面的引用。 例如:
$ cc -o prog .... -lA -lB -lC -lA -lB -lC -lA |
确定和维护重复指定的归档是一个繁琐的任务。使用 -z rescan 选项可以简化此过程。处理完所有输入文件后,此选项将导致重新处理整个归档列表。此处理过程尝试查到解析符号引用的其他归档成员。继续重新扫描此归档,直到扫描归档列表一遍但未提取任何新成员为止。因此,上一个示例可以简化为:
$ cc -o prog -z rescan .... -lA -lB -lC |
上面所有示例都假定链接编辑器了解在哪里搜索命令行中列出的库。缺省情况下,在链接 32 位目标文件时,链接编辑器只知道在三个标准目录中查找库:先搜索 /usr/ccs/lib,然后搜索 /lib,最后搜索 /usr/lib。在链接 64 位目标文件时,只使用两个标准目录:先搜索 /lib/64,然后搜索 /usr/lib/64。必须显式地将要搜索的所有其他目录添加到链接编辑器的搜索路径中。
可以使用命令行选项或环境变量来更改链接编辑器的搜索路径。
可以使用 -L 选项将新的路径名添加到库搜索路径中。在命令行中遇到此选项时,将改变搜索路径。例如,以下命令搜索 path1,然后搜索 /usr/ccs/lib 和 /lib,最后搜索 /usr/lib 来查找 libfoo。此命令搜索 path1,然后搜索 path2,接着搜索 /usr/ccs/lib、/lib 和 /usr/lib 来查找 libbar。
$ cc -o prog main.o -Lpath1 file1.c -lfoo file2.c -Lpath2 -lbar |
使用 -L 选项定义的路径名仅由链接编辑器使用。这些路径名不会记录在要创建的输出文件映像中。因此,运行时链接程序不能使用这些路径名。
如果要链接编辑器在当前目录中搜索库,则必须指定 -L。可以使用句点 (.) 来表示当前目录。
可以使用 -Y 选项更改链接编辑器搜索的缺省目录。随此选项提供的参数采用以冒号分隔的目录列表形式。例如,以下命令仅在目录 /opt/COMPILER/lib 和 /home/me/lib 中搜索 libfoo。
$ cc -o prog main.c -YP,/opt/COMPILER/lib:/home/me/lib -lfoo |
还可以使用环境变量 LD_LIBRARY_PATH(采用冒号分隔的目录列表形式)将要搜索的目录添加到链接编辑器的库搜索路径中。LD_LIBRARY_PATH 最常见的形式是以分号分隔的两个目录列表。系统按照命令行中提供的列表依次进行搜索。
以下示例说明在设置 LD_LIBRARY_PATH 且调用链接编辑器时使用多个 -L 的情况下所得结果:
$ LD_LIBRARY_PATH=dir1:dir2;dir3 $ export LD_LIBRARY_PATH $ cc -o prog main.c -Lpath1 ... -Lpath2 ... -Lpathn -lfoo |
有效搜索路径为 dir1:dir2:path1:path2... pathn:dir3:/usr/ccs/lib:/lib:/usr/lib。
如果在 LD_LIBRARY_PATH 定义中未指定分号,则将在解释所有 -L 选项之后解释指定的目录列表。在以下示例中,有效搜索路径为 path1:path2... pathn:dir1:dir2:/usr/ccs/lib:/lib:/usr/lib。
$ LD_LIBRARY_PATH=dir1:dir2 $ export LD_LIBRARY_PATH $ cc -o prog main.c -Lpath1 ... -Lpath2 ... -Lpathn -lfoo |
还可以使用此环境变量扩充运行时链接程序的搜索路径。 请参见运行时链接程序搜索的目录。为了防止此环境变量影响链接编辑器,请使用 -i 选项。
运行时链接程序在两个缺省位置中查找依赖项。在处理 32 位目标文件时,缺省位置为 /lib 和 /usr/lib。在处理 64 位目标文件时,缺省位置为 /lib/64 和 /usr/lib/64。必须显式地将要搜索的所有其他目录添加到运行时链接程序的搜索路径中。
当动态可执行文件或共享库与其他共享库链接时,这些共享库将作为依赖性进行记录。运行时链接程序执行进程期间必须找到这些依赖性。链接动态库时,可以在输出文件中记录一个或多个搜索路径。这些搜索路径称为运行路径。运行时链接程序使用目标文件的运行路径来查找该目标文件的依赖性。
可以使用 -z nodefaultlib 选项生成专用目标文件,以便在运行时不搜索任何缺省位置。该选项的用法表示使用目标文件的运行路径可以查找该目标文件的所有依赖性。如果不使用此选项,则无论如何扩充运行时链接程序的搜索路径,该搜索路径中的最后一个元素始终是缺省位置。
可以使用运行时配置文件管理缺省搜索路径。 请参见配置缺省搜索路径。但是,目标文件的创建者不应依赖于此文件的存在。应始终确保目标文件仅使用其运行路径或缺省位置即可找到它的依赖性。
可以使用 -R 选项(采用冒号分隔的目录列表形式)将运行路径记录在动态可执行文件或共享库中。以下示例将运行路径 /home/me/lib:/home/you/lib 记录在动态可执行文件 prog 中。
$ cc -o prog main.c -R/home/me/lib:/home/you/lib -Lpath1 \ -Lpath2 file1.c file2.c -lfoo -lbar |
运行时链接程序使用这些路径(后接缺省位置)来获取任何共享库的依赖性。在本示例中,此运行路径用于查找 libfoo.so.1 和 libbar.so.1。
链接编辑器接受多个 -R 选项。指定的多个选项串联在一起,用冒号分隔。因此,上一个示例还可以按如下所示表示。
$ cc -o prog main.c -R/home/me/lib -Lpath1 -R/home/you/lib \ -Lpath2 file1.c file2.c -lfoo -lbar |
对于可以安装在各种位置的目标文件,$ORIGIN 动态字符串标记提供了一种记录运行路径的灵活方法。 请参见查找关联的依赖项。
以前指定 -R 选项的替代方法是设置环境变量 LD_RUN_PATH,并使链接编辑器可以使用此环境变量。LD_RUN_PATH 和 -R 的作用域和功能完全相同,但如果同时指定了这两者,则 -R 会取代 LD_RUN_PATH。
动态库可以提供用于运行时初始化和终止处理的代码。每次在进程中装入动态库时,都会执行一次动态库的初始化代码。每次从进程中卸载动态库或进程终止时,都会执行一次动态库的终止代码。可以将此代码封装在以下两种节类型的任意一种中:函数指针数组或单个代码块。这两种节类型都是通过串联输入可重定位目标文件中的相似节生成的。
.preinit_array、.init_array 和 .fini_array 节分别提供运行时预初始化数组、初始化数组和终止函数数组。创建动态库时,链接编辑器相应地使用 .dynamic 标记对(DT_PREINIT_[ARRAY/ARRAYSZ]、DT_INIT_[ARRAY/ARRAYSZ] 和 DT_FINI_[ARRAY/ARRAYSZ])标识这些数组。这些标记标识关联的节,以便运行时链接程序可以调用这些节。预初始化数组仅适用于动态可执行文件。
.init 和 .fini 节分别提供运行时初始化代码块和终止代码块。编译器驱动程序通常提供 .init 和 .fini 节以及添加到输入文件列表开头和末尾的文件。编译器提供这些文件的作用相当于将可重定位目标文件中的 .init 和 .fini 代码封装到各个函数中。这些函数分别用保留符号名称 _init 和 _fini 标识。 创建动态库时,链接编辑器相应地使用 .dynamic 标记(DT_INIT 和 DT_FINI)标识这些符号。这些标记标识关联的节,以便运行时链接程序可以调用这些节。
有关运行时执行初始化和终止代码的更多信息,请参见初始化和终止例程。
链接编辑器可以使用 -z initarray 和 -z finiarray 选项直接注册初始化函数和终止函数。例如,以下命令将 foo() 的地址放置在 .initarray 元素中,并将 bar() 的地址放置在 .finiarray 元素中。
$ cat main.c #include <stdio.h> void foo() { (void) printf("initializing: foo()\n"); } void bar() { (void) printf("finalizing: bar()\n"); } main() { (void) printf("main()\n"); return (0); } $ cc -o main -zinitarray=foo -zfiniarray=bar main.c $ main initializing: foo() main() finalizing: bar() |
可以使用汇编程序直接创建初始化节和终止节。但是,大多数编译器提供特殊元语来简化其声明。例如,可以使用以下 #pragma 定义重新编写上面的代码示例。这些定义导致在 .init 节中调用 foo(),并在 .fini 节中调用 bar()。
$ cat main.c #include <stdio.h> #pragma init (foo) #pragma fini (bar) ....... $ cc -o main main.c $ main initializing: foo() main() finalizing: bar() |
分布在几个可重定位目标文件中的初始化和终止代码包含在归档库或共享库中时,这些代码可以产生不同的行为。对使用此归档的应用程序进行链接编辑时,可能仅提取此归档中包含的部分目标文件。这些目标文件可能仅提供分布在归档成员中的部分初始化和终止代码。在运行时仅执行此部分代码。在运行时装入依赖性时,针对共享库生成的相同应用程序将执行所有累积的初始化和终止代码。
在运行时确定进程中执行初始化和终止代码的顺序是一个很复杂的问题,需要进行依赖性分析。限制初始化代码和终止代码的内容可简化此分析过程。简化的初始化代码和终止代码提供可预测的运行时行为。 有关更多详细信息,请参见初始化和终止顺序。
如果初始化代码中包含可以使用 dldump(3C) 转储其内存的动态库,则数据初始化应该是一个独立的过程。
在输入文件处理期间,输入可重定位目标文件中的所有局部符号都将传递到输出文件映像。所有全局符号都在链接编辑器内部累积。在此内部符号表中搜索可重定位目标文件提供的每个全局符号。 如果遇到了名称与上一个输入文件中某个符号的名称相同的符号,则调用符号解析过程。符号解析过程决定这两项中哪一项会被保留。
完成输入文件处理并且没有发生致命符号解析错误时,链接编辑器将确定是否存在任何未解析的符号引用。未解析的符号引用可能导致链接编辑终止。
最后,将链接编辑器的内部符号表添加到要创建的映像的符号表中。
以下各小节详细说明了符号解析和未定义符号处理过程。
符号解析的方式很广,有简单直观的,也有错综复杂的。大多数解析由链接编辑器执行,且没有任何提示。但是,某些重定位可能伴随有警告诊断,而其他重定位可能导致致命错误状态。
这两种符号的解析取决于符号的属性、提供符号的文件的类型以及要生成的文件的类型。有关符号属性的完整说明,请参见符号表节。但是,对于以下论述,标识了三种基本符号类型:
符号解析最简单的形式涉及使用优先级关系。此关系中,已定义符号优先于暂定符号,而暂定符号又优先于未定义符号。
以下 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 表示的类别。除非编程语言以最原始的方式使用数据类型,否则这些符号类型与数据类型无关。
在类似以上示例的情况下,在可重定位目标文件与共享库之间进行解析时将采用可重定位目标文件定义。或者,在两个共享库之间进行解析时采用第一个定义。在弱绑定符号或全局绑定符号之间进行这种解析时,还会生成警告。
无法解析的符号冲突会导致致命错误状态,并生成相应的错误消息。此消息指出符号名称和提供这些符号的文件的名称。不生成输出文件。虽然致命状态足以导致链接编辑终止,但会先完成所有输入文件的处理。在此方式下,可以标识所有致命解析错误。
当两个可重定位目标文件都定义相同名称的非弱符号时,就会出现最常见的致命错误状态:
$ 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.c 和 bar.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.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 指令定义符号。 请参见定义其他符号。
自包含的共享库(通过指定的依赖性来满足对外部符号的所有引用)可提供最大的灵活性。此共享库可由许多用户使用,并且这些用户无需确定和建立依赖性来满足共享库的要求。
无论要生成哪种类型的输出文件,未解析的弱符号引用不会产生致命错误状态。
如果要生成静态可执行文件,则可将此符号转换为绝对符号,并指定值零。
如果要生成动态可执行文件或共享库,则可将此符号保留为未定义弱引用,并指定值零。在进程执行期间,运行时链接程序将搜索此符号。如果运行时链接程序未找到匹配项,则将此引用绑定到地址零,而不是生成致命重定位错误。
以前,这些未定义弱引用符号被用作一种机制,用于测试功能是否存在。例如,在共享库 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_DEFAULT 或 RTLD_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 生成动态可执行文件,此目标文件引用符号 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。有关链接编辑器处理归档多遍的论述,请参见归档处理。
可以使用链接编辑器的 -M 选项和关联的 mapfile 提供一组更丰富的符号定义。符号定义 mapfile 的项使用以下语法。
[ name ] { scope: symbol [ = [ type ] [ value ] [ size ] [ attribute ] ]; } [ dependency ]; |
此组符号定义的标签(如果存在)标识映像中的版本定义。 请参见第 5 章,应用程序二进制接口与版本控制。
指示要生成的输出文件中符号绑定的可见性。在链接编辑过程中,使用 mapfile 定义的所有符号都将视为全局符号。将针对任何其他具有相同名称的符号(从所有输入文件中获取)解析这些符号。以下定义和别名定义在要创建的目标文件中符号的可见性:
此范围的符号对所有外部目标文件都可见。在运行时绑定从目标文件内部对这种符号的引用,从而允许进行插入。
此范围的符号对所有外部目标文件都可见。在链接编辑时绑定从目标文件内部对这种符号的引用,从而防止在运行时插入。此范围定义与具有 STV_PROTECTED 可见性的符号产生相同的效果。 请参见表 7–20。
此范围的符号缩减为具有本地绑定的符号。此范围的符号对其他外部目标文件不可见。此范围定义与具有 STV_HIDDEN 可见性的符号产生相同的效果。 请参见表 7–20。
此范围的符号是 hidden。删除这些符号的符号表项。
所需符号的名称。如果该名称未后跟符号属性 type、value、size 或 extern 之一,则将创建符号引用。此引用与本节前面所述的使用 -u 选项生成的引用完全相同。如果符号名称后跟任何符号属性,则将使用关联的属性生成符号定义。
当符号属于 local 范围时,可以将此符号名称定义为特殊自动缩减指令 "*"。此指令将任何 mapfile 中未显式定义为 global 的所有全局符号降级为要生成的动态库中的本地绑定。
指示符号类型属性。此属性可以为 data、function 或 COMMON。前两种类型的属性会产生绝对符号定义。 请参见符号表节。后一种类型的属性会产生暂定符号定义。
指示值属性。此属性的格式为 Vnumber。
指示大小属性。此属性的格式为 Snumber。
此关键字提供符号的其他属性:
指示在外部对要创建的目标文件定义符号。 此属性可以与 DIRECT 或 NODIRECT 属性配合使用,以建立单独的直接或非直接引用。还可以使用此选项抑制将使用 -z defs 选项标记的未定义符号。
指示应直接绑定到此符号。此属性可以与 EXTERN 属性配合使用以控制绑定到外部符号。 请参见直接绑定。
指示不应直接绑定到此符号。此状态适用于要创建的目标文件中的引用或外部引用中的引用。此属性可以与 EXTERN 属性配合使用以控制绑定到外部符号。 请参见直接绑定。
指示此符号在共享库 name 中是一个过滤器。 请参见生成标准过滤器。过滤器符号不需要输入可重定位目标文件提供任何后备实现。因此,使用此指令并定义符号的类型可以创建绝对符号表项。
指示此符号在共享库 name 中是一个辅助过滤器。 请参见生成辅助过滤器。
表示此定义继承的版本定义。 请参见第 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.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 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 的引用绑定到共享库提供的实现。
由于符号 bar 和 str 具有全局绑定,因此还可以直接引用这些符号。此可见性会产生严重后果,因为您可能会在以后更改作为函数 foo 的基础的实现。这样做可能会无意中导致已绑定到 bar 或 str 的现有应用程序失败或行为异常。
全局绑定符号 bar 和 str 的另一个后果是,可以在这些符号中插入相同名称的符号。简单解析一节中说明了在共享库中插入符号。此插入可能是有意的,可以用来禁用共享库提供的预期功能。另一方面,此插入可能是无意的,是将相同通用符号名称同时用于应用程序和共享库的结果。
在开发共享库时,可以通过将符号 bar 和 str 的范围缩减为本地绑定来防止出现这种情况。在以下示例中,不能再将符号 bar 和 str 用作共享库接口。因此,外部目标文件不能引用或插入这些符号。您已经有效地定义了共享库的接口。可以在隐藏基础实现详细信息的同时管理此接口。
$ 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 |
缩减符号范围还具有其他性能方面的优点。现在,运行时必需的针对符号 bar 和 str 的符号重定位已缩减为相对重定位。有关符号重定位开销的详细信息,请参见何时执行重定位。
随着链接编辑期间处理的符号数的增加,在 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 显示链接编辑期间执行的任何字符串表压缩。
在完成输入文件处理和符号解析并且没有出现致命错误后,链接编辑器将生成输出文件。链接编辑器首先生成完成输出文件必需的其他节。这些节包括所有输入文件中的符号表,这些符号表包含局部符号定义以及已解析的全局符号和弱符号信息。
此外,还包括运行时链接程序需要的任何输出重定位和动态信息节。确定所有输出节信息后,将计算输出文件总大小。然后,相应地创建输出文件映像。
创建动态可执行文件或共享库时,通常会生成两个符号表。.dynsym 表及其关联的字符串表 .dynstr 包含寄存器符号、全局符号、弱符号和节符号。这些节成为在运行时作为进程映像一部分映射的 text 段的一部分。 请参见 mmap(2)。使用此映射,运行时链接程序可以读取这些节,以便执行任何必需的重定位。
.symtab 表及其关联的字符串表 .strtab 包含在输入文件处理过程中收集的所有符号。这些节不能作为进程映像的一部分进行映射。使用链接编辑器的 -s 选项或在链接编辑后使用 strip(1) 甚至可以从映像中删除这些节。
生成符号表期间,将创建保留符号。这些符号对于链接进程有特殊意义。不能在代码中定义这些符号。
对链接编辑器提供的地址表(即 .got 节)的位置无关的引用。此表由与位置无关的数据引用构造而成,这些数据引用出现在使用 -K pic 选项编译的目标文件中。 请参见与位置无关的代码。
对链接编辑器提供了地址表(即 .plt 节)的与位置无关的引用。此表由与位置无关的函数引用构造而成,这些数据引用出现在使用 -K pic 选项编译的目标文件中。 请参见与位置无关的代码。
生成可执行文件时,链接编辑器将查找其他符号,以定义可执行文件的入口点。如果使用链接编辑器的 -e 选项指定了一个符号,则将使用该符号。否则,链接编辑器将查找保留符号名称 _start,然后查找 main。如果这些符号都不存在,则将使用文本段的第一个地址。
通常在编译时记录可重定位目标文件的硬件和软件功能。链接编辑器合并所有输入可重定位目标文件的功能来创建输出文件的最终功能节。 请参见硬件和软件功能节。
此外,还可以在链接编辑器创建输出文件时定义功能。使用 mapfile 和链接编辑器的 -M 选项标识这些功能。使用 mapfile 定义的功能可以扩充或覆盖输入可重定位目标文件提供的功能。
以下各小节说明如何使用 mapfile 定义功能。
目标文件的硬件功能标识目标文件正常执行要满足的平台硬件要求。例如,要求可能是,标识需要在某些 x86 体系结构上可用的 MMX 或 SSE 功能的代码。
可以使用以下 mapfile 语法标识硬件功能要求:
hwcap_1 = TOKEN | Vval [ OVERRIDE ]; |
使用一个或多个标记限定 hwcap_1 声明,这些标记是硬件功能的符号表示。此外,通过在值前面加上 V 作为前缀可以提供表示多个功能中的某个功能的数值。对于 SPARC 平台,硬件功能定义为 sys/auxv_SPARC.h 中的 AV_ 值。对于 x86 平台,硬件功能定义为 sys/auxv_386.h 中的 AV_ 值。
以下 x86 示例说明如何将 MMX 和 SSE 声明为目标文件 foo.so.1 需要的硬件功能。
$ egrep "MMX|SSE" /usr/include/sys/auxv_386.h #define AV_386_MMX 0x0040 #define AV_386_SSE 0x0800 $ cat mapfile hwcap_1 = SSE MMX; $ cc -o foo.so.1 -G -K pic -Mmapfile foo.c -lc $ elfdump -H foo.so.1 Hardware/Software Capabilities Section: .SUNW_cap index tag value [0] CA_SUNW_HW_1 0x840 [ SSE MMX ] |
可重定位目标文件可以包含硬件功能值。链接编辑器可以合并多个输入可重定位目标文件中的任何硬件功能值。生成的 CA_SUNW_HW_1 值是对关联的输入值进行按位或运算的结果。缺省情况下,这些值与 mapfile 指定的硬件功能合并。
可以使用 OVERRIDE 关键字在 mapfile 中显式控制输出文件的硬件功能要求。OVERRIDE 关键字和硬件功能值 0 可有效地从要生成的目标文件中删除任何硬件功能要求。
$ elfdump -H foo.o Hardware/Software Capabilities Section: .SUNW_cap index tag value [0] CA_SUNW_HW_1 0x840 [ SSE MMX ] $ cat mapfile hwcap_1 = V0x0 OVERRIDE; $ cc -o bar.o -r -Mmapfile foo.o $ elfdump -H bar.o $ |
运行时链接程序会针对进程可用的硬件功能验证目标文件定义的任何硬件功能要求。如果无法满足任何硬件功能要求,则不会在运行时装入该目标文件。例如,如果进程不能使用 SSE 功能,则 ldd(1) 将指示以下错误。
$ ldd prog foo.so.1 => ./foo.so.1 - hardware capability unsupported: \ 0x800 [ SSE ] libc.so.1 => /lib/libc.so.1 |
利用不同硬件功能的动态库可以使用过滤器提供灵活的运行时环境。 请参见特定于硬件功能的共享库。
目标文件的软件功能标识对于调试或监视进程可能很重要的软件特征。目前,可识别的唯一软件功能与目标文件使用的帧指针有关。目标文件可以声明已知其帧指针的使用状态。然后,将帧指针声明为正在使用或未使用来限定此状态。
sys/elf.h 中定义的两个标志表示帧指针状态:
#define SF1_SUNW_FPKNWN 0x001 #define SF1_SUNW_FPUSED 0x002 |
可以使用以下 mapfile 语法标识这些软件功能要求:
sfcap_1 = TOKEN | Vval [ OVERRIDE ]; |
可以使用 FPKNWN 和 FPUSED 标志限定 sfcap_1 声明。或者,可以使用表示这些状态的数值进行限定。
可重定位目标文件可以包含软件功能值。链接编辑器合并多个输入可重定位目标文件中的软件功能值。可按如下方法根据两个输入值计算 CA_SUNW_SF_1 的值。
表 2–1 CA_SUNW_SF_1 标志组合状态表
输入文件 1 |
输入文件 2 |
||
---|---|---|---|
|
SF1_SUNW_FPKNWN SF1_SUNW_FPUSED |
SF1_SUNW_FPKNWN |
<unknown> |
SF1_SUNW_FPKNWN SF1_SUNW_FPUSED |
SF1_SUNW_FPKNWN SF1_SUNW_FPUSED |
SF1_SUNW_FPKNWN |
SF1_SUNW_FPKNWN SF1_SUNW_FPUSED |
SF1_SUNW_FPKNWN |
SF1_SUNW_FPKNWN |
SF1_SUNW_FPKNWN |
SF1_SUNW_FPKNWN |
<unknown> |
SF1_SUNW_FPKNWN SF1_SUNW_FPUSED |
SF1_SUNW_FPKNWN |
<unknown> |
此计算方法适用于每个可重定位目标文件值和 mapfile 值。如果不存在 .SUNW_cap 节,或者此节不包含 CA_SUNW_SF_1 值,或者未设置 SF1_SUNW_FPKNW 和 SF1_SUNW_FPUSED 标志,则目标文件的软件功能未知。
缺省情况下,使用相同的状态模型处理 mapfile 指定的任何软件功能。
可以使用 OVERRIDE 关键字在 mapfile 中显式控制输出文件的软件功能要求。OVERRIDE 关键字和软件功能值 0 可有效地从要生成的目标文件中删除任何软件功能要求。
$ elfdump -H foo.o Hardware/Software Capabilities Section: .SUNW_cap index tag value [0] CA_SUNW_SF_1 0x3 [ SF1_SUNW_FPKNWN SF1_SUNW_FPUSED ] $ cat mapfile sfcap_1 = V0x0 OVERRIDE; $ cc -o bar.o -r -Mmapfile foo.o $ elfdump -H bar.o $ |
创建了输出文件后,将输入文件中的所有数据节复制到新的映像。将输入文件指定的所有重定位应用于输出映像。还要将必须生成的所有其他重定位信息写入新的映像。
重定位处理通常很容易,但可能会出现错误状态并伴随有特定错误消息。有两种状态需要进行详细地论述。第一种状态涉及位置相关代码产生的文本重定位。与位置无关的代码中对此状态进行了更详细的说明。第二种状态可以由位移重定位产生,下一小节中将对位移重定位进行更全面的说明。
如果将位移重定位应用于可以在复制重定位中使用的数据项,则可能会出现错误状态。复制重定位中对复制重定位进行了详细介绍。
已重定位的偏移和重定位目标保持分隔相同的位移时,位移重定位仍然有效。复制重定位是指将共享库中的全局数据项复制到可执行文件的 .bss 中。此复制将保留可执行文件的只读文本段。如果对复制的数据应用了位移重定位,或者外部重定位是对复制的数据进行位移,则位移重定位将变为无效。
尝试捕获这些类型的错误时要解决的问题包括:
生成共享库时,如果复制的数据涉及位移重定位,则标记可能存在问题的任何潜在复制可重定位数据项。构造共享库期间,链接编辑器无法确定将对数据项执行哪种外部引用。因此,只能标记潜在问题。
生成可执行文件时,标记其数据涉及位移重定位的复制重定位的创建过程。
然而,在链接编辑时创建复制重定位期间,可能会完成应用于共享库的位移重定位。因此,对引用此共享库的应用程序进行的链接编辑无法确定任何复制重定位数据中的有效位移。
为了帮助诊断这些问题,链接编辑器使用一个或多个动态 DT_FLAGS_1 标志指示动态库使用的位移重定位,如表 7–34 中所示。此外,可以使用链接编辑器的 -z verbose 选项显示可疑重定位。
例如,假设将创建具有全局数据项 bar[] 的共享库,将对该数据项应用位移重定位。如果从动态可执行文件引用,则此项可能已进行了复制重定位。链接编辑器使用以下内容对此情况提出警告:
$ cc -G -o libfoo.so.1 -z verbose -K pic foo.o ld: warning: relocation warning: R_SPARC_DISP32: file foo.o: symbol foo: \ displacement relocation to be applied to the symbol bar: at 0x194: \ displacement relocation will be visible in output image |
现在,如果创建引用数据项 bar[] 的应用程序,则将创建复制重定位。此复制将导致位移重定位无效。因为链接编辑器可以很清楚地发现此情况,所以无论是否使用 -z verbose 选项,都将生成一条错误消息。
$ cc -o prog prog.o -L. -lfoo ld: warning: relocation error: R_SPARC_DISP32: file foo.so: symbol foo: \ displacement relocation applied to the symbol bar at: 0x194: \ the symbol bar is a copy relocated symbol |
当 ldd(1) 与 -d 或 -r 选项配合使用时,使用位移动态标志可生成类似的重定位警告。
通过确保要重定位(偏移)的符号定义和重定位的符号目标都是局部的,可以避免这些错误状态。请使用静态定义或链接编辑器的作用域设置方法。 请参见缩减符号范围。通过使用功能接口访问共享库中的数据可以避免此类型的重定位问题。
Solaris 链接程序附带有调试库。使用此库,可以更详细地跟踪链接编辑过程。此库有助于您了解或调试应用程序和库的链接编辑。使用此库显示的信息类型应保持不变。不过,信息的确切格式可能随发行版的不同而有所变化。
如果您不太了解 ELF 格式,则可能会不熟悉某些调试输出。不过,也许您希望大概了解其中许多方面。
使用 -D 选项可以启用调试。生成的所有输出将被定向到标准错误。必须使用一个或多个标记扩充此选项,以指示需要的调试类型。在命令行中键入 -D help 可以显示可用的标记。
$ ld -Dhelp debug: debug: For debugging the link-editing of an application: debug: LD_OPTIONS=-Dtoken1,token2 cc -o prog ... debug: or, debug: ld -Dtoken1,token2 -o prog ... debug: where placement of -D on the command line is significant debug: and options can be switched off by prepending with `!'. debug: debug: debug: args display input argument processing debug: basic provide basic trace information/warnings debug: cap display hardware/software capability processing debug: detail provide more information in conjunction with other options debug: entry display entrance criteria descriptors debug: files display input file processing (files and libraries) debug: got display GOT symbol information debug: help display this help message debug: libs display library search paths; detail flag shows actual debug: library lookup (-l) processing debug: map display map file processing debug: move display move section processing debug: reloc display relocation processing debug: sections display input section processing debug: segments display available output segments and address/offset debug: processing; detail flag shows associated sections debug: statistics display processing statistics debug: strtab display information about string table compression; detail debug: shows layout of string tables debug: support display support library processing debug: symbols display symbol table processing; detail flag shows debug: internal symbol table addition and resolution debug: tls display TLS processing info debug: unused display unused/unreferenced files; detail flag shows debug: unused sections debug: versions display version processing |
此列表是一个示例,用于显示对链接编辑器有意义的选项。确切选项可能随发行版的不同而有所变化。
大多数编译器驱动程序在预处理阶段解释 -D 选项。因此,LD_OPTIONS 环境变量是一种适合将此选项传递到链接编辑器的机制。
以下示例说明如何跟踪输入文件。确定已找到的库或已从归档中提取的可重定位目标文件时,此语法特别有用。
$ LD_OPTIONS=-Dfiles cc -o prog main.o -L. -lfoo ............ debug: file=main.o [ ET_REL ] debug: file=./libfoo.a [ archive ] debug: file=./libfoo.a(foo.o) [ ET_REL ] debug: file=./libfoo.a [ archive ] (again) ............ |
此例中,从归档库 libfoo.a 中提取了成员 foo.o,以满足对 prog 的链接编辑要求。请注意,对归档搜索了两次,以验证提取 foo.o 是否没有提取其他可重定位目标文件。多个 "again" 诊断指示该归档使用 lorder(1) 和 tsort(1) 进行排序的候选归档。
使用 symbols 标记,可以确定导致提取归档成员的符号和进行初始符号引用的目标文件。
$ LD_OPTIONS=-Dsymbols cc -o prog main.o -L. -lfoo ............ debug: symbol table processing; input file=main.o [ ET_REL ] ............ debug: symbol[7]=foo (global); adding debug: debug: symbol table processing; input file=./libfoo.a [ archive ] debug: archive[0]=bar debug: archive[1]=foo (foo.o) resolves undefined or tentative symbol debug: debug: symbol table processing; input file=./libfoo(foo.o) [ ET_REL ] ............. |
符号 foo 由 main.o 引用,并且被添加到链接编辑器的内部符号表中。此符号引用导致从归档 libfoo.a 中提取可重定位目标文件 foo.o。
本文档中对此输出进行了简化。
将 detail 标记和 symbols 标记一起使用,可以观察输入文件处理期间的符号解析详细信息。
$ LD_OPTIONS=-Dsymbols,detail cc -o prog main.o -L. -lfoo ............ debug: symbol table processing; input file=main.o [ ET_REL ] ............ debug: symbol[7]=foo (global); adding debug: entered 0x000000 0x000000 NOTY GLOB UNDEF REF_REL_NEED debug: debug: symbol table processing; input file=./libfoo.a [ archive ] debug: archive[0]=bar debug: archive[1]=foo (foo.o) resolves undefined or tentative symbol debug: debug: symbol table processing; input file=./libfoo.a(foo.o) [ ET_REL ] debug: symbol[1]=foo.c ............. debug: symbol[7]=bar (global); adding debug: entered 0x000000 0x000004 OBJT GLOB 3 REF_REL_NEED debug: symbol[8]=foo (global); resolving [7][0] debug: old 0x000000 0x000000 NOTY GLOB UNDEF main.o debug: new 0x000000 0x000024 FUNC GLOB 2 ./libfoo.a(foo.o) debug: resolved 0x000000 0x000024 FUNC GLOB 2 REF_REL_NEED ............ |
已使用提取的归档成员 foo.o 中的符号定义覆盖 main.o 中未定义原始符号 foo。详细的符号信息反映每个符号的属性。
在上一个示例中,可以看到使用一些调试标记可产生大量输出。要监视部分输入文件的活动,可直接在链接编辑命令行中放置 -D 选项。可以通过切换打开和关闭此选项。在以下示例中,只有在处理库 libbar 期间,才会打开符号处理的显示功能。
$ ld .... -o prog main.o -L. -Dsymbols -lbar -D!symbols .... |
要获取链接编辑命令行,可能必须从使用的任何驱动程序展开编译行。 请参见使用编译器驱动程序。