链接程序和库指南

第 3 章 运行时链接程序

在初始化和执行动态可执行文件的执行过程中,会调用一个解释程序将应用程序与其依赖项绑定。在 Solaris OS 中,此解释程序称为运行时链接程序。

在对动态可执行文件进行链接编辑过程中,将会创建一个特殊的 .interp 节以及关联的程序头。此节包含用于指定程序的解释程序的路径名。链接编辑器提供的缺省名称是运行时链接程序的名称:/usr/lib/ld.so.1(对于 32 位可执行文件)和 /usr/lib/64/ld.so.1(对于 64 位可执行文件)。


注 –

ld.so.1 是共享库的特例。此处使用的版本号为 1。但是,以后的 Solaris 发行版可能会提供更高的版本号。


在执行动态库的过程中,内核将装入该文件并读取程序头信息。请参见程序头。内核可根据此信息查找所需解释程序的名称。内核会装入并将控制权转交给此解释程序,同时传递足够的信息以便解释程序继续执行应用程序。

除了初始化应用程序以外,运行时链接程序还会提供用来使应用程序扩展其地址空间的服务。此过程涉及装入其他目标文件以及绑定到这些目标文件提供的符号。

运行时链接程序的作用:

共享库依赖项

当运行时链接程序为某一程序创建内存段时,依赖项会说明提供该程序的服务时所需的共享库。通过重复连接引用的共享库及其依赖项,运行时链接程序可生成完整的进程映像。


注 –

即使在依赖项列表中多次引用了某个共享库,运行时链接程序也只会将该目标文件连接到进程一次。


查找共享库依赖项

链接动态可执行文件时,会显式引用一个或多个共享库。这些目标文件作为依赖项记录在动态可执行文件中。

运行时链接程序将使用此依赖项信息来查找并装入关联目标文件。这些依赖项的处理顺序与可执行文件的链接编辑期间对依赖项的引用顺序相同。

装入所有动态可执行文件的依赖项之后,将按依赖项的装入顺序检查每个依赖项,以找出其他依赖项。此过程会一直继续,直至找到并装入所有依赖项。此方法将导致所有依赖项按广度优先顺序排序。

运行时链接程序搜索的目录

运行时链接程序在两个缺省位置中查找依赖项。在处理 32 位目标文件时,缺省位置为 /lib 和 /usr/lib。在处理 64 位目标文件时,缺省位置为 /lib/64 和 /usr/lib/64。指定为简单文件名的任何依赖项都使用这些缺省目录名称作为前缀。生成的路径名用于查找实际文件。

使用 ldd(1) 可以显示动态可执行文件或共享库的依赖项。例如,文件 /usr/bin/cat 具有下列依赖项:


$ ldd /usr/bin/cat

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

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

文件 /usr/bin/cat 具有依赖项文件libc.so.1 和 libm.so.2,即需要文件 libc.so.1 和 libm.so.2。

可以使用 dump(1) 检查目标文件中记录的依赖项。使用此命令可以显示文件的 .dynamic 节,并查找具有 NEEDED 标记的项。在以下示例中,前面的 ldd(1) 示例中显示的依赖项 libm.so.2 没有记录在文件 /usr/bin/cat 中。ldd(1) 显示了指定文件的全部依赖项,而 libm.so.2 实际上是 /lib/libc.so.1 的依赖项。


$ dump -Lvp /usr/bin/cat

 

/usr/bin/cat:

[INDEX] Tag      Value

[1]     NEEDED   libc.so.1

.........

在前面的 dump(1) 示例中,依赖项表示为简单文件名。换言之,名称中没有 '/'。使用简单文件名要求运行时链接程序根据一组规则生成路径名。包含嵌入 '/' 的文件名均按原样使用。

记录简单文件名是记录依赖项的一种最灵活的标准机制。链接编辑器的 -h 选项记录依赖项中的简单名称。 请参见命名约定和记录共享库名称。

通常,依赖项分布在 /lib 及 /usr/lib 或 /lib/64 及 /usr/lib/64 以外的目录中。如果动态可执行文件或共享库需要在其他目录中查找依赖项,则必须显式指示运行时链接程序搜索此目录。

通过在链接编辑目标文件过程中记录运行路径,可以基于每个目标文件指定其他搜索路径。有关记录此信息的详细信息,请参见运行时链接程序搜索的目录。

使用 dump(1) 可以显示运行路径记录。请参考包含 RUNPATH 标记的 .dynamic 项。在以下示例中,prog 具有依赖项 libfoo.so.1。运行时链接程序必须先搜索目录 /home/me/lib 和 /home/you/lib,然后在缺省位置中查找。


$ dump -Lvp prog

 

prog:

[INDEX] Tag      Value

[1]     NEEDED   libfoo.so.1

[2]     NEEDED   libc.so.1

[3]     RUNPATH  /home/me/lib:/home/you/lib

.........

添加运行时链接程序搜索路径的另一种方法是设置环境变量 LD_LIBRARY_PATH。可以将该环境变量(在进程启动时即时对其分析)设置为一组以冒号分隔的目录。运行时链接程序会先搜索这些目录,然后搜索指定的任何运行路径或缺省目录。

这些环境变量非常适合在调试时使用,如将应用程序强制绑定到局部依赖项。在以下示例中,前面示例中的文件 prog 将绑定到当前工作目录中的 libfoo.so.1。


$ LD_LIBRARY_PATH=. prog

尽管 LD_LIBRARY_PATH 作为一种影响运行时链接程序搜索路径的临时机制很有用,但强烈建议不要在生产软件中使用它。可引用此环境变量的所有动态可执行文件都将扩充其搜索路径。此扩充可能导致性能整体下降。另外,根据使用环境变量和运行时链接程序搜索的目录中的说明,LD_LIBRARY_PATH 还会影响链接编辑器。

环境搜索路径可能导致 64 位可执行文件搜索包含与要查找的名称匹配的 32 位库的路径,反之亦然。运行时链接程序会拒绝不匹配的 32 位库,并继续搜索有效的 64 位匹配项。如果未找到匹配项,则会生成一条错误消息。通过将 LD_DEBUG 环境变量设置为包含 files 标记,可以详细观察此拒绝。 请参见调试库。


$ LD_LIBRARY_PATH=/lib/64 LD_DEBUG=files /usr/bin/ls

...

00283: file=libc.so.1;  needed by /usr/bin/ls

00283: 

00283: file=/lib/64/libc.so.1  rejected: ELF class mismatch: 32–bit/64–bit

00283: 

00283: file=/lib/libc.so.1  [ ELF ]; generating link map

00283:     dynamic:  0xef631180  base:  0xef580000  size:      0xb8000

00283:     entry:    0xef5a1240  phdr:  0xef580034  phnum:           3

00283:      lmid:           0x0

00283: 

00283: file=/lib/libc.so.1;  analyzing  [ RTLD_GLOBAL  RTLD_LAZY ]

...

如果无法找到依赖项,则 ldd(1) 会指示找不到该目标文件。如果尝试执行应用程序,则会导致运行时链接程序生成相应的错误消息:


$ ldd prog

        libfoo.so.1 =>   (file not found)

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

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

$ prog

ld.so.1: prog: fatal: libfoo.so.1: open failed: No such file or directory

配置缺省搜索路径

对于 32 位应用程序,运行时链接程序使用的缺省搜索路径为 /lib 和 /usr/lib。对于 64 位应用程序,缺省搜索路径为 /lib/64 和 /usr/lib/64。使用 crle(1) 实用程序创建的运行时配置文件,可以管理这些搜索路径。在为生成时未使用适当 运行路径的应用程序建立搜索路径时,此文件通常很有用。

对于 32 位应用程序,可在缺省位置 /var/ld/ld.config 构造配置文件;对于 64 位应用程序,则可在缺省位置 /var/ld/64/ld.config 构造配置文件。此文件影响系统中各自类型的所有应用程序。此外,也可在其他位置创建配置文件,并且可使用运行时链接程序的 LD_CONFIG 环境变量选择这些文件。在缺省位置安装配置文件之前测试该文件时,后一种方法会很有用。

动态字符串标记

运行时链接程序允许扩展各种动态字符串标记。这些标记适用于过滤器、运行路径和依赖项定义。

重定位处理

运行时链接程序在装入应用程序所需的全部依赖项之后,将会处理每个目标文件并执行所有必需的重定位。

在目标文件的链接编辑过程中,随可重定位输入目标文件提供的任何重定位信息均会应用于输出文件。但是,在创建动态可执行文件或共享库时,许多重定位无法在链接编辑时完成。这些重定位需要仅在目标文件装入内存时才知道的逻辑地址。在这种情况下,链接编辑器将在输出文件映像中生成新的重定位记录。然后,运行时链接程序必须处理这些新的重定位记录。

有关许多重定位类型的更详细说明,请参见重定位类型(特定于处理器)。重定位存在两个基本类型。

使用 dump(1) 可以显示目标文件的重定位记录。在以下示例中,文件 libbar.so.1 包含两条重定位记录,用于指示必须更新全局偏移表或 .got 节。


$ dump -rvp libbar.so.1



libbar.so.1:



.rela.got:

Offset      Symndx                Type              Addend



0x10438     0                     R_SPARC_RELATIVE  0

0x1043c     foo                   R_SPARC_GLOB_DAT  0

第一个重定位是一个简单的相对重定位,这可通过其重定位类型以及值为零的符号索引 (Symndx) 字节看出。此重定位需要使用将目标文件装入内存的基本地址来更新关联的 .got 偏移。

第二个重定位需要符号 foo 的地址。要完成此重定位,运行时链接程序必须从动态可执行文件或其依赖项之一查找该符号。

重定位符号查找

运行时链接程序负责搜索目标文件在运行时所需的符号。此符号搜索基于请求目标文件的符号搜索范围,以及进程中每个目标文件所提供的符号可见性。装入目标文件时,可将这些属性作为缺省属性应用。此外,也可将这些属性作为 dlopen(3C) 的特定模式提供。在某些情况下,可在生成目标文件时将这些属性记录在目标文件中。

通常,用户应该熟悉应用于动态可执行文件及其依赖项的缺省搜索模型,以及应用于通过 dlopen(3C) 获取的目标文件的缺省搜索模型。前者将在下一节缺省符号查找中概述,后者(也可使用各种符号查找属性)将在符号查找中介绍。

动态库使用直接绑定时,将提供替代的符号查找模型。此模型指示运行时链接程序直接在链接编辑时提供符号的目标文件中搜索符号。 请参见直接绑定。

缺省符号查找

动态可执行文件及随其装入的所有依赖项都被指定了 world 搜索范围和 global 符号可见性。 请参见符号查找。针对动态可执行文件或随其装入的任何依赖项的符号查找会导致搜索每个目标文件。运行时链接程序将从动态可执行文件开始,并按目标文件的装入顺序搜索每个依赖项。

如以上各节中所述,ldd(1) 将按依赖项的装入顺序列出动态可执行文件的依赖项。例如,共享库 libbar.so.1 需要符号 foo 的地址来完成重定位。动态可执行文件 prog 将 libbar.so.1 指定为其依赖项之一。


$ ldd prog

        libfoo.so.1 =>   /home/me/lib/libfoo.so.1

        libbar.so.1 =>   /home/me/lib/libbar.so.1

运行时链接程序首先会在动态可执行文件 prog 中查找 foo,然后在共享库 /home/me/lib/libfoo.so.1 中查找,最后在共享库 /home/me/lib/libbar.so.1 中查找。


注 –

符号查找操作的开销可能很大,尤其是在符号名称大小和依赖项数目增加的情况下。这方面的性能将在性能注意事项中详细介绍。有关替代查找模型,请参见直接绑定。


缺省重定位处理模型还允许转换为延迟装入 (lazy loading) 环境。如果在当前装入的目标文件中找不到某符号,则会处理所有暂挂的延迟装入目标文件,以尝试查找该符号。此装入是对尚未完整定义其依赖项的目标文件的补偿。但是,该补偿可能会破坏延迟装入的优点。

插入

缺省情况下,运行时链接程序首先在动态可执行文件中搜索符号,然后在每个依赖项中进行搜索。使用此模型时,第一次出现的所需符号满足搜索要求。因此,如果同一符号存在多个实例,则会在所有其他实例中插入第一个实例。

简单解析中概述了插入如何影响符号解析。缩减符号范围中提供了有关更改符号可见性,从而减少意外插入几率的机制。

如果目标文件被显式标识为插入项,则可以对每个目标文件强制执行插入。使用环境变量 LD_PRELOAD 装入或通过链接编辑器的 -z interpose 选项创建的任何目标文件都会标识为插入项。运行时链接程序搜索符号时,将在应用程序之后、任何其他依赖项之前搜索标识为插入项的任何目标文件。

仅当在进行任何进程重定位之前装入了插入项的情况下,才能保证可以使用插入项提供的所有接口。在重定位处理开始之前,将装入使用环境变量 LD_PRELOAD 提供的插入项,或作为应用程序的非延迟装入依赖项建立的插入项。启动重定位之后,引入进程中的插入项会降级为正常依赖项。如果插入项是延迟装入的,或者是由于使用 dlopen(3C) 而装入的,则插入项可能会降级。可使用 ldd(1) 来检测前一种类别。


% ldd -Lr prog

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

        foo.so.2 =>      ./foo.so.2

        libmapmalloc.so.1 =>     /usr/lib/libmapmalloc.so.1

        loading after relocation has started: interposition request \

                (DF_1_INTERPOSE) ignored: /usr/lib/libmapmalloc.so.1

注 –

如果链接编辑器在处理延迟装入的依赖项时遇到显式定义的插入项,则插入项将被记录为非延迟可装入依赖项。


直接绑定

使用直接绑定的目标文件维护符号引用与提供定义的依赖项之间的关系。运行时链接程序使用此信息直接搜索关联目标文件中的符号,而不执行缺省符号搜索模型。只能对使用链接编辑指定的依赖项建立直接绑定信息。因此,建议使用 -z defs 选项。

可使用下列机制之一建立符号引用与符号定义的直接绑定。

直接绑定可以大大降低由于包含许多符号重定位与依赖项的动态进程而导致的符号查找开销。此模型还允许在已直接绑定到的不同目标文件中查找同名的多个符号。


注 –

通过将环境变量 LD_NODIRECT 设置为非空值,可在运行时禁用直接绑定。


缺省符号搜索模型允许将符号的所有引用绑定到某个定义。由于直接绑定忽略缺省搜索模型,因此直接绑定禁用隐式插入符号。但是,在搜索提供符号定义的目标文件之前,会先搜索任何显式标识为插入项的目标文件。显式插入项包括使用环境变量 LD_PRELOAD 装入的目标文件,或使用链接编辑器的 -z interpose 选项创建的目标文件。 请参见插入。

某些接口可为缺省技术提供替代实现。这些接口期望其实现成为该技术在进程内的唯一实例。例如 malloc(3C) 系列。malloc() 系列实现有多种,并且每个系列都期望成为进程中使用的唯一实现。应该避免直接绑定到此类系列中的接口,否则同一进程可能会引用该技术的多个实例。例如,进程中的一个依赖项可针对 libc.so.1 直接绑定,而另一个依赖项可针对 libmapmalloc.so.1 直接绑定。对 malloc() 和 free() 的两种不同实现的不一致使用很可能会产生错误。

提供期望成为进程中的单一实例的接口的目标文件应该避免直接绑定到其接口。为防止任何调用方直接绑定到某接口,可以使用下列机制之一标记该接口。

非直接标记禁止任何符号引用直接绑定到实现。用于满足引用要求的符号搜索将使用缺省符号搜索模型。在生成随 Solaris 提供的各种 malloc() 系列实现时,使用了非直接标记。


注 –

NODIRECT mapfile 指令可与命令行选项 -B direct 或 -z direct 组合使用。未显式定义 NODIRECT 的符号位于该命令行指令之后。同样,DIRECT mapfile 指令也可与命令行选项 -B nodirect 组合使用。未显式定义 DIRECT 的符号位于该命令行指令之后。


执行重定位的时间

根据重定位的执行时间,重定位可分为两种类型。产生这种区别是由对已重定位偏移进行的引用的类型所致。

即时引用指的是必须在装入目标文件后立即确定的重定位。这些引用通常是目标文件代码使用的数据项、函数指针,甚至是通过与位置相关的共享库进行的函数调用。这些重定位无法向运行时链接程序提供有关何时引用重定位项的信息。因此,必须在装入目标文件时,并在应用程序获取或重新获取控制权之前执行所有即时重定位。

延迟引用指的是在目标文件执行时可确定的重定位。这些引用通常是通过与位置无关的共享库进行的全局函数调用,或者是通过动态可执行文件进行的外部函数调用。在对提供这些引用的任何动态模块进行编译和链接编辑的过程中,关联的函数调用将成为对过程链接表项的调用。这些项构成 .plt 节。每个过程链接表项都成为包含关联重定位的延迟引用。

在首次调用过程链接表项时,控制权会移交给运行时链接程序。运行时链接程序将查找所需符号,并重写关联目标文件中的项信息。将来调用此过程链接表项时,将直接转至相应函数。使用此机制,可以推迟此类型的重定位,直到调用函数的第一个实例。此过程有时称为延迟绑定。

运行时链接程序的缺省模式是在每次提供过程链接表重定位时执行延迟绑定。通过将环境变量 LD_BIND_NOW 设置为任意非空值,可以覆盖此缺省模式。此环境变量设置将导致运行时链接程序在装入目标文件时,同时执行即时引用和延迟引用重定位。这些重定位在应用程序获取或重新获取控制权之前执行。例如,根据以下环境变量来处理文件 prog 及其依赖项中的所有重定位。在将控制权转交给应用程序之前处理这些重定位。


$ LD_BIND_NOW=1 prog

此外,也可使用 dlopen(3C) 来访问目标文件,并将模式定义为 RTLD_NOW。还可使用链接编辑器的 -z now 选项来生成目标文件,以指示该目标文件需要在装入时进行完整的重定位处理。此重定位要求还将在运行时传播至所标记目标文件的所有依赖项。


注 –

前面的即时引用和延迟引用示例都很典型。但是,过程链接表项的创建最终受用作链接编辑输入的可重定位目标文件提供的重定位信息控制。R_SPARC_WPLT30 和 R_386_PLT32 等重定位记录指示链接编辑器创建过程链接表项。这些重定位由与位置无关的代码公用。

但是,通常会通过与位置相关的代码创建动态可执行文件,该代码可能不会指示需要过程链接表项。由于动态可执行文件具有固定位置,因此链接编辑器可在将引用绑定到外部函数定义时创建过程链接表项。无论原始重定位记录如何,都会创建此过程链接表项。


重定位错误

如果找不到符号,则会发生最常见的重定位错误。此情况将会产生相应的运行时链接程序错误消息并终止应用程序。在以下示例中,找不到在文件 libfoo.so.1 中引用的符号 bar。


$ ldd prog

        libfoo.so.1 =>   ./libfoo.so.1

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

        libbar.so.1 =>   ./libbar.so.1

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

$ prog

ld.so.1: prog: fatal: relocation error: file ./libfoo.so.1: \

symbol bar: referenced symbol not found

$

在对动态可执行文件进行链接编辑的过程中,此类别的任何潜在重定位错误都会标记为致命未定义符号。 有关示例,请参见生成可执行的输出文件。但是,如果运行时找到的依赖项与链接编辑过程中引用的原始依赖项不兼容,则可能会发生运行时重定位错误。在前面的示例中,根据包含 bar 的符号定义的 libbar.so.1 共享库的版本生成了 prog。

在链接编辑过程中使用 -z nodefs 选项,将抑制验证目标文件运行时重定位要求。抑制验证还可能会导致运行时重定位错误。

如果由于找不到用作即时引用的符号而发生重定位错误,则会在进程初始化期间立即出现该错误状态。对于延迟绑定的缺省模式,如果找不到用作延迟引用的符号,则会在应用程序获取控制权后出现该错误状态。后一种情况可能需要几分钟、几个月,也可能从不发生,具体情况视整个代码中使用的执行路径而定。

为防止发生此类错误,可使用 ldd(1) 来验证任何动态可执行文件或共享库的重定位要求。

如果在使用 ldd(1) 时指定 -d 选项,将列显每个依赖项并处理所有即时引用重定位。如果无法解析引用,则会生成诊断消息。在前面的示例中,-d 选项将导致以下错误诊断。


$ ldd -d prog

        libfoo.so.1 =>   ./libfoo.so.1

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

        libbar.so.1 =>   ./libbar.so.1

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

        symbol not found: bar           (./libfoo.so.1)

如果在使用ldd(1) 时指定 -r 选项,将处理所有即时引用和延迟引用重定位。只要有一种类型的重定位无法被解析,就会生成诊断消息。

装入其他目标文件

运行时链接程序允许您使用环境变量 LD_PRELOAD 在进程初始化期间引入新目标文件,从而提供其他级别的灵活性。此环境变量可初始化为共享库或可重定位目标文件名,也可初始化为用空格分隔的文件名字符串。这些目标文件将在装入动态可执行文件之后以及装入任何依赖项之前装入。这些目标文件都被指定了 world 搜索范围和 global 符号可见性。

在以下示例中,首先装入动态可执行文件 prog,然后装入共享库 newstuff.so.1。接下来装入在 prog 中定义的依赖项。


$ LD_PRELOAD=./newstuff.so.1 prog

可以使用 ldd(1) 显示这些目标文件的处理顺序。


$ LD_PRELOAD=./newstuff.so.1 ldd prog

        ./newstuff.so.1 => ./newstuff.so

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

在以下示例中,预装入比较复杂且耗时。


$ LD_PRELOAD="./foo.o ./bar.o" prog

运行时链接程序首先会对可重定位目标文件 foo.o 和 bar.o 进行链接编辑,以生成在内存中保存的共享库。然后,会按照前面示例中预装入共享库 newstuff.so.1 的方式,在动态可执行文件及其依赖项之间插入此内存映像。同样,可以使用 ldd(1) 显示这些目标文件的处理顺序:


$ LD_PRELOAD="./foo.o ./bar.o" ldd prog

        ./foo.o =>       ./foo.o

        ./bar.o =>       ./bar.o

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

在动态可执行文件后插入目标文件的这些机制将插入概念引入到另一个层次。您可以使用这些机制来试验驻留在标准共享库中的函数的新实现。如果预装入包含此函数的目标文件,则该目标文件将插入到原始功能中。因此,原始功能会完全隐藏在新的预装入版本之后。

预装入的另一个用途是扩充驻留在标准共享库中的函数。通过在原始符号中插入新符号,新函数可以执行其他处理。新函数还可调用原始函数。此机制通常会将 dlopen(3C) 与特殊句柄 RTLD_NEXT 配合使用,以获取原始符号的地址。

延迟装入动态依赖项

在内存中装入动态库时,将会检查该目标文件的任何其他依赖项。缺省情况下,将立即装入存在的所有依赖项。此循环会一直继续,直到找遍整个依赖项树为止。最后,解析由重定位指定的所有目标文件间数据引用。无论应用程序在执行期间是否引用了这些依赖项中的代码,都会执行这些操作。

在延迟装入模型中,标记为延迟装入的所有依赖项仅在显式引用时装入。通过利用函数调用的延迟绑定,依赖项装入将会一直延迟,直到第一次引用该函数。因此,绝不会装入从不引用的目标文件。

重定位引用可以是即时的,也可以是延迟的。因为初始化目标文件时必须解析即时引用,所以必须即时装入满足此引用要求的任何依赖项。因此,将这类依赖项标识为延迟可装入几乎没有效果。 请参见执行重定位的时间。通常,建议不要在动态库之间进行即时引用。

链接编辑器对调试库 liblddbg 的引用将使用延迟装入。由于不会经常进行调试,因此每次调用链接编辑器时装入此库既无必要又需要很大开销。通过指示可以延迟装入此库,处理库的开销将转至要求调试输出的那些调用。

实现延迟装入模型的替代方法是在需要时使用 dlopen() 和 dlsym() 来装入并绑定到依赖项。如果 dlsym() 引用数很少,则此模型非常理想。如果在链接编辑时不知道依赖项名称或位置,则此模型也很适用。对于更复杂的与已知依赖项的交互,对正常符号引用进行编码并指定要延迟装入的依赖项更为简单。

通过链接编辑器选项 -z lazyload 和 -z nolazyload,可将目标文件分别指定为延迟装入或正常装入。这些选项在链接编辑命令行上与位置相关。该选项之后的任何依赖项都将采用其指定的装入属性。缺省情况下,-z nolazyload 选项有效。

以下简单程序具有依赖项 libdebug.so.1。动态节 (.dynamic) 显示 libdebug.so.1 被标记为延迟装入。符号信息节 (.SUNW_syminfo) 显示触发 libdebug.so.1 装入的符号引用。


$ cc -o prog prog.c -L. -zlazyload -ldebug -znolazyload -lelf -R'$ORIGIN'

$ elfdump -d prog

 

Dynamic Section:  .dynamic

     index  tag           value

       [0]  POSFLAG_1     0x1           [ LAZY ]

       [1]  NEEDED        0x123         libdebug.so.1

       [2]  NEEDED        0x131         libelf.so.1

       [3]  NEEDED        0x13d         libc.so.1

       [4]  RUNPATH       0x147         $ORIGIN

       ...

$ elfdump -y prog

 

Syminfo section: .SUNW_syminfo

     index  flgs        bound to        symbol

      ....

      [52]  DL      [1] libdebug.so.1   debug

值为 LAZY 的 POSFLAG_1 指示应该延迟装入下面的 NEEDED 项 libdebug.so.1。由于 libelf.so.1 没有前述的 LAZY 标志,因此该库将在程序初始启动时装入。


注 –

libc.so.1 具有特殊的系统要求,该要求指出不应延迟装入该文件。 如果在处理 libc.so.1 时 -z lazyload 有效,则实际上会忽略该标志。


使用延迟装入时,可能需要准确声明在应用程序使用的目标文件中的依赖项和运行路径。例如,假定有两个目标文件 libA.so 和 libB.so,它们同时引用了 libX.so 中的符号。libA.so 将 libX.so 声明为依赖项,但 libB.so 没有这样操作。通常,将 libA.so 与 libB.so 一起使用时,libB.so 可以引用 libX.so,因为 libA.so 使此依赖项可用。但是,如果 libA.so 将 libX.so 声明为延迟装入,则在 libB.so 引用此依赖项时可能不会装入 libX.so。如果 libB.so 将 libX.so 声明为依赖项,但未能提供查找该依赖项所需的运行路径,则可能会出现类似错误。

无论是否延迟装入,动态库都应声明其所有依赖项以及如何查找这些依赖项。对于延迟装入,此依赖项信息更为重要。


注 –

通过将环境变量 LD_NOLAZYLOAD 设置为非空值,可在运行时禁用延迟装入。


提供 dlopen() 的替代项

延迟装入可以提供替代 dlopen(3C) 和 dlsym(3C) 的方法。 请参见运行时链接编程接口。例如,libfoo.so.1 中的以下代码将验证是否装入了目标文件,然后调用该目标文件提供的接口。


void foo()

{

    void * handle;



    if ((handle = dlopen("libbar.so.1", RTLD_LAZY)) != NULL) {

        int (* fptr)();



        if ((fptr = (int (*)())dlsym(handle, "bar1")) != NULL)

            (*fptr)(arg1);

        if ((fptr = (int (*)())dlsym(handle, "bar2")) != NULL)

            (*fptr)(arg2);

        ....

}

如果提供所需接口的目标文件满足下列条件,则可以简化此代码。

使用延迟装入,可以实现同样的 libbar.so.1 推迟装入。在此情况下,对函数 bar1() 的引用将导致延迟装入关联的依赖项。此外,标准函数调用可用于编译器或 lint(1) 验证。


void foo()

{

    bar1(arg1);

    bar2(arg2);

    ....

}

$ cc -G -o libfoo.so.1 foo.c -L. -zlazyload -zdefs -lbar -R'$ORIGIN'

但是,如果提供所需接口的目标文件并非始终可用,则此模型会失败。在此情况下,最好能够在不必知道依赖项名称的情况下测试依赖项是否存在。需要一种测试满足函数引用要求的依赖项可用性的方法。

带有 RTLD_PROBE 句柄的 dlsym(3C) 可用于验证依赖项是否存在以及是否将其装入。例如,对 bar1() 的引用可以验证在链接编辑时建立的延迟依赖项是否可用。此测试可用于控制对依赖项以使用 dlopen(3C) 的方式提供的函数的引用。


void foo()

{

    if (dlsym(RTLD_PROBE, "bar1")) {

        bar1(arg1);

        bar2(arg2);

        ....

}

此方法允许安全推迟装入已记录的依赖项以及标准函数调用。


注 –

特殊句柄 RTLD_DEFAULT 提供的机制与使用 RTLD_PROBE 类似。但是,使用 RTLD_DEFAULT 可能会导致在尝试查找的符号不存在时处理所有暂挂延迟装入目标文件。此装入是对尚未完整定义其依赖项的目标文件的补偿。但是,该补偿可能会破坏延迟装入的优点。

建议使用 -z defs 选项来生成利用延迟装入的所有目标文件。


初始化和终止例程

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

在将控制权转交给应用程序之前,运行时链接程序将处理应用程序中找到的所有初始化节及所有装入的依赖项。如果在进程执行期间装入新动态库,则会在装入该目标文件的过程中处理其初始化节。初始化节 .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) 而从进程卸载的所有目标文件,也可执行终止处理。

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

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

安全性

在安全进程中,对其依赖项及运行路径的评估应用了一些限制,以避免产生恶意依赖项替换或符号插入。

如果 issetugid(2) 系统调用对某进程返回 true,则运行时链接程序将该进程归类为安全进程。

对于 32 位目标文件,运行时链接程序已知的缺省可信目录为 /lib/secure 和 /usr/lib/secure。对于 64 位目标文件,运行时链接程序已知的缺省可信目录为 /lib/secure/64 和 /usr/lib/secure/64。实用程序 crle(1) 可用于指定适用于安全应用程序的其他可信目录。使用此技术的管理员应确保已对目标目录进行了适当的保护,以防受到恶意入侵。

如果 LD_LIBRARY_PATH 系列环境变量对安全进程有效,则仅将此变量指定的可信目录用于扩充运行时链接程序的搜索规则。 请参见运行时链接程序搜索的目录。

在安全进程中,将使用应用程序或其任何依赖项指定的运行路径。但是,运行路径必须是全路径名,即路径名必须以 '/' 开头。

在安全进程中,仅当 $ORIGIN 字符串扩展为可信目录时,才允许对其进行扩展。 请参见安全。

在安全进程中,LD_CONFIG 会被忽略。如果存在缺省配置文件,则安全进程将使用该配置文件。 请参见crle(1)。

在安全进程中,LD_SIGNAL 会被忽略。

在安全进程中使用 LD_PRELOAD 或 LD_AUDIT 环境变量可装入其他目标文件。必须将这些目标文件指定为全路径名或简单文件名。全路径名仅限于已知的可信目录。简单文件名(名称中没有 '/')的查找受前面描述的搜索路径限制的约束。简单文件名只能解析为已知的可信目录。

在安全进程中,将使用前面描述的路径名限制来处理组成简单文件名的所有依赖项。以全路径名或相对路径名表示的依赖项按原样使用。因此,安全进程的开发者应确保作为这些依赖项之一引用的目标目录受到适当的保护,以避免恶意侵入。

在创建安全进程时,不要使用相对路径名来表示依赖项或构造 dlopen(3C) 路径名。此限制适用于应用程序及所有依赖项。

运行时链接编程接口

在应用程序的链接编辑期间指定的依赖项,由运行时链接程序在进程初始化过程中处理。除了此机制以外,应用程序还可在执行期间通过绑定到其他目标文件来扩展其地址空间。应用程序将有效地使用处理应用程序标准依赖项所用的相同运行时链接程序服务。

延迟目标文件绑定有以下几个优点。

应用程序可使用下列典型方案来访问其他共享库。

运行时链接程序的服务将在头文件 dlfcn.h 中进行定义,并且通过共享库 libc.so.1 使该服务可用于应用程序。在以下示例中,文件 main.c 可以引用任何 dlopen(3C) 系列例程,并且应用程序 prog 可在运行时绑定到这些例程。


$ cc -o prog main.c

注 –

在 Solaris 以前的发行版中,共享库 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>

 

main(int argc, char ** argv)

{

        void *  handle;

        .....

 

        if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL) {

                (void) printf("dlopen: %s\n", dlerror());

                exit (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) 指定的任何共享库。有关版本化的更多信息,请参见协调版本化文件名。


如果找不到所需目标文件,则 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((const char *)0, RTLD_LAZY)) == NULL) {

则通过指定了 RTLD_GLOBAL 标志的 dlopen(3C),可使用从 dlopen(3C) 返回的句柄在应用程序本身、进程初始化时装入的任何依赖项或添加到进程地址空间的所有目标文件中查找符号。

重定位处理

如第 3 章,运行时链接程序中所述,在找到并装入所有目标文件后,运行时链接程序必须处理每个目标文件并执行所有必需的重定位。另外,还必须以同一方式重定位使用 dlopen(3C) 引入进程地址空间中的所有目标文件。

对于简单应用程序,此过程很简单。但是,对于使用较复杂的应用程序(包含涉及多个目标文件的 dlopen(3C) 调用,且可能包含公共依赖项)的用户,则此过程可能相当重要。

可根据重定位的时间对重定位进行分类。运行时链接程序的缺省行为是在初始化时处理所有即时引用重定位,以及在进程执行期间处理所有延迟引用(此机制通常称为延迟绑定)。

该机制也适用于模式定义为 RTLD_LAZY 时使用 dlopen(3C) 添加的所有目标文件。替代方法是要求在添加目标文件时立即执行目标文件的所有重定位。您可以使用 RTLD_NOW 模式,也可以使用链接编辑器的 -z now 选项在生成目标文件时将此要求记录在目标文件中。此重定位要求将传播至要打开的目标文件的所有依赖项。

重定位还可分为非符号重定位和符号重定位。本节的余下部分将讨论有关符号重定位的问题(无论这些重定位何时进行),并重点介绍符号查找的某些细节信息。

符号查找

如果 dlopen(3C) 获取的目标文件引用全局符号,则运行时链接程序必须从构成进程的目标文件池中查找此符号。 如果缺少直接绑定,则会将缺省符号搜索模型应用于通过 dlopen() 获取的目标文件。但是,dlopen() 模式以及构成进程的目标文件的属性允许使用替代的符号搜索模型。

需要直接绑定的目标文件将直接在关联的依赖项中搜索符号(尽管要维护后面介绍的所有属性)。 请参见直接绑定。

目标文件有两个属性会影响符号查找。第一个属性是请求目标文件的符号搜索范围,第二个属性是进程中每个目标文件提供的符号可见性。目标文件的搜索范围可以是:

world

目标文件可以查找进程中的任何其他全局目标文件。

group

目标文件只能查找同一组中的目标文件。通过使用 dlopen(3C) 获取的目标文件或使用链接编辑器的 -B group 选项生成的目标文件创建的依赖项树构成一个唯一的组。

目标文件中符号的可见性可以是:

global

可在具有 world 搜索范围的任何目标文件中引用该目标文件的符号。

local

只能在构成同一组的其他目标文件中引用该目标文件的符号。

缺省情况下,使用 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() 请求。

通过 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() 请求

多个 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() 请求

具有公共依赖项的多个 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() 打开第二个目标文件,则这两个目标文件都会被指定给一个唯一的组。这种情况可以防止一个目标文件在另一个目标文件中查找符号。

在某些实现中,初始目标文件必须导出符号以便重定位第二个目标文件。通过以下两种机制之一,可以满足此要求:

如果初始目标文件是第二个目标文件的显式依赖项,则会将该初始目标文件指定给第二个目标文件所在的组。因此,初始目标文件可以为第二个目标文件的重定位提供符号。

如果许多目标文件可使用 dlopen(3C) 打开第二个目标文件,并且每个初始目标文件必须导出相同符号以满足第二个目标文件重定位的需要,则不能对第二个目标文件指定显式依赖项。在此情况下,可使用 RTLD_PARENT 标志扩充第二个目标文件的 dlopen(3C) 模式。此标志将导致第二个目标文件所在的组以显式依赖项的方式传播至初始目标文件。

这两种方法之间有一点区别。如果指定显式依赖项,则依赖项本身将成为第二个目标文件的 dlopen(3C) 依赖项树的一部分,从而可用于使用 dlsym(3C) 的符号查找。如果使用 RTLD_PARENT 获取第二个目标文件,则使用 dlsym(3C) 的符号查找不能使用初始目标文件。

如果第二个目标文件是通过 dlopen(3C) 从具有全局符号可见性的初始目标文件获取的,则 RTLD_PARENT 模式既是冗余的,也是无害的。从应用程序或应用程序的依赖项之一调用 dlopen(3C) 时,通常会发生这种情况。

获取新符号

进程可以使用 dlsym(3C) 获取特定符号的地址。此函数采用句柄和符号名称,并将符号地址返回给调用方。该句柄通过以下方式指示符号搜索:

在以下可能很常见的示例中,应用程序首先会将其他目标文件添加到其地址空间。然后,应用程序会使用 dlsym(3C) 来查找函数或数据符号。接下来,应用程序将使用这些符号来调用这些新目标文件中提供的服务。文件 main.c 包含以下代码:


#include    <stdio.h>

#include    <dlfcn.h>

 

main()

{

        void *  handle;

        int *   dptr, (* fptr)();

 

        if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL) {

                (void) printf("dlopen: %s\n", dlerror());

                exit (1);

        }

 

        if (((fptr = (int (*)())dlsym(handle, "foo")) == NULL) ||

            ((dptr = (int *)dlsym(handle, "bar")) == NULL)) {

                (void) printf("dlsym: %s\n", dlerror());

                exit (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,应用程序可以测试是否存在其他符号。符号搜索采用重定位调用目标文件时所用的同一模型。 请参见缺省符号查找模型。例如,如果应用程序 prog 包含以下代码片段:


        if ((fptr = (int (*)())dlsym(RTLD_DEFAULT, "foo")) != NULL)

                (*fptr)();

则会首先在 prog 中搜索 foo,然后在 /lib/libc.so.1 中搜索。如果此代码片段包含在图 3–1 中显示的示例的文件 B.so.1 中,则会继续在 B.so.1 中搜索 foo,然后在 C.so.1 中搜索。

此机制为未定义的弱引用的使用提供了强大而灵活的替代方案,如弱符号中所述。

使用插入

使用特殊句柄 RTLD_NEXT,应用程序可在符号范围内查找下一个符号。例如,如果应用程序 prog 包含以下代码片段:


        if ((fptr = (int (*)())dlsym(RTLD_NEXT, "foo")) == NULL) {

                (void) printf("dlsym: %s\n", dlerror());

                exit (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 (0);

                }

        }

 

        (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,可提供可预测的插入方法。在一般目标文件依赖项中使用此方法时应该十分小心,因为目标文件的实际装入顺序有时无法预测。

调试帮助

Solaris 链接程序附带有调试库和 mdb(1) 模块。使用调试库,可以更详细地跟踪运行时链接过程。使用 mdb(1) 模块,可以进行交互式进程调试。

调试库

此调试库有助于了解或调试应用程序和依赖项的执行。使用此库显示的信息类型应保持不变。不过,信息的确切格式可能随发行版的不同而有所变化。

不了解运行时链接程序的用户可能不熟悉某些调试输出。不过,也许您希望大概了解其中许多方面。

可使用环境变量 LD_DEBUG 来启用调试。所有调试输出都使用进程标识符作为前缀,并且在缺省情况下会指示为标准错误。必须使用一个或多个标记来扩充此环境变量,以指示所需调试的类型。

使用 LD_DEBUG=help,可以显示 LD_DEBUG 中可用的标记。当进程在显示信息后终止时,可以使用任何动态可执行文件请求此信息。


$ LD_DEBUG=help prog

11693:

11693:           For debugging the runtime linking of an application:

11693:                  LD_DEBUG=token1,token2  prog

11693:           enables diagnostics to the stderr.  The additional

11693:           option:

11693:                  LD_DEBUG_OUTPUT=file

11693:           redirects the diagnostics to an output file created

11593:           using the specified name and the process id as a

11693:           suffix.  All diagnostics are prepended with the

11693:           process id.

11693:

11693:

11693: audit     display runtime link-audit processing

11693: basic     provide basic trace information/warnings

11693: bindings  display symbol binding; detail flag shows

11693:             absolute:relative addresses

11693: cap       display hardware/software capability processing

11693: detail    provide more information in conjunction with other

11693:             options

11693: files     display input file processing (files and libraries)

11693: help      display this help message

11693: init      display init and fini processing

11693: libs      display library search paths

11693: move      display move section processing

11693: reloc     display relocation processing

11693: symbols   display symbol table processing

11693: tls       display TLS processing info

11693: unused    display unused/unreferenced files

11693: versions  display version processing

此示例显示对运行时链接程序有意义的选项。确切选项可能随发行版的不同而有所变化。

环境变量 LD_DEBUG_OUTPUT 可用来指定使用输出文件来代替标准错误。进程标识符将作为后缀添加到输出文件。

对于安全的应用程序,不允许进行调试。

其中一个最有用的调试选项是显示运行时进行的符号绑定。以下示例使用很简单的动态可执行文件,该可执行文件依赖于两个局部共享库。


$ cat bar.c

int bar = 10;

$ cc -o bar.so.1 -K pic -G bar.c

 

$ cat foo.c

foo(int data)

{

        return (data);

}

$ cc -o foo.so.1 -K pic -G foo.c

 

$ cat main.c

extern  int     foo();

extern  int     bar;

 

main()

{

        return (foo(bar));

}

$ cc -o prog main.c -R/tmp:. foo.so.1 bar.so.1

通过设置 LD_DEBUG=bindings,可以显示运行时符号绑定。


$ LD_DEBUG=bindings prog

11753: .......

11753: binding file=prog to file=./bar.so.1: symbol bar

11753: .......

11753: transferring control: prog

11753: .......

11753: binding file=prog to file=./foo.so.1: symbol foo

11753: .......

符号 bar 是即时重定位所需的,将在应用程序获取控制权之前绑定。然而,符号 foo 是延迟重定位所需的,将在应用程序获得第一次调用函数的控制权之后绑定。此重定位说明了延迟绑定的缺省模式。如果设置了环境变量 LD_BIND_NOW,则所有符号绑定都会在应用程序获取控制权之前进行。

通过设置 LD_DEBUG=bindings,detail,可提供有关实际绑定位置的实际地址和相对地址的其他信息。

当运行时链接程序执行函数重定位时,将重写与函数 .plt 关联的数据。通过 .plt 进行的后续调用将直接转至该函数。环境变量 LD_BIND_NOT 可设置为任何值以免更新此数据。通过将此变量与对详细绑定的调试请求一起使用,可以全盘了解所有函数绑定的运行时情况。此组合可能会产生大量输出,从而导致应用程序性能下降。

可以使用 LD_DEBUG 来显示使用的各个搜索路径。例如,通过设置 LD_DEBUG=libs,可以显示用于查找所有依赖项的搜索路径机制。


$ LD_DEBUG=libs prog

11775:

11775: find object=foo.so.1; searching

11775:  search path=/tmp:.  (RPATH from file prog)

11775:  trying path=/tmp/foo.so.1

11775:  trying path=./foo.so.1

11775:

11775: find object=bar.so.1; searching

11775:  search path=/tmp:.  (RPATH from file prog)

11775:  trying path=/tmp/bar.so.1

11775:  trying path=./bar.so.1

11775: .......

应用程序 prog 中记录的运行路径将影响两个依赖项 foo.so.1 和 bar.so.1 的搜索。

同样,可通过设置 LD_DEBUG=symbols 来显示每个符号查找的搜索路径。组合使用 symbols 和 bindings 可生成符号重定位进程的完整信息。


$ LD_DEBUG=bindings,symbols prog

11782: .......

11782: symbol=bar;  lookup in file=./foo.so.1  [ ELF ]

11782: symbol=bar;  lookup in file=./bar.so.1  [ ELF ]

11782: binding file=prog to file=./bar.so.1: symbol bar

11782: .......

11782: transferring control: prog

11782: .......

11782: symbol=foo;  lookup in file=prog  [ ELF ]

11782: symbol=foo;  lookup in file=./foo.so.1  [ ELF ]

11782: binding file=prog to file=./foo.so.1: symbol foo

11782: .......

在前面的示例中,未在应用程序 prog 中搜索符号 bar。省略数据引用查找是因为在处理复制重定位时使用了优化。有关此重定位类型的更多详细信息,请参见复制重定位。

调试器模块

调试器模块提供了一组可在 mdb(1) 下装入的 dcmds 和 walkers。此模块可用于检查运行时链接程序的各种内部数据结构。许多调试信息都要求您熟悉运行时链接程序的内部构造,并且该信息会随发行版的不同而有所变化。但是,这些数据结构中的某些元素显示了动态链接进程的基本组件,有助于进行一般调试。

以下示例显示了一些将 mdb(1) 与运行时链接程序调试器模块一起使用的简单情况。


$ cat main.c

#include  <dlfnc.h>



int main()

{

        void *  handle;

        void (* fptr)();



        if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL)

                return (1);



        if ((fptr = (void (*)())dlsym(handle, "foo")) == NULL)

                return (1);



        (*fptr)();

        return (0);

}

$ cc -o main main.c -R.

如果 mdb(1) 尚未自动装入调试器模块 ld.so,则显式装入该模块。之后可以检查调试器模块的功能。


$ mdb main

> ::load ld.so

> ::dmods -l ld.so



ld.so

-----------------------------------------------------------------

  dcmd Bind                 - Display a Binding descriptor

  dcmd Callers              - Display Rt_map CALLERS binding descriptors

  dcmd Depends              - Display Rt_map DEPENDS binding descriptors

  dcmd ElfDyn               - Display Elf_Dyn entry

  dcmd ElfEhdr              - Display Elf_Ehdr entry

  dcmd ElfPhdr              - Display Elf_Phdr entry

  dcmd Groups               - Display Rt_map GROUPS group handles

  dcmd GrpDesc              - Display a Group Descriptor

  dcmd GrpHdl               - Display a Group Handle

  dcmd Handles              - Display Rt_map HANDLES group descriptors

  ....

> ::bp main

> :r

进程中的每个动态库都表示为链接映射 Rt_map,该映射在链接映射列表中对其进行维护。可使用 Rt_maps 显示进程的所有链接映射。


> ::Rt_maps

Link-map lists (dynlm_list): 0xffbfe0d0

----------------------------------------------

  Lm_list: 0xff3f6f60  (LM_ID_BASE)

  ----------------------------------------------

    lmco        rtmap       ADDR()     NAME()

    ----------------------------------------------

    [0xc]       0xff3f0fdc 0x00010000 main

    [0xc]       0xff3f1394 0xff280000 /lib/libc.so.1

  ----------------------------------------------

  Lm_list: 0xff3f6f88  (LM_ID_LDSO)

  ----------------------------------------------

    [0xc]       0xff3f0c78 0xff3b0000 /lib/ld.so.1

可使用 Rt_map 显示单个链接映射。


> 0xff3f9040::Rt_map

Rt_map located at: 0xff3f9040

     NAME: main

 PATHNAME: /export/home/user/main

     ADDR: 0x00010000         DYN: 0x000207bc

     NEXT: 0xff3f9460        PREV: 0x00000000

      FCT: 0xff3f6f18    TLSMODID:          0

     INIT: 0x00010710        FINI: 0x0001071c

   GROUPS: 0x00000000     HANDLES: 0x00000000

  DEPENDS: 0xff3f96e8     CALLERS: 0x00000000

    .....

可使用 ElfDyn dcmd 显示目标文件的 .dynamic 节。以下示例显示了前 4 项。


> 0x000207bc,4::ElfDyn

Elf_Dyn located at: 0x207bc

    0x207bc  NEEDED       0x0000010f

Elf_Dyn located at: 0x207c4

    0x207c4  NEEDED       0x00000124

Elf_Dyn located at: 0x207cc

    0x207cc  INIT         0x00010710

Elf_Dyn located at: 0x207d4

    0x207d4  FINI         0x0001071c

mdb(1) 在设置推迟断点时也很有用。在此示例中,函数 foo() 中的断点可能会很有用。但是,在对 foo.so.1 执行 dlopen(3C) 之前,调试器不知道此符号。推迟断点会指示调试器在装入动态库时设置实际断点。


> ::bp foo.so.1`foo

> :c

> mdb: You've got symbols!

> mdb: stop at foo.so.1`foo

mdb: target stopped at:

foo.so.1`foo:   save      %sp, -0x68, %sp

此时,已经装入了新目标文件:


> *ld.so`lml_main::Rt_maps

lmco    rtmap       ADDR()     NAME()

----------------------------------------------

[0xc]   0xff3f0fdc 0x00010000 main

[0xc]   0xff3f1394 0xff280000 /lib/libc.so.1

[0xc]   0xff3f9ca4 0xff380000 ./foo.so.1

[0xc]   0xff37006c 0xff260000 ./bar.so.1

foo.so.1 的链接映射显示了 dlopen(3C) 返回的句柄。可使用 Handles 来扩展此结构。


> 0xff3f9ca4::Handles -v

HANDLES for ./foo.so.1

----------------------------------------------

  HANDLE: 0xff3f9f60 Alist[used 1: total 1]

    ----------------------------------------------

    Group Handle located at: 0xff3f9f28

    ----------------------------------------------

        owner:               ./foo.so.1

        flags: 0x00000000    [ 0 ]

       refcnt:          1    depends: 0xff3f9fa0 Alist[used 2: total 4]

        ----------------------------------------------

        Group Descriptor located at: 0xff3f9fac

           depend: 0xff3f9ca4    ./foo.so.1

            flags: 0x00000003    [ AVAIL-TO-DLSYM,ADD-DEPENDENCIES ]

        ----------------------------------------------

        Group Descriptor located at: 0xff3f9fd8

           depend: 0xff37006c    ./bar.so.1

            flags: 0x00000003    [ AVAIL-TO-DLSYM,ADD-DEPENDENCIES ]

句柄的依赖项由一组链接映射组成,这些链接映射表示可满足 dlsym(3C) 请求的句柄的目标文件。在此情况下,依赖项为 foo.so.1 和 bar.so.1。


注 –

前面的示例提供了调试器模块功能的基本指南,但确切的命令、用法和输出可能会随发行版的不同而有所变化。有关系统中可用的确切功能,请参阅用法和帮助信息。