链接程序和库指南

初始化和终止例程

动态库可以提供用于运行时初始化和终止处理的代码。每次在进程中装入动态库时,都会执行一次动态库的初始化代码。每次在进程中卸载动态库或进程终止时,都会执行一次动态库的终止代码。

在将控制权转交给应用程序之前,运行时链接程序将处理应用程序中找到的所有初始化节及所有装入的依赖项。如果在进程执行期间装入新动态库,则会在装入该目标文件的过程中处理其初始化节。初始化节 .preinit_array.init_array.init 由链接编辑器在生成动态库时创建。

运行时链接程序执行的函数的地址包含在 .preinit_array.init_array 节中。这些函数的执行顺序与其地址在数组中的显示顺序相同。运行时链接程序将 .init 节作为单独的函数执行。如果某目标文件同时包含 .init 节和 .init_array 节,则会首先处理 .init 节,然后再处理该目标文件的 .init_array 节定义的函数。

动态可执行文件可在 .preinit_array 节中提供预初始化函数。这些函数将在运行时链接程序生成进程映像并执行重定位之后但执行任何其他初始化函数之前执行。 预初始化函数不允许在共享库中执行。


注 –

编译器驱动程序提供的进程启动机制通过应用程序来调用动态可执行文件中的任何 .init 节。执行所有依赖项初始化节之后,最后会调用动态可执行文件中的 .init 节。


动态库还可提供终止节。终止节 .fini_array.fini 由链接编辑器在生成动态库时创建。

所有终止节都传递给 atexit(3C)。当进程调用 exit(2) 时,将调用这些终止例程。使用 dlclose(3C) 从运行的进程中删除目标文件时,也会调用终止节。

运行时链接程序执行的函数的地址包含在 .fini_array 节中。这些函数的执行顺序与其地址在数组中的显示顺序相反。运行时链接程序将 .fini 节作为单独的函数执行。如果某目标文件同时包含 .fini 节和 .fini_array 节,则会首先处理 .fini_array 节定义的函数,然后再处理该目标文件的 .fini 节。


注 –

编译器驱动程序提供的进程终止机制通过应用程序来调用动态可执行文件中的任何 .fini 节。在执行所有依赖项终止节之前,首先会调用动态可执行文件的 .fini 节。


有关链接编辑器创建初始化节和终止节的更多信息,请参见初始化和终止节

初始化和终止顺序

确定运行时进程中初始化和终止代码的执行顺序是一个很复杂的过程,需要进行依赖项分析。此过程在原来初始化节和终止节的基础上有了很大发展。此过程试图达到现代语言和当前编程技术的预期目标。但是,可能存在很难满足用户期望的情况。 通过了解这些情况以及限制初始化代码和终止代码的内容,可以实现灵活的可预测运行时行为。

初始化节的目标是在引用同一目标文件中的任何其他代码之前执行一小节代码。终止节的目标是在目标文件完成执行后执行一小节代码。自包含的初始化节和终止节可以轻松满足这些要求。

但是,初始化节通常更为复杂,它会引用其他目标文件提供的外部接口。因此,将会建立依赖项,并且在从其他目标文件进行引用之前,必须在该依赖项中执行某个目标文件的初始化节。应用程序可建立详细的依赖项分层结构。此外,依赖项还可在其分层结构中创建循环。装入其他目标文件或更改已装入目标文件的重定位模式的初始化节会使得情况更加复杂。这些问题已经导致产生了各种排序和执行方法,以尝试达到这些节的原始目标。

对于 Solaris 2.6 之前的发行版,依赖项初始化例程是以相反装入顺序(即,使用 ldd(1) 显示的依赖项的相反顺序)调用的。同样,依赖项终止例程是以装入顺序调用的。但是,由于依赖项分层结构较为复杂,因此这种简单排序方法可能不适合。

对于 Solaris 2.6 发行版,运行时链接程序会构造以拓扑方式排序的已装入目标文件列表。此列表是根据每个目标文件表示的依赖项关系以及所表示依赖项外部的符号绑定生成的。


注意 – 注意 –

在 Solaris 8 10/00 发行版之前,环境变量 LD_BREADTH 可设置为非空值。此设置强制运行时链接程序按 Solaris 2.6 发行版之前的顺序执行初始化节和终止节。该功能自此发行版以后已被禁用,因为许多应用程序的初始化依赖项变得很复杂并且要求进行拓扑排序。 现已忽略 LD_BREADTH 设置而无任何提示。


初始化节是按依赖项的相反拓扑顺序执行的。如果发现循环依赖项,则无法对构成循环的目标文件进行拓扑排序。任何循环依赖项的初始化节都会以其相反装入顺序执行。同样,会以依赖项的拓扑顺序调用终止节。任何循环依赖项的终止节以其装入顺序执行。

可通过使用 带有 -i 选项的 ldd(1),就目标文件依赖项的初始化顺序进行静态分析。例如,以下动态可执行文件及其依赖项显示了循环依赖项:


$ dump -Lv B.so.1 | grep NEEDED

[1]     NEEDED      C.so.1

$ dump -Lv C.so.1 | grep NEEDED

[1]     NEEDED      B.so.1

$ dump -Lv main | grep NEEDED

[1]     NEEDED      A.so.1

[2]     NEEDED      B.so.1

[3]     NEEDED      libc.so.1

$ ldd -i main

        A.so.1 =>        ./A.so.1

        B.so.1 =>        ./B.so.1

        libc.so.1 =>     /lib/libc.so.1

        C.so.1 =>        ./C.so.1

        libm.so.2 =>     /lib/libm.so.2



   cyclic dependencies detected, group[1]:

        ./libC.so.1

        ./libB.so.1



   init object=/lib/libc.so.1

   init object=./A.so.1

   init object=./C.so.1 - cyclic group [1], referenced by:

        ./B.so.1

   init object=./B.so.1 - cyclic group [1], referenced by:

        ./C.so.1

上述分析完全从显式依赖项关系的拓扑排序得出。但是,频繁创建了未定义所需依赖项的目标文件。为此,进行依赖项分析时还会引入符号绑定。将符号绑定与显式依赖项结合有助于生成更准确的依赖性关系。通过使用带有 -i-d 选项的 ldd(1),可以获取更准确的初始化顺序静态分析。

装入目标文件的最常见模型使用延迟绑定。对于此模型,将仅在初始化处理之前处理即时引用符号绑定。延迟引用中的符号绑定可能仍会处于暂挂状态。这些绑定可以扩展到现在为止已建立的依赖项关系。通过使用带有 -i-r 选项的 ldd(1),可以对引入所有符号绑定的初始化顺序进行静态分析。

实际上,大多数应用程序都使用延迟绑定。因此,计算初始化顺序之前完成的依赖项分析会遵循使用 ldd -id 的静态分析。但是,由于此依赖项分析可能不完整,并且可能存在循环依赖项,因此运行时链接程序允许进行动态初始化。

动态初始化会尝试在调用同一目标文件中的任何函数之前执行该目标文件的初始化节。在延迟符号绑定过程中,运行时链接程序将确定是否已调用要绑定到的目标文件的初始化节。如果未调用,则运行时链接程序将在从符号绑定过程返回之前执行初始化节。

不能使用 ldd(1) 来显示动态初始化。但是,通过将 LD_DEBUG 环境变量设置为包括标记 init,可在运行时观察初始化调用的确切顺序。 请参见调试库。通过添加调试标记 detail,可以捕获丰富的运行时初始化信息和终止信息。此信息包括依赖项列表、拓扑处理以及循环依赖项的标识。

仅当处理延迟引用时,动态初始化才可用。以下各项禁用此动态初始化。

到现在为止已介绍的初始化方法可能仍然不足以处理某些动态活动。初始化节可显式使用 dlopen(3C) 或隐式使用延迟装入和过滤器来装入其他目标文件。初始化节还可提升现有目标文件的重定位。对于已装入来应用延迟绑定的目标文件,均解析这些绑定(如果使用 dlopen(3C) 在模式 RTLD_NOW 下引用同一目标文件)。此重定位提升将有效地抑制在动态解析函数调用时可用的动态初始化功能。

每次装入新目标文件或者提升现有目标文件的重定位时,都会启动这些目标文件的拓扑排序。实际上,在建立新的初始化要求并执行关联的初始化节时,将暂停原始初始化执行。此模型尝试确保新引用的目标文件进行适当的初始化,以供原始初始化节使用。 但是,此并行操作可能会导致不需要的递归。

处理使用延迟绑定的目标文件时,运行时链接程序可以检测某些级别的递归。可通过设置 LD_DEBUG=init 来显示此递归。例如,执行 foo.so.1 的初始化节可能会导致调用另一目标文件。如果此目标文件随后引用了 foo.so.1 中的接口,则会创建循环。运行时链接程序可在绑定对 foo.so.1 的延迟函数引用时检测此递归。


$ LD_DEBUG=init prog

00905: .......

00905: warning: calling foo.so.1 whose init has not completed

00905: .......

运行时链接程序无法检测通过已重定位的引用导致的递归。

递归开销可能很大并且存在问题,因此,应减少可通过初始化节触发的外部引用数和动态装入活动数,以便消除递归。

对于使用 dlopen(3C) 添加到运行的进程的所有目标文件,可以重复执行初始化处理。另外,对于因为调用 dlclose(3C) 而从进程卸载的所有目标文件,也可执行终止处理。

以上各节按尝试满足用户期望的方式,介绍了用于执行初始化节和终止节的各种方法。但是,还应采用编码样式和链接编辑做法来简化依赖项之间的初始化和终止关系。该简化有助于进行可预测的初始化处理和终止处理,同时不会轻易出现意外依赖项排序带来的负面影响。

应尽量精简初始化节和终止节的内容。通过运行时初始化目标文件来避免创建全局构造函数。减少初始化和终止代码对其他依赖项的依赖。定义所有动态库的依赖项要求。 请参见生成共享库输出文件。不要表示不需要的依赖项。 请参见共享库处理。避免循环依赖项。不要依赖于初始化或终止序列的顺序。目标文件的排序可能会受到共享库和应用程序开发的影响。 请参见依赖项排序