链接编辑器按输入文件在命令行中出现的顺序读取这些文件。将打开并检查每个文件以确定文件的 ELF 类型,从而确定文件的处理方式。作为链接编辑的输入应用的文件类型由链接编辑的绑定模式(静态或动态)确定。
在静态模式下,链接编辑器仅接受可重定位目标文件或归档库作为输入文件。在动态模式下,链接编辑器还接受共享目标文件。
对于链接编辑过程,可重定位目标文件是最基本的输入文件类型。这些文件中的程序数据节将串联成要生成的输出文件映像。组织链接编辑信息节供以后使用。这些节不会成为输出文件映像的一部分,因为将生成新的信息节替代它们。符号将被收集到内部符号表中以进行验证和解析。然后,使用此表在输出映像中创建一个或多个符号表。
虽然可以在链接编辑命令行中直接指定输入文件,但通常使用 -l 选项指定归档库和共享目标文件。请参见与其他库链接。在链接编辑期间,归档库和共享目标文件的解释完全不同。下面两小节详细说明了这些差别。
使用 ar(1) 生成归档。归档通常由一组可重定位目标文件和一个归档符号表组成。该符号表提供符号定义与提供这些定义的目标文件之间的关联。缺省情况下,链接编辑器有选择地提取归档成员。链接编辑器使用未解析的符号引用从归档中选择完成绑定过程所需的目标文件。也可以显式提取归档的所有成员。
归档成员包含满足暂定 (tentative) 符号定义(目前保存在链接编辑器的内部符号表中)的数据符号定义。例如,FORTRAN COMMON 块定义,它导致提取定义相同 DATA 符号的可重定位目标文件。
归档成员包含的某个符号定义与需要隐藏可见性或受保护可见性的一个引用相匹配。请参见表 12-20。
有选择地提取归档时,除非 -z weakextract 选项有效,否则弱符号引用不会从归档中提取目标文件。有关更多信息,请参见简单解析。
在有选择地提取归档的情况下,链接编辑器会检查整个归档多遍。将根据需要提取可重定位目标文件,以满足链接编辑器内部符号表中累积的符号信息。链接编辑器检查完归档一遍(但未提取任何可重定位目标文件)之后,将处理下一个输入文件。
由于遇到归档时仅从归档中提取需要的可重定位目标文件,因此命令行中归档的位置可能很重要。请参见命令行中归档的位置。
注 - 虽然链接编辑器检查整个归档多遍以解析符号,但这种机制的开销会很大。对于包含随机组织的可重定位目标文件的大型归档,更是如此。在这些情况下,应使用 lorder(1) 和 tsort(1) 之类的工具对归档中的可重定位目标文件进行排序。该排序操作可减少链接编辑器必须检查归档的遍数。
共享目标文件是一个或多个输入文件的上一次链接编辑生成的不可分割完整单元。链接编辑器处理共享目标文件时,共享目标文件的所有内容将成为生成的输出文件映像的逻辑部分。包含此逻辑部分意味着,链接编辑过程可以使用在共享目标文件中定义的所有符号项。
链接编辑器不使用共享目标文件的程序数据节和大多数链接编辑信息节。绑定共享目标文件以生成可运行的进程时,运行时链接程序将解释这些节。但是,会记录出现的共享目标文件。信息存储在输出文件映像中,以表明此目标文件是运行时必须使用的依赖项。
缺省情况下,链接编辑过程中指定的所有共享目标文件在要生成的目标文件中都记录为依赖项。无论要生成的目标文件实际上是否引用共享目标文件提供的符号,都会进行此记录。为了最大程度地降低运行时链接的开销,请仅指定将解析所生成目标文件中的符号引用的那些依赖项。可以使用链接编辑器的调试功能和带有 -u 选项的 ldd(1) 确定未使用的依赖项。可以使用链接编辑器的 -z ignore 选项抑制所有未使用的共享目标文件的依赖项记录。
如果某个共享目标文件依赖于其他共享目标文件,则也会处理这些依赖项。处理完所有命令行输入文件后将进行此处理,以完成符号解析过程。不过,在要生成的输出文件映像中,不会将共享目标文件名称作为依赖项进行记录。
虽然命令行中共享目标文件的位置没有归档处理那么重要,但该位置具有全局效果。可重定位目标文件与共享目标文件之间以及多个共享目标文件之间允许存在多个名称相同的符号。请参见符号解析。
链接编辑器处理共享目标文件的顺序由存储在输出文件映像中的依赖项信息维护。在无延迟装入的情况下,运行时链接程序按相同的顺序装入指定的共享目标文件。因此,链接编辑器和运行时链接程序选择多重定义的一系列符号中第一次出现的某个符号。
虽然编译器驱动程序通常会确保对链接编辑器指定适当的库,但您必须经常提供自己的库。通过显式指定链接编辑器需要的输入文件可以指定共享目标文件和归档。但是,更常见且更灵活的方法涉及使用链接编辑器的 -l 选项。
根据约定,通常指定共享目标文件具有前缀 lib 和后缀 .so。指定归档具有前缀 lib 和后缀 .a。例如,libfoo.so 是共享目标文件版的 "foo" 实现,可用于编译环境。libfoo.a 是库的归档版本。
这些约定可由链接编辑器的 -l 选项识别。此选项通常用于为链接编辑提供其他库。以下示例指示链接编辑器搜索 libfoo.so。如果链接编辑器未找到 libfoo.so,则在继续搜索下一个目录之前将搜索 libfoo.a。
$ cc -o prog file1.c file2.c -lfoo
以动态模式进行链接编辑时,可以选择同时链接共享目标文件和归档。以静态模式进行链接编辑,仅接受归档库作为输入。
在动态模式下使用 -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。
命令行中归档的位置可以影响要生成的输出文件。链接编辑器搜索归档只是为了解析先前遇到过的未定义或暂定的外部引用。完成此搜索并提取所有需要的成员后,链接编辑器将继续处理命令行中的下一个输入文件。
因此,缺省情况下,不能使用归档来解析命令行中归档后面的输入文件中的任何新引用。例如,以下命令指示链接编辑器搜索 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-now 选项可以简化此过程。在命令行中遇到 -z rescan-now 选项时,链接编辑器会立即处理该选项。命令行中该选项之前的所有已处理归档都将立即重新处理。此处理过程会尝试查找可解析符号引用的其他归档成员。继续重新扫描此归档,直到扫描归档列表一遍但未提取到任何新成员为止。因此,上一个示例可进行如下简化:
$ cc -o prog .... -lA -lB -lC -z rescan-now
或者,也可以使用 -z rescan-start 和 -z rescan-end 选项将相互依赖的归档一起划分到一个归档组中。链接编辑器在命令行中遇到结束分隔符时,会立即重新处理这些组。在组中找到的归档会重新进行处理,以尝试查找可解析符号引用的其他归档成员。继续重新扫描此归档,直到扫描归档组一遍但未提取到任何新成员为止。如果使用归档组,上一个示例的编写如下:
$ cc -o prog .... -z rescan-start -lA -lB -lC -z rescan-end
注 - 应当在命令行的末尾指定任何归档,除非多重定义冲突要求采用其他方式。
上述所有示例都假定链接编辑器知道在哪里搜索命令行中列出的库。缺省情况下,在链接 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
可以使用 -L 选项补充通过使用 -Y 选项指定的目录。编译器驱动程序通常使用 -Y 选项提供特定于编译器的搜索路径。
还可以使用环境变量 LD_LIBRARY_PATH 将要搜索的目录添加链接编辑器的库搜索路径中。通常,LD_LIBRARY_PATH 采用冒号分隔的目录列表形式。LD_LIBRARY_PATH 最常见的形式是以分号分隔的两个目录列表。在命令行中提供的 -Y 列表之前和之后搜索这些列表。
以下示例说明在设置 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
运行时链接程序在两个缺省位置中查找依赖项。在处理 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。
动态目标文件可以提供用于运行时初始化和终止处理的代码。每次在进程中装入动态目标文件时,都会执行一次动态目标文件的初始化代码。每次在进程中卸载动态目标文件或进程终止时,都会执行一次动态目标文件的终止代码。可以将此代码封装在以下两种节类型的任意一种中:函数指针数组或单个代码块。这两种节类型都是通过串联输入可重定位目标文件中的相似节生成的。
.pre_initarray、.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() 的地址放置在 .init_array 元素中,并将 bar() 的地址放置在 .fini_array 元素中。
$ cat main.c #include <stdio.h> void foo() { (void) printf("initializing: foo()\n"); } void bar() { (void) printf("finalizing: bar()\n"); } void main() { (void) printf("main()\n"); } $ 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) 转储其内存的动态目标文件,那么数据初始化应该是一个独立的过程。