链接程序和库指南

输入文件处理

链接编辑器按输入文件在命令行中的出现顺序读取这些文件。将打开并检查每个文件以确定文件的 ELF 类型,从而确定文件的处理方式。作为链接编辑的输入应用的文件类型由链接编辑的绑定模式(静态动态)确定。

静态模式下,链接编辑器仅接受可重定位目标文件或归档库作为输入文件。在动态模式下,链接编辑器还接受共享库。

对于链接编辑过程,可重定位目标文件是最基本的输入文件类型。这些文件中的程序数据节将串联成要生成的输出文件映像。组织链接编辑信息节供以后使用。这些节不会成为输出文件映像的一部分,因为将生成新的节替代它们。符号将被收集到内部符号表中以进行验证和解析。然后,使用此表在输出映像中创建一个或多个符号表。

虽然可以在链接编辑命令行中直接指定输入文件,但通常使用 -l 选项指定归档库和共享库。 请参见与其他库链接。在链接编辑期间,归档库和共享库的解释完全不同。下面两小节详细说明了这些差别。

归档处理

使用 ar(1) 生成归档。归档通常由一组可重定位目标文件和归档符号表组成。该符号表提供符号定义与提供这些定义的目标文件之间的关联关系。缺省情况下,链接编辑器有选择地提取归档成员。链接编辑器使用未解析的符号引用从归档中选择所需的目标文件以完成绑定过程。也可以显式提取归档的所有成员。

在以下情况下,链接编辑器从归档中提取可重定位目标文件:

有选择地提取归档时,除非 -z weakextract 选项有效,否则弱符号引用不会从归档中提取目标文件。 有关更多信息,请参见简单解析


注 –

使用选项 -z weakextract-z allextract-z defaultextract,可以在多个归档之间切换归档提取机制。


在有选择地提取归档的情况下,链接编辑器会检查整个归档多遍。将根据需要提取可重定位目标文件,以满足链接编辑器内部符号表中累积的符号信息。链接编辑器检查完归档一遍但未提取任何可重定位目标文件之后,将处理下个输入文件。

由于遇到归档时仅从归档中提取需要的可重定位目标文件,因此命令行中归档的位置可能很重要。 请参见命令行中归档的位置


注 –

虽然链接编辑器检查整个归档多遍以解析符号,但此机制的开销很大。对于包含随机组织的可重定位目标文件的大型归档,更是如此。在这些情况下,应使用诸如 lorder(1)tsort(1) 的工具对归档中的可重定位目标文件排序。该排序操作可减少链接编辑器必须检查归档的遍数。


共享库处理

共享库是一个或多个输入文件的上一次链接编辑生成的不可分割完整单元。链接编辑器处理共享库时,共享库的所有内容将成为生成的输出文件映像的逻辑部分。包含此逻辑部分意味着,链接编辑过程可以使用在共享库中定义的所有符号项。在执行进程期间实际上会复制共享库。

链接编辑器不使用共享库的程序数据节和大多数链接编辑信息节。绑定共享库以生成可运行的进程时,运行时链接程序将解释这些节。但是,会记录出现的共享库。信息存储在输出文件映像中,以便表明此目标文件是在运行时必须使用的依赖项。

缺省情况下,链接编辑过程中指定的所有共享库在要生成的目标文件中都记录为依赖项。无论要生成的目标文件实际上是否引用共享库提供的符号,都会进行此记录。为了最大程度地降低运行时链接的开销,请仅指定将解析所生成目标文件中的符号引用的那些依赖项。可以使用链接编辑器的调试功能和带有 -u 选项的 ldd(1) 确定未使用的依赖项。或者,链接编辑器的 -z ignore 选项可以抑制记录未使用的共享库的依赖项。

如果某个共享库依赖于其他共享库,则也会处理这些依赖项。处理完所有命令行输入文件后将进行此处理,以完成符号解析过程。不过,在要生成的输出文件映像中,不会将共享库名称作为依赖项进行记录。

虽然命令行中共享库的位置没有归档处理那么重要,但该位置具有全局效果。可重定位库与共享库之间以及多个共享库之间允许存在多个名称相同的符号。 请参见符号解析

链接编辑器处理共享库的顺序由存储在输出文件映像中的依赖性信息维护。运行时链接程序读取此信息,并按相同的顺序装入指定的共享库。因此,链接编辑器和运行时链接程序选择多重定义的一系列符号中第一次出现的某个符号。


注 –

多个符号定义在使用 -m 选项生成的装入映射输出中报告。


与其他库链接

虽然编译器驱动程序通常确保对链接编辑器指定适当的库,但您经常必须提供自己的库。通过显式指定链接编辑器需要的输入文件可以指定共享库和归档。但是,更常见且更灵活的方法涉及使用链接编辑器的 -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.cfile3.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

可以使用 -L 选项补充使用 -Y 选项指定的目录。

使用环境变量

还可以使用环境变量 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.1libbar.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_INITDT_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) 转储其内存的动态库,则数据初始化应该是一个独立的过程。