在应用程序的链接编辑期间指定的依赖项,由运行时链接程序在进程初始化过程中处理。除了此机制以外,应用程序还可在执行期间通过绑定到其他目标文件来扩展其地址空间。应用程序将有效地使用处理应用程序标准依赖项所用的相同运行时链接程序服务。
延迟目标文件绑定有以下几个优点。
通过在需要目标文件时而不是在应用程序初始化期间处理目标文件,可以极大地缩短启动时间。如果在应用程序的特定运行期间不需要某目标文件提供的服务,则表示不需要该目标文件。用于提供帮助或调试信息的目标文件可能会出现此情况。
根据所需的确切服务(如用于网络协议),应用程序可在若干不同目标文件间进行选择。
在执行期间添加到进程地址空间的所有目标文件都可在使用后释放。
应用程序可使用下列典型方案来访问其他共享目标文件。
使用 dlopen(3C) 来查找共享目标文件并将其添加到运行的应用程序的地址空间。同时查找并添加此共享目标文件的所有依赖项。
重定位添加的共享目标文件及其依赖项。调用这些目标文件中的所有初始化节。
应用程序使用 dlsym(3C) 来查找已添加目标文件中的符号。然后,应用程序可引用该数据或调用这些新符号定义的函数。
在应用程序处理完这些目标文件后,可使用 dlclose(3C) 来释放地址空间。此时将调用要释放的目标文件中的所有终止节。
可使用 dlerror(3C) 来显示由于使用运行时链接程序接口例程而导致的所有错误状态。
运行时链接程序的服务将在头文件 dlfcn.h 中进行定义,并且通过共享目标文件 libc.so.1 使该服务可用于应用程序。在以下示例中,文件 main.c 可以引用任何 dlopen(3C) 系列例程,并且应用程序 prog 可在运行时绑定到这些例程。
$ cc -o prog main.c
注 - 在以前的 Oracle Solaris OS 发行版中,共享目标文件 libdl.so.1 提供了动态链接接口。libdl.so.1 仍然可用于支持所有现有依赖项。但是,libdl.so.1 提供的动态链接接口现在可从 libc.so.1 获取。不再需要使用 -ldl 进行链接。
使用 dlopen(3C),可将其他目标文件添加到运行的进程的地址空间。此函数将路径名和绑定模式作为参数,并向应用程序返回一个句柄。通过 dlsym(3C),可使用此句柄来查找供应用程序使用的符号。
如果将路径名指定为简单文件名(名称中没有 '/'),则运行时链接程序将使用一组规则来生成相应的路径名。包含 '/' 的路径名均按原样使用。
这些搜索路径规则与用于查找所有初始依赖项的规则完全相同。请参见运行时链接程序搜索的目录。例如,文件 main.c 包含以下代码片段。
#include <stdio.h> #include <dlfcn.h> int main(int argc, char **argv) { void *handle; ..... if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL) { (void) printf("dlopen: %s\n", dlerror()); return (1); } .....
要查找共享目标文件 foo.so.1,运行时链接程序应使用进程初始化时存在的 LD_LIBRARY_PATH 定义。接下来,使用链接编辑 prog 过程中指定的运行路径。最后,使用缺省位置 /lib 和 /usr/lib(对于 32 位目标文件)或 /lib/64 和 /usr/lib/64(对于 64 位目标文件)。
如果将路径名指定为:
if ((handle = dlopen("./foo.so.1", RTLD_LAZY)) == NULL) {
则运行时链接程序只会在进程的当前工作目录中搜索该文件。
如果找不到所需目标文件,则 dlopen(3C) 会返回 NULL 句柄。在此情况下,可使用 dlerror(3C) 来显示失败的真正原因。例如:
$ cc -o prog main.c $ prog dlopen: ld.so.1: prog: fatal: foo.so.1: open failed: No such \ file or directory
如果 dlopen(3C) 添加的目标文件依赖于其他目标文件,则也会将这些目标文件引入进程的地址空间中。此进程将一直继续,直到装入指定目标文件的所有依赖项。此依赖项树称为组。
如果 dlopen(3C) 指定的目标文件或其任何依赖项已经是进程映像的一部分,则不会进一步处理这些目标文件,而是向应用程序返回一个有效句柄。此机制可避免多次装入同一目标文件,并且使应用程序可以获取指向自身的句柄。例如,如果前面的 main.c 示例包含以下 dlopen() 调用:
if ((handle = dlopen(0, RTLD_LAZY)) == NULL) {
则通过指定了 RTLD_GLOBAL 标志的 dlopen(3C),可使用从 dlopen(3C) 返回的句柄在应用程序本身、进程初始化时装入的任何依赖项或添加到进程地址空间的所有目标文件中查找符号。
在找到并装入所有目标文件后,运行时链接程序必须处理每个目标文件并执行所有必需的重定位。另外,还必须以同一方式重定位使用 dlopen(3C) 引入进程地址空间中的所有目标文件。
对于简单应用程序,此过程很简单。但是,对于使用较复杂的应用程序(包含涉及多个目标文件的 dlopen(3C) 调用,且可能包含公共依赖项)的用户,则此过程可能相当重要。
可根据重定位的时间对重定位进行分类。运行时链接程序的缺省行为是在初始化时处理所有即时引用重定位,以及在进程执行期间处理所有延迟引用(此机制通常称为延迟绑定)。
该机制也适用于模式定义为 RTLD_LAZY 时使用 dlopen(3C) 添加的所有目标文件。替代方法是要求在添加目标文件时立即执行目标文件的所有重定位。您可以使用 RTLD_NOW 模式,也可以使用链接编辑器的 -z now 选项在生成目标文件时将此要求记录在目标文件中。此重定位要求将传播至要打开的目标文件的所有依赖项。
重定位还可分为非符号重定位和符号重定位。本节的余下部分将讨论有关符号重定位的问题(无论这些重定位何时进行),并重点介绍符号查找的某些细节信息。
如果 dlopen(3C) 获取的目标文件引用全局符号,则运行时链接程序必须从构成进程的目标文件池中查找此符号。如果缺少直接绑定,则会将缺省符号搜索模型应用于通过 dlopen() 获取的目标文件。但是,dlopen() 模式以及构成进程的目标文件的属性允许使用替代的符号搜索模型。
需要直接绑定的目标文件将直接在关联的依赖项中搜索符号(尽管要维护后面介绍的所有属性)。请参见附录 D。
缺省情况下,使用 dlopen(3C) 获取的目标文件将被指定 world 符号搜索作用域和 local 符号可见性。缺省符号查找模型一节将使用此缺省模型说明典型的目标文件组交互。定义全局目标文件、隔离组和目标文件分层结构等节将介绍使用 dlopen(3C) 模式和文件属性扩展缺省符号查找模型的示例。
对于通过基本 dlopen(3C) 添加的每个目标文件,运行时链接程序将首先在动态可执行文件中查找符号。之后,运行时链接程序在进程初始化期间提供的每个目标文件中查找。如果找不到该符号,运行时链接程序将继续搜索。接下来,运行时链接程序会在通过 dlopen(3C) 获取的目标文件及其依赖项中查找。
使用缺省符号查找模型时,可以转换到延迟装入环境。如果在当前装入的目标文件中找不到某符号,则会处理所有暂挂的延迟装入目标文件,以尝试查找该符号。此装入是对尚未完整定义其依赖项的目标文件的补偿。但是,该补偿可能会破坏延迟装入的优点。
在以下示例中,动态可执行文件 prog 和共享目标文件 B.so.1 具有下列依赖项。
$ ldd prog A.so.1 => ./A.so.1 $ ldd B.so.1 C.so.1 => ./C.so.1
如果 prog 通过 dlopen(3C) 获取共享目标文件 B.so.1,则会首先在 prog 中查找重定位共享目标文件 B.so.1 和 C.so.1 所需的任何符号,然后依次在 A.so.1、B.so.1 和 C.so.1 中进行查找。在此简单示例中,将通过 dlopen(3C) 获取的共享目标文件视作已在对应用程序的原始链接编辑结束时将它们添加进来。例如,可以采用图解方式表示前面列表中引用的目标文件,如下图所示。
图 3-1 单个 dlopen() 请求
通过 dlopen(3C) 获取的目标文件(显示为阴影块)所需的任何符号查找,将从动态可执行文件 prog 继续执行一直到最后一个共享目标文件 C.so.1。
此符号查找是按装入目标文件时为目标文件指定的属性建立的。请记住,已为动态可执行文件及随其装入的所有依赖项指定了全局符号可见性,并且为新目标文件指定了全局符号搜索作用域。因此,新目标文件能够在原始目标文件中查找符号。新目标文件也会构成一个唯一的组,其中每个目标文件都具有局部符号可见性。因此,该组中的每个目标文件都可在其他组成员中查找符号。
这些新目标文件不会影响应用程序或其初始依赖项所需的正常符号查找。例如,如果 A.so.1 要求在执行了前面的 dlopen(3C) 后进行函数重定位,则运行时链接程序对重定位符号的正常搜索是首先在 prog 中查找,然后在 A.so.1 中查找。运行时链接程序不会继续在 B.so.1 或 C.so.1 中查找。
此符号查找同样是按装入目标文件时为目标文件指定的属性建立的。对于动态可执行文件及随其装入的所有依赖项,都指定全局符号搜索作用域。此作用域不允许它们在仅提供局部符号可见性的新目标文件中查找符号。
这些符号搜索和符号可见性属性用于维护目标文件之间的关联。这些关联基于它们引入进程地址空间的情况以及目标文件之间的依赖项关系。将与给定 dlopen(3C) 关联的目标文件指定给唯一的组,可以确保仅允许与同一 dlopen(3C) 关联的目标文件在目标文件本身及其关联的依赖项中查找符号。
定义目标文件之间的关联的概念在多次执行 dlopen(3C) 的应用程序中更为清晰。例如,假定共享目标文件 D.so.1 具有以下依赖项:
$ ldd D.so.1 E.so.1 => ./E.so.1
并且 prog 应用程序使用 dlopen(3C) 装入此共享目标文件及共享目标文件 B.so.1。下图说明了目标文件之间的符号查找关系。
图 3-2 多个 dlopen() 请求
假定 B.so.1 和 D.so.1 都包含符号 foo 的定义,C.so.1 和 E.so.1 都包含需要此符号的重定位。由于目标文件与唯一的组关联,因此 C.so.1 绑定到 B.so.1 中的定义,而 E.so.1 绑定到 D.so.1 中的定义。此机制用于为通过多次调用 dlopen(3C) 获取的目标文件提供最直观的绑定。
在到现在为止已介绍的情况中使用目标文件时,执行每个 dlopen(3C) 的顺序对生成的符号绑定没有影响。但是,如果目标文件具有公共依赖项,则生成的绑定可能会受到进行 dlopen(3C) 调用的顺序的影响。
在以下示例中,共享目标文件 O.so.1 和 P.so.1 具有相同的公共依赖项。
$ ldd O.so.1 Z.so.1 => ./Z.so.1 $ ldd P.so.1 Z.so.1 => ./Z.so.1
在此示例中,prog 应用程序将对其中每个共享目标文件执行 dlopen(3C)。由于共享目标文件 Z.so.1 是 O.so.1 和 P.so.1 的公共依赖项,因此将为与两次 dlopen(3C) 调用关联的两个组指定 Z.so.1。此关系如下图所示。
图 3-3 具有公共依赖项的多个 dlopen() 请求
Z.so.1 可同时供 O.so.1 和 P.so.1 用于查找符号。更重要的是,就 dlopen(3C) 排序而言,Z.so.1 还可用于同时在 O.so.1 和 P.so.1 中查找符号。
因此,如果 O.so.1 和 P.so.1 同时包含符号 foo 的定义(这是 Z.so.1 重定位所需的),则进行的实际绑定不可预测,因为它会受到 dlopen(3C) 调用的顺序的影响。如果符号 foo 的功能在定义它的两个共享目标文件间不同,则在 Z.so.1 中执行代码的整体结果可能会因应用程序的 dlopen(3C) 排序而异。
通过使用 RTLD_GLOBAL 标志扩充模式参数,可将为通过 dlopen(3C) 获取的目标文件缺省指定的局部符号可见性提升为全局可见性。在此模式下,具有全局符号搜索作用域的任何其他目标文件可使用通过 dlopen(3C) 获取的所有目标文件来查找符号。
此外,通过 dlopen(3C) 获取的、带有 RTLD_GLOBAL 标志的任何目标文件都可供使用 dlopen() 及值为 0 的路径名的符号查找使用。
注 - 如果某个组成员定义了局部符号可见性,并且被另一个定义了全局符号可见性的组引用,则该目标文件的可见性将变为局部和全局可见性的串联。即使以后删除该全局组引用,也会保留此属性提升。
通过使用 RTLD_GROUP 标志扩充模式参数,可将为通过 dlopen(3C) 获取的目标文件缺省指定的全局符号搜索作用域缩小为组。在此模式下,仅允许通过 dlopen(3C) 获取的所有目标文件在其各自的组中查找符号。
使用链接编辑器的 -B group 选项,可在生成目标文件时为其指定组符号搜索作用域。
注 - 如果某个组成员定义了组搜索要求,并且被另一个定义了全局搜索要求的组引用,则该目标文件的搜索要求将变为组和全局搜索的串联。即使以后删除该全局组引用,也会保留此属性提升。
如果初始目标文件是从 dlopen(3C) 获取的,并且使用 dlopen() 打开第二个目标文件,则这两个目标文件都会被指定给一个唯一的组。这种情况可以防止一个目标文件在另一个目标文件中查找符号。
在某些实现中,初始目标文件必须导出符号以便重定位第二个目标文件。通过以下两种机制之一,可以满足此要求:
使初始目标文件成为第二个目标文件的显式依赖项。
使用 RTLD_PARENT 模式标志对第二个目标文件执行 dlopen(3C)。
如果初始目标文件是第二个目标文件的显式依赖项,则会将该初始目标文件指定给第二个目标文件所在的组。因此,初始目标文件可以为第二个目标文件的重定位提供符号。
如果许多目标文件可使用 dlopen(3C) 打开第二个目标文件,并且每个初始目标文件必须导出相同符号以满足第二个目标文件重定位的需要,则不能对第二个目标文件指定显式依赖项。在此情况下,可使用 RTLD_PARENT 标志扩充第二个目标文件的 dlopen(3C) 模式。此标志将导致第二个目标文件所在的组以显式依赖项的方式传播至初始目标文件。
这两种方法之间有一点区别。如果指定显式依赖项,则依赖项本身将成为第二个目标文件的 dlopen(3C) 依赖项树的一部分,从而可用于使用 dlsym(3C) 的符号查找。如果使用 RTLD_PARENT 获取第二个目标文件,则使用 dlsym(3C) 的符号查找不能使用初始目标文件。
如果第二个目标文件是通过 dlopen(3C) 从具有全局符号可见性的初始目标文件获取的,则 RTLD_PARENT 模式既是冗余的,也是无害的。从应用程序或应用程序的依赖项之一调用 dlopen(3C) 时,通常会发生这种情况。
进程可以使用 dlsym(3C) 获取特定符号的地址。此函数采用句柄和符号名称,并将符号地址返回给调用者。该句柄通过以下方式指示符号搜索:
可通过指定目标文件的 dlopen(3C) 返回句柄。该句柄允许从指定目标文件及定义其依赖项树的目标文件获取符号。使用模式 RTLD_FIRST 返回的句柄仅允许从指定目标文件获取符号。
可通过其值为 0 的路径名的 dlopen(3C) 返回句柄。该句柄允许从关联链接映射的启动目标文件及定义其依赖项树的目标文件获取符号。通常,启动目标文件为动态可执行文件。对于关联链接映射,该句柄还允许从通过 dlopen(3C) 获取且模式为 RTLD_GLOBAL 的任何目标文件获取符号。使用模式 RTLD_FIRST 返回的句柄仅允许从关联链接映射的启动目标文件获取符号。
特殊句柄 RTLD_DEFAULT 和 RTLD_PROBE 允许从关联链接映射的启动目标文件及定义其依赖项树的目标文件获取符号。此句柄还允许从通过 dlopen(3C) 获取且与调用者同属一组的任何目标文件获取符号。使用 RTLD_DEFAULT 或 RTLD_PROBE 时采用在解析调用目标文件中的符号重定位时所用的同一模型。
在以下可能很常见的示例中,应用程序首先会将其他目标文件添加到其地址空间。然后,应用程序会使用 dlsym(3C) 来查找函数或数据符号。接下来,应用程序将使用这些符号来调用这些新目标文件中提供的服务。文件 main.c 包含以下代码:
#include <stdio.h> #include <dlfcn.h> int main() { void *handle; int *dptr, (*fptr)(); if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL) { (void) printf("dlopen: %s\n", dlerror()); return (1); } if (((fptr = (int (*)())dlsym(handle, "foo")) == NULL) || ((dptr = (int *)dlsym(handle, "bar")) == NULL)) { (void) printf("dlsym: %s\n", dlerror()); return (1); } return ((*fptr)(*dptr)); }
首先会在文件 foo.so.1 中搜索符号 foo 和 bar,然后在与此文件关联的所有依赖项中搜索。接下来,在 return() 语句中使用单个参数 bar 调用函数 foo。
使用前面的文件 main.c 生成的应用程序 prog 包含下列依赖项。
$ ldd prog libc.so.1 => /lib/libc.so.1
如果在 dlopen(3C) 中指定的文件名的值为 0,则会首先在 prog 中搜索符号 foo 和 bar,然后在 /lib/libc.so.1 中搜索。
该句柄指示启动符号搜索所在根。搜索机制将从此根开始采用重定位符号查找中所述的模型。
如果找不到所需符号,则 dlsym(3C) 会返回 NULL 值。在此情况下,可使用 dlerror(3C) 来指示失败的真正原因。在以下示例中,应用程序 prog 找不到符号 bar。
$ prog dlsym: ld.so.1: main: fatal: bar: can't find symbol
使用特殊句柄 RTLD_DEFAULT 和 RTLD_PROBE,应用程序可以测试是否存在其他符号。
RTLD_DEFAULT 句柄采用运行时链接程序所用的同一规则来解析调用目标文件中的任何符号引用。请参见缺省符号查找模型。应注意该模型的两个方面。
与动态可执行文件中的同一符号引用匹配的符号引用将绑定到与可执行文件中的引用关联的过程链接表项。请参见过程链接表(特定于处理器)。动态链接的这种人为因素确保进程中的所有组件看到的是函数的单一地址。
如果在当前装入进程的目标文件中找不到符号,则会启动一个延迟装入回退。将对装入的所有动态目标文件重复执行此回退,从而装入所有暂挂的延迟可装入目标文件,以尝试解析符号。该模型对尚未完整定义其依赖项的目标文件进行补偿。但是,该补偿可能会破坏延迟装入的优点。如果找不到重定位符号,可能会装入不需要的目标文件,或是全面装入所有的延迟可装入目标文件。
RTLD_PROBE 采用与 RTLD_DEFAULT 类似的模型,但是与标有 RTLD_DEFAULT 的模型在两个方面有所不同。RTLD_PROBE 只能绑定到显式符号定义,而不能绑定到可执行文件中的任何过程链接表项。此外,RTLD_PROBE 不会启动一个全面的延迟装入回退。RTLD_PROBE 是用来检测现有进程中符号是否存在的最合适的标志。
RTLD_DEFAULT 和 RTLD_PROBE 都可以启动显式延迟装入。目标文件可以引用函数,且该引用可以通过一个延迟可装入依赖项建立。调用该函数之前,可以使用 RTLD_DEFAULT 或 RTLD_PROBE 测试函数是否存在。由于目标文件会引用函数,因此首先要尝试装入关联的延迟依赖项。随后遵循 RTLD_DEFAULT 和 RTLD_PROBE 的规则以绑定到函数。在下面的示例中,RTLD_PROBE 调用用于触发延迟装入,以及在存在依赖项时绑定到装入的依赖项。
void foo() { if (dlsym(RTLD_PROBE, "foo1")) { foo1(arg1); foo2(arg2); .... }
要为功能性测试提供一个强大而灵活的模型,关联的延迟依赖项应显式标记为推迟。请参见提供 dlopen() 的替代项。该标记还在运行时提供更改推迟依赖项的方式。
RTLD_DEFAULT 或 RTLD_PROBE 的使用为未定义的弱引用的使用提供了一个更强大的替代方案,如弱符号中所述。
使用特殊句柄 RTLD_NEXT,应用程序可在符号作用域内查找下一个符号。例如,如果应用程序 prog 包含以下代码片段:
if ((fptr = (int (*)())dlsym(RTLD_NEXT, "foo")) == NULL) { (void) printf("dlsym: %s\n", dlerror()); return (1); } return ((*fptr)());
则会在与 prog 关联的共享目标文件(在此情况下为 /lib/libc.so.1)中搜索 foo。如果此代码片段包含在图 3-1 中显示的示例的文件 B.so.1 中,则仅会在 C.so.1 中搜索 foo。
使用 RTLD_NEXT 提供了使用符号插入的方法。例如,可通过前面的目标文件插入目标文件中的函数,然后扩充原始函数的处理。例如,在共享目标文件 malloc.so.1 中放置以下代码片段。
#include <sys/types.h> #include <dlfcn.h> #include <stdio.h> void * malloc(size_t size) { static void *(*fptr)() = 0; char buffer[50]; if (fptr == 0) { fptr = (void *(*)())dlsym(RTLD_NEXT, "malloc"); if (fptr == NULL) { (void) printf("dlopen: %s\n", dlerror()); return (NULL); } } (void) sprintf(buffer, "malloc: %#x bytes\n", size); (void) write(1, buffer, strlen(buffer)); return ((*fptr)(size)); }
malloc.so.1 可插入到 malloc(3C) 通常所在的系统库 /lib/libc.so.1 之前。现在,在调用原始函数以完成分配之前,插入对 malloc() 的调用:
$ cc -o malloc.so.1 -G -K pic malloc.c $ cc -o prog file1.o file2.o ..... -R. malloc.so.1 $ prog malloc: 0x32 bytes malloc: 0x14 bytes ..........
$ cc -o malloc.so.1 -G -K pic malloc.c $ cc -o prog main.c $ LD_PRELOAD=./malloc.so.1 prog malloc: 0x32 bytes malloc: 0x14 bytes ..........
注 - 使用任何插入方法的用户在处理任何可能的递归时都必须小心。前面的示例使用 sprintf(3C),而不是直接使用 printf(3C) 来格式化诊断消息,以避免由于 printf(3C) 可能使用 malloc(3C) 而导致产生递归。
在动态可执行文件或预装入的目标文件中使用 RTLD_NEXT,可提供可预测的插入方法。在一般目标文件依赖项中使用此方法时应该十分小心,因为目标文件的实际装入顺序有时无法预测。