链接编辑器提供了许多支持接口,用于实现链接编辑器的监视和修改功能以及运行时链接程序处理功能。要使用这些接口,除了了解前几章中所介绍的概念之外,通常还需要对链接编辑的概念有更深入的了解。本章介绍了以下接口:
ld-support – 链接编辑器支持接口
rtld-audit – 运行时链接程序审计接口
rtld-debugger – 运行时链接程序调试器接口
链接编辑器可执行许多操作,其中包括打开文件以及串联这些文件中的各节。监视并不时修改这些操作通常会对编译系统的各组件有利。
本节介绍了 ld-support 接口,通过此接口可以检查输入文件,并在某种程度上还可以修改链接编辑过程中所用到的那些文件的输入文件数据。使用此接口的两个应用程序分别为链接编辑器本身(使用此接口处理可重定位目标文件内的调试信息)以及 make(1S) 实用程序(使用此接口保存状态信息)。
ld-support 接口由提供一个或多个支持接口例程的支持库组成。该库是在链接编辑过程中装入的。在链接编辑的不同阶段会调用该库中的某些支持例程。
使用此接口时,应该熟悉 elf(3ELF) 结构和文件格式。
链接编辑器可接受一个或多个通过 SGS_SUPPORT 环境变量或链接编辑器的 -S 选项提供的支持库。此环境变量由冒号分隔的支持库列表组成:
$ SGS_SUPPORT=./support.so.1:libldstab.so.1 cc ... |
-S 选项用于指定单个支持库。可以指定多个 -S 选项:
$ LD_OPTIONS="-S./support.so.1 -Slibldstab.so.1" cc ... |
支持库是共享库。链接编辑器会使用 dlopen(3C) 按照指定库的顺序打开每个支持库。如果遇到环境变量和 -S 选项,则首先处理通过此环境变量指定的支持库。然后,使用 dlsym(3C) 搜索每个支持库以查找所有支持接口例程。这些支持例程随后会在链接编辑的不同阶段调用。
支持库必须与所调用的链接编辑器的 ELF 类保持一致,可以是 32 位或 64 位。 有关更多详细信息,请参见32 位环境和 64 位环境。
缺省情况下,链接编辑器使用 Solaris 支持库 libldstab.so.1 来处理和压缩输入可重定位目标文件内提供的编译器生成的调试信息。如果对使用 -S 选项指定的任何支持库调用链接编辑器,则抑制此缺省处理。如果除支持库服务之外还要求 libldstab.so.1 的缺省处理,请将 libldstab.so.1 显式添加到提供给链接编辑器的支持库列表中。
如32 位环境和 64 位环境中所述,64 位链接编辑器 ld(1) 可以生成 32 位目标文件,32 位链接编辑器可以生成 64 位目标文件。对于其中每个目标文件,都定义关联支持接口。
64 位目标文件的支持接口类似于 32 位目标文件的接口,但是以 64 为后缀结尾。例如 ld_start() 和 ld_start64()。通过此约定,两种方式实现的支持接口可以分别位于 32 位类和 64 位类的单个共享库 libldstab.so.1 中。
可以为 SGS_SUPPORT 环境变量指定 _32 或 _64 后缀,并且可以使用链接编辑器选项 -z ld32 和 -z ld64 定义 -S 选项的要求。这些定义只能分别通过链接编辑器的 32 位或 64 位类来解释。通过此操作,可在可能不知道链接编辑器的类的情况下指定两类支持库。
所有 ld-support 接口均在头文件 link.h 中定义。所有接口参数均为基本的 C 类型或 ELF 类型。可以通过 ELF 访问库 libelf 检查 ELF 数据类型。有关 libelf 内容的说明,请参见 elf(3ELF)。以下接口函数由 ld-support 接口提供,并且按照预期的使用顺序进行了说明。
uint_t ld_version(uint_t version);
链接编辑器使用其可以支持的最高版本的 ld-support 接口来调用此接口。支持库可以检验此版本是否达到使用的最低要求,并返回支持库要求使用的版本。此版本通常为 LD_SUP_VCURRENT。
如果支持库没有提供此接口,则采用初始支持级别 LD_SUP_VERSION1。
如果支持库返回版本零,或者返回的版本值高于链接编辑器所支持的 ld-support 接口的版本,则无法使用支持库。
此函数在初始验证链接编辑器命令行之后调用,表示开始处理输入文件。
void ld_start(const char * name, const Elf32_Half type, const char * caller); void ld_start64(const char * name, const Elf64_Half type, const char * caller);
name 是所创建的输出文件名。type 是输出文件类型,可以为 ET_DYN、ET_REL 或 ET_EXEC,如 sys/elf.h 中所定义。caller 是调用接口的应用程序,通常为 /usr/ccs/bin/ld。
void ld_file(const char * name, const Elf_Kind kind, int flags, Elf * elf); void ld_file64(const char * name, const Elf_Kind kind, int flags, Elf * elf);
name 是要处理的输入文件。kind 表示输入文件类型,可以是 ELF_K_AR 或 ELF_K_ELF,如 libelf.h 中所定义。flags 表示链接编辑器获取文件的方式,可以是以下一个或多个定义:
LD_SUP_DERIVED-文件名不是在命令行中显式指定的。文件是从 -l 扩展派生而来,或者文件标识提取的归档成员。
LD_SUP_EXTRACTED-文件提取自归档。
LD_SUP_INHERITED-文件作为命令行共享库的依赖项获取。
如果未指定 flags 值,则表明已在命令行中显式指定了输入文件。elf 是指向文件的 ELF 描述符的指针。
此函数会针对输入文件的每一节调用,并且在链接编辑器确定是否应将节传播给输出文件之前即会调用此函数。此函数不同于 ld_section() 处理,后者仅针对组成输出文件的各节进行调用。
void ld_input_section(const char * name, Elf32_Shdr ** shdr, Elf32_Word sndx, Elf_Data * data, Elf * elf, unit_t flags); void ld_input_section64(const char * name, Elf64_Shdr ** shdr, Elf64_Word sndx, Elf_Data * data, Elf * elf, uint_t flags);
name 是输入节的名称。shdr 是指向关联节标题的指针。sndx 是输入文件内的节索引。data 是指向关联数据缓冲区的指针。elf 是指向文件的 ELF 描述符的指针。flags 保留供将来使用。
节标题的修改是通过重新分配节标题并为新标题重新指定 *shdr 来完成的。链接编辑器使用从 ld_input_section() 返回时 *shdr 所指向的节标题信息来处理节。
通过重新分配数据并重新指定 Elf_Data 缓冲区的 d_buf 指针,可以修改数据。对数据进行任何修改都应确保正确设置 Elf_Data 缓冲区的 d_size 元素。对于成为输出映像一部分的输入节,将 d_size 元素设置为零可以有效地删除输出映像中的数据。
flags 字段指向初始值为零的 uint_t 数据字段。虽然在将来的更新中可通过链接编辑器或支持库来指定标志,但是当前未指定任何标志。
此函数针对传播给输出文件的输入文件的每一节调用,并且在执行任何节数据处理之前即会调用此函数。
void ld_section(const char * name, Elf32_Shdr * shdr, Elf32_Word sndx, Elf_Data * data, Elf * elf); void ld_section64(const char * name, Elf64_Shdr * shdr, Elf64_Word sndx, Elf_Data * data, Elf * elf);
name 是输入节的名称。shdr 是指向关联节标题的指针。sndx 是输入文件内的节索引。data 是指向关联数据缓冲区的指针。elf 是指向文件 ELF 描述符的指针。
通过重新分配数据并重新指定 Elf_Data 缓冲区的 d_buf 指针,可以修改数据。对数据进行任何修改都应确保正确设置 Elf_Data 缓冲区的 d_size 元素。对于成为输出映像一部分的输入节,将 d_size 元素设置为零可以有效地删除输出映像中的数据。
使用链接编辑器的 -s 选项删除的各节,或者由于 SHT_SUNW_COMDAT 处理或SHF_EXCLUDE 标识而废弃的各节不会向 ld_section() 进行报告。请参见COMDAT 节和表 7–8。
此函数在完成输入文件处理之后但在对输出文件进行布局之前调用。
void ld_input_done(uint_t flags);
flags 字段指向初始值为零的 uint_t 数据字段。虽然在将来的更新中可通过链接编辑器或支持库来指定标志,但是当前未指定任何标志。
void ld_atexit(int status); void ld_atexit64(int status);
status 是将由链接编辑器返回的 exit(2) 代码,可以是 EXIT_FAILURE 或 EXIT_SUCCESS,如 stdlib.h 中所定义。
以下示例创建了一个支持库,其中列显了在 32 位链接编辑过程中处理的任何可重定位目标文件的节名。
$ cat support.c #include <link.h> #include <stdio.h> static int indent = 0; void ld_start(const char * name, const Elf32_Half type, const char * caller) { (void) printf("output image: %s\n", name); } void ld_file(const char * name, const Elf_Kind kind, int flags, Elf * elf) { if (flags & LD_SUP_EXTRACTED) indent = 4; else indent = 2; (void) printf("%*sfile: %s\n", indent, "", name); } void ld_section(const char * name, Elf32_Shdr * shdr, Elf32_Word sndx, Elf_Data * data, Elf * elf) { Elf32_Ehdr * ehdr = elf32_getehdr(elf); if (ehdr->e_type == ET_REL) (void) printf("%*s section [%ld]: %s\n", indent, "", (long)sndx, name); } |
此支持库依赖于 libelf 来提供用于确定输入文件类型的 ELF 访问函数 elf32_getehdr(3ELF)。此支持库通过使用以下命令生成:
$ cc -o support.so.1 -G -K pic support.c -lelf -lc |
以下示例说明了从可重定位目标文件和本地归档库构造普通应用程序所产生的节诊断信息。使用 -S 选项不仅可以处理缺省调试信息,还可以调用支持库。
$ LD_OPTIONS="-S./support.so.1 -Slibldstab.so.1" \ cc -o prog main.c -L. -lfoo output image: prog file: /opt/COMPILER/crti.o section [1]: .shstrtab section [2]: .text ....... file: /opt/COMPILER/crt1.o section [1]: .shstrtab section [2]: .text ....... file: /opt/COMPILER/values-xt.o section [1]: .shstrtab section [2]: .text ....... file: main.o section [1]: .shstrtab section [2]: .text ....... file: ./libfoo.a file: ./libfoo.a(foo.o) section [1]: .shstrtab section [2]: .text ....... file: /lib/libc.so file: /opt/COMPILER/crtn.o section [1]: .shstrtab section [2]: .text ....... |
为了简化输出,已经减少了本示例中显示的节数。另外,编译器驱动程序所包含的文件也会有所不同。
进程可以使用 rtld-audit 接口访问与其自身相关的运行时链接信息。配置共享库中介绍对共享库的运行时配置即是使用此机制的一个示例。
rtld-audit 接口实现为提供一个或多个审计接口例程的审计库。如果将该库作为进程的一部分装入,则运行时链接程序会在进程执行的不同阶段调用审计例程。审计库可以使用这些接口访问以下各项:
依赖项搜索。可以通过审计库替换搜索路径。
与装入的目标文件相关的信息。
装入的目标文件之间进行的符号绑定。可以通过审计库更改这些绑定。
通过利用过程链接表各项所提供的延迟绑定机制,可以审计函数调用及其返回值。可以通过审计库修改函数参数及其返回值。请参见过程链接表(特定于处理器)。
通过预装入专用的共享库可以实现其中的部分功能。但是,预装入的目标文件与进程目标文件存在于同一名称空间内。这种预装入通常会限制预装入的共享库的实现或者使实现变得更为复杂。 rtld-audit 接口会为用户提供唯一的名称空间,用于在其中执行其审计库。此名称空间可确保在进程内进行正常绑定时审计库不会侵入。
运行时链接程序将动态可执行文件与其依赖项绑定时,会生成链接映射的链接列表,用于对此进程进行说明。/usr/include/sys/link.h 中定义的链接映射结构说明了进程内的每个目标文件。绑定应用程序的目标文件所需的符号搜索机制会遍历此链接映射的列表。此链接映射列表用于提供进程符号解析的名称空间。
运行时链接程序也通过链接映射来进行说明。此链接映射以不同于应用程序目标文件列表的列表中进行维护。因此,运行时链接程序驻留在其自己唯一的名称空间中,从而可防止以任何方式将应用程序直接绑定到运行时链接程序内的服务。应用程序只能通过过滤器 libc.so.1 或libdl.so.1 调用运行时链接程序的公共服务。
rtld-audit 接口使用自己的用于维护审计库的链接映射列表。因此,在应用程序的的符号绑定要求中,不涉及审计库。通过 dlmopen(3C) 可检查应用程序链接映射列表。将 RTLD_NOLOAD 标志用于 dlmopen(3C) 时,审计库可以在不装入目标文件的情况下查询此目标文件是否存在。
/usr/include/link.h 中定义了两个标识符,用于定义应用程序和运行时链接程序的链接映射列表:
#define LM_ID_BASE 0 /* application link-map list */ #define LM_ID_LDSO 1 /* runtime linker link-map list */
针对每个 rtld-audit 支持库会指定一个唯一的新链接映射标识符。
审计库的生成方式与其他任何共享库的生成方式相同。但是,必须注意进程内审计库名称空间的唯一性。
该库必须提供所有依赖性需求。
该库不应使用无法用于进程内多个接口实例的系统接口。
如果审计库调用 printf(3C),则审计库必须定义与 libc 之间的依赖性。请参见生成共享库输出文件。由于审计库具有唯一的名称空间,因此,所审计的应用程序中提供的 libc 无法满足符号引用。如果审计库依赖于 libc,则会向进程中装入两种版本的 libc.so.1。一种版本用于满足应用程序链接映射列表的绑定要求,另一种版本用于满足审计链接映射列表的绑定要求。
要确保生成的审计库会记录所有的依赖项,请使用链接编辑器的 -z defs 选项。
部分系统接口会假定其是进程内实现的唯一实例,例如信号和 malloc(3C)。审计库应该避免使用此类接口,因为这样做可能会无意中更改应用程序的行为。
审计库可以使用 mapmalloc(3MALLOC) 来分配内存,因为此分配方法可以与应用程序通常使用的任何分配方案同时存在。
rtld-audit 接口可通过以下两种方法之一来启用。每种方法都会指示一个所审计的目标文件的范围。
全局审计,通过使用环境变量 LD_AUDIT 来启用。 通过此方法可用的审计库附带有与进程所使用的所有动态库相关的信息。
局部审计,通过生成目标文件时在目标文件内记录的动态项来启用。通过此方法可用的审计库附带有与标识用于审计的那些动态库相关的信息。
任一调用方法都包含一个字符串,其中包含通过 dlopen(3C) 装入的以冒号分隔的共享库列表。每个目标文件都装入各自的审计链接映射列表中。使用 dlsym(3C) 可搜索每个目标文件中的审计例程。在应用程序执行过程中的不同阶段会调用找到的审计例程。
通过 rtld-audit 接口可以提供多个审计库。希望以此方式使用的审计库不应更改通常由运行时链接程序返回的绑定。更改这些绑定会在后面的审计库中产生意外结果。
安全应用程序只能从受信任的目录中获取审计库。缺省情况下,用于 32 位目标文件的运行时链接程序可识别的受信任的目录仅有 /lib/secure 和 /usr/lib/secure。对于 64 位目标文件,受信任的目录是 /lib/secure/64 和 /usr/lib/secure/64。
使用链接编辑器选项 -p 或 -P 生成目标文件时,可以确定局部审计要求。例如,要使用审计库 audit.so.1 审计 libfoo.so.1,请在链接编辑时使用 -p 选项记录要求:
$ cc -G -o libfoo.so.1 -Wl,-paudit.so.1 -K pic foo.c $ dump -Lv libfoo.so.1 | fgrep AUDIT [3] AUDIT audit.so.1 |
在运行时,如果存在此审计标识符,则会装入审计库并将与标识目标文件相关的信息传递到此审计库。
如果单独使用此机制,则在装入审计库之前会显示搜索标识目标文件之类的信息。要提供尽可能多的审计信息,需要将存在的要求局部审计的目标文件传播给此目标文件的用户。例如,如果生成的应用程序依赖于 libfoo.so.1,则会对此应用程序进行标识,指明其依赖项需要审计:
$ cc -o main main.c libfoo.so.1 $ dump -Lv main | fgrep AUDIT [5] DEPAUDIT audit.so.1 |
通过此机制启用的审计会导致向审计库中传递与所有应用程序显式依赖项有关的信息。使用链接编辑器的 -P 选项,还可以在创建目标文件时直接记录此依赖项审计:
$ cc -o main main.c -Wl,-Paudit.so.1 $ dump -Lv main | fgrep AUDIT [5] DEPAUDIT audit.so.1 |
通过将环境变量 LD_NOAUDIT 设置为非空值,可以在运行时禁用审计。
rtld-audit 接口提供了以下函数。这些函数按照其预期的使用顺序进行说明。
为了简化讨论,对体系结构或目标文件类特定接口的引用会缩减为其通用名称。例如,对 la_symbind32() 和 la_symbind64() 的引用会指定为 la_symbind()。
此函数可提供运行时链接程序与审计库之间的初次握手。必须提供此接口才能装入审计库。
uint_t la_version(uint_t version);
运行时链接程序通过其可以支持的 version 最高的 rtld-audit 接口来调用此接口。审计库可以检验此版本是否足以供其使用,并返回审计库预期使用的版本。此版本通常为 /usr/include/link.h 中定义的 LAV_CURRENT。
如果审计库返回零,或者返回的版本高于运行时链接程序所支持的 rtld-audit 接口的版本,则会废弃该审计库。
void la_activity(uintptr_t * cookie, uint_t flags);
cookie 标识作为链接映射标题的目标文件。flags 表示活动类型,如 /usr/include/link.h 中所定义:
LA_ACT_ADD-正在向链接映射列表中添加目标文件。
LA_ACT_DELETE-正在从链接映射列表中删除目标文件。
LA_ACT_CONSISTENT-已经完成目标文件活动。
char * la_objsearch(const char * name, uintptr_t * cookie, uint_t flags);
name 表示所搜索的文件名或路径名。cookie 标识启动搜索的目标文件。flags 标识 name 的来源和创建方式,如 /usr/include/link.h 中所定义:
LA_SER_ORIG-初始搜索名称。通常,此名称表示记录为 DT_NEEDED 项的文件名或者提供给 dlopen(3C) 的参数。
LA_SER_RUNPATH-已经通过 运行路径 组件创建了路径名。
LA_SER_DEFAULT-已经通过缺省搜索路径组件创建了路径名。
LA_SER_CONFIG-路径组件源自配置文件。请参见crle(1)。
LA_SER_SECURE-路径组件特定于安全目标文件。
返回值会指明运行时链接程序应该继续处理的搜索路径名。值为零表示应该忽略此路径。监视搜索路径的审计库会返回 name。
uint_t la_objopen(Link_map * lmp, Lmid_t lmid, uintptr_t * cookie);
lmp 提供说明新目标文件的链接映射结构。lmid 标识添加了目标文件的链接映射列表。cookie 提供指向某个标识符的指针。此标识符会初始化为目标文件 lmp。审计库可以修改此标识符,以便更好地标识其他 rtld-audit 接口例程的目标文件。
la_objopen() 函数会返回表示与此目标文件相关的符号绑定的值。返回值是 /usr/include/link.h 中定义的以下值的掩码:
LA_FLG_BINDTO-审计到此目标文件的符号绑定。
LA_FLG_BINDFROM-审计来自此目标文件的符号绑定。
通过这些值,审计程序可以选择要使用 la_symbind() 监视的目标文件。返回值为零表示绑定信息与此目标文件无关。
例如,审计程序可以监视从 libfoo.so 到 libbar.so 的绑定。将 la_objopen() 用于 libfoo.so 会返回 LA_FLG_BINDFROM。将 la_objopen()用于 libbar.so 会返回 LA_FLG_BINDTO。
审计程序可以监视 libfoo.so 与 libbar.so 之间的所有绑定。将 la_objopen() 用于这两个目标文件会返回 LA_FLG_BINDFROM 和 LA_FLG_BINDTO。
审计程序可以监视到 libbar.so 的所有绑定。将 la_objopen() 用于 libbar.so 会返回 LA_FLG_BINDTO。所有 la_objopen() 调用都会返回 LA_FLG_BINDFROM。
此函数在过滤器装入新的 filtee 时调用。请参见作为过滤器的共享库。
int la_objfilter(uintptr_t * fltrcook, const char * fltestr, uintptr_t * fltecook, uint_t flags);
fltrcook 标识过滤器。fltestr 指向 filtee 字符串。fltecook 标识 filtee。flags 当前未使用。对于过滤器和 filtee,la_objfilter() 在 la_objopen() 之后调用。
值为零表示应该忽略此 filtee。监视过滤器使用情况的审计库会返回非零值。
为应用程序装入所有目标文件之后但在将控制权转交给应用程序之前,会调用一次此函数。
void la_preinit(uintptr_t * cookie);
cookie 标识启动进程的主目标文件,通常为动态可执行文件。
在已经通过 la_objopen() 标记用于绑定通知的两个目标文件之间进行绑定时,会调用此函数。
uintptr_t la_symbind32(Elf32_Sym * sym, uint_t ndx, uintptr_t * refcook, uintptr_t * defcook, uint_t * flags); uintptr_t la_symbind64(Elf64_Sym * sym, uint_t ndx, uintptr_t * refcook, uintptr_t * defcook, uint_t * flags, const char * sym_name);
sym 是构造的符号结构,其 sym->st_value 表示所绑定的符号定义的地址。 请参见 /usr/include/sys/elf.h。la_symbind32() 可将 sym->st_name 调整为指向实际符号名称。la_symbind64() 可保留 sym->st_name 作为绑定目标文件字符串表的索引。
ndx 表示绑定目标文件的动态符号表内的符号索引。refcook 标识引用此符号的目标文件。此标识符与传递给 la_objopen() 函数的标识符相同,此函数会返回 LA_FLG_BINDFROM。defcook 标识定义此符号的目标文件。此标识符与传递给 la_objopen() 的标识符相同,此函数会返回 LA_FLG_BINDTO。
flags 指向可以传送与绑定相关的信息的数据项。此数据项还可用于修改对此过程链接表项的继续审计。该值是 /usr/include/link.h 中定义的符号绑定标志的掩码。
可以为 la_symbind() 提供以下标志:
LA_SYMB_DLSYM-由于调用 dlsym(3C) 而发生的符号绑定。
LA_SYMB_ALTVALUE (LAV_VERSION2)-通过先前调用 la_symbind() 为符号值返回替换值。
如果 la_pltenter() 或 la_pltexit() 函数存在,则对于过程链接表的各项,这些函数在 la_symbind() 之后调用。每次引用符号时都会调用这些函数。 另请参见审计接口限制。
la_symbind() 可以提供以下标志来更改此缺省行为。这些标志可应用于按位或运算(包含边界值),并通过 flags 参数来指示其值。
LA_SYMB_NOPLTENTER-请勿针对此符号调用 la_pltenter() 函数。
LA_SYMB_NOPLTEXIT-请勿针对此符号调用 la_pltexit() 函数。
返回值表示在此调用后将控制权传递到的地址。监视符号绑定的审计库应该返回值 sym->st_value,以便将控制权传递给绑定符号定义。审计库可以通过返回不同的值对符号绑定进行专门重定向。
sym_name 仅适用于 la_symbind64(),其中包含所处理的符号的名称。对于 32 位接口,可在 sym->st_name 字段中使用此名称。
这些函数是系统特定的。调用过程链接表中位于已经标记用于绑定通知的两个目标文件之间的一项时,会调用这些函数。
uintptr_t la_sparcv8_pltenter(Elf32_Sym * sym, uint_t ndx, uintptr_t * refcook, uintptr_t * defcook, La_sparcv8_regs * regs, uint_t * flags); uintptr_t la_sparcv9_pltenter(Elf64_Sym * sym, uint_t ndx, uintptr_t * refcook, uintptr_t * defcook, La_sparcv9_regs * regs, uint_t * flags, const char * sym_name); uintptr_t la_i86_pltenter(Elf32_Sym * sym, uint_t ndx, uintptr_t * refcook, uintptr_t * defcook, La_i86_regs * regs, uint_t * flags); uintptr_t la_amd64_pltenter(Elf64_Sym * sym, uint_t ndx, uintptr_t * refcook, uintptr_t * defcook, La_amd64_regs * regs, uint_t * flags, const char * sym_name);
sym、ndx、refcook、defcook 和 sym_name 提供的信息与传递给 la_symbind() 的信息相同。
对于 la_sparcv8_pltenter() 和 la_sparcv9_pltenter(),regs 指向输出寄存器。对于 la_i86_pltenter(),regs 指向栈寄存器和帧寄存器。对于 la_amd64_pltenter(),regs 指向栈寄存器、帧寄存器以及用于传递整数参数的寄存器。regs 在 /usr/include/link.h 中定义。
flags 指向可以传送与绑定相关的信息的数据项。此数据项可用于修改对此过程链接表项的继续审计。此数据项与 la_symbind() 中的 flags 指向的数据项相同。
la_pltenter() 可以提供以下标志来更改当前的审计行为。这些标志可应用于按位或运算(包含边界值),并通过 flags 参数来指示其值。
LA_SYMB_NOPLTENTER-不能针对此符号再次调用 la_pltenter()。
LA_SYMB_NOPLTEXIT-不能针对此符号再次调用 la_pltexit()。
返回值表示在此调用后将控制权传递到的地址。监视符号绑定的审计库应该返回值 sym->st_value,以便将控制权传递给绑定符号定义。审计库可以通过返回不同的值对符号绑定进行专门重定向。
返回过程链接表中位于已经标记用于绑定通知的两个目标文件之间的一项时,会调用此函数。此函数在调用方获取控制权之前调用。
uintptr_t la_pltexit(Elf32_Sym * sym, uint_t ndx, uintptr_t * refcook, uintptr_t * defcook, uintptr_t retval); uintptr_t la_pltexit64(Elf64_Sym * sym, uint_t ndx, uintptr_t * refcook, uintptr_t * defcook, uintptr_t retval, const char * sym_name);
sym、ndx、refcook、defcook 和 sym_name 提供的信息与传递给 la_symbind() 的信息相同。retval 是绑定函数的返回代码。监视符号绑定的审计库应该返回 retval。审计库可以专门返回不同的值。
la_pltexit() 接口是实验接口。请参见审计接口限制。
此函数在执行目标文件的任何终止代码之后和卸载目标文件之前调用。
uint_t la_objclose(uintptr_t * cookie);
cookie 标识目标文件,并从先前的 la_objopen() 中获取。当前会忽略任何返回值。
以下简单示例创建了一个审计库,其中列显了动态可执行文件 date(1) 装入的每个共享库依赖项的名称。
$ cat audit.c #include <link.h> #include <stdio.h> uint_t la_version(uint_t version) { return (LAV_CURRENT); } uint_t la_objopen(Link_map * lmp, Lmid_t lmid, uintptr_t * cookie) { if (lmid == LM_ID_BASE) (void) printf("file: %s loaded\n", lmp->l_name); return (0); } $ cc -o audit.so.1 -G -K pic -z defs audit.c -lmapmalloc -lc $ LD_AUDIT=./audit.so.1 date file: date loaded file: /lib/libc.so.1 loaded file: /lib/libm.so.2 loaded file: /usr/lib/locale/en_US/en_US.so.2 loaded Thur Aug 10 17:03:55 PST 2000 |
/usr/demo/link_audit 下的 SUNWosdem 软件包中提供了许多使用 rtld-audit 接口的演示应用程序:
sotruss(1) 和 whocalls(1) 包括在 SUNWtoo 软件包中。perfcnt 和 symbindrep 是程序示例。这些应用程序不适用于生产环境。
使用 la_pltexit() 系列存在一些限制。这些限制是由于需要在调用方和被调用方之间插入额外栈帧,以提供 la_pltexit() 返回值。此要求在仅调用 la_pltenter() 例程时不会产生问题。在这种情况下,可以在将控制权转交给目标函数之前清除任何干预栈。
由于存在这些限制,因此应该将 la_pltexit() 视为实验接口。在不确定的情况下,请避免使用 la_pltexit() 例程。
有少量函数可以直接检查栈或对其状态做出假设。这些函数的一些示例包括 setjmp(3C) 系列、vfork(2) 以及返回结构而不是指向结构的指针的任何函数。为支持 la_pltexit() 而创建的额外栈会破坏这些函数。
由于运行时链接程序无法检测此类型的函数,因此,审计库创建者会负责针对此类例程禁用 la_pltexit()。
运行时链接程序可执行许多操作,包括将目标文件映射到内存中以及绑定符号。调试程序通常需要在分析应用程序的过程中访问说明这些运行时链接程序操作的信息。这些调试程序作为不同于其所分析的应用程序的进程运行。
本节介绍了用于监视和修改其他进程中的动态链接应用程序的 rtld-debugger 接口。此接口的体系结构采用 libc_db(3LIB) 中所使用的模型。
使用 rtld-debugger 接口时,至少涉及两个进程:
控制进程与 rtld-debugger 接口库链接,并使用该库来检查目标进程的动态方面。64 位控制进程可以调试 64 位目标和 32 位目标。但是,32 位控制进程只能调试 32 位目标。
当控制进程为调试器并且其目标为动态可执行文件时,最需要使用 rtld-debugger 接口。
rtld-debugger 接口可启用目标进程的以下活动:
与运行时链接程序初次会合。
通知装入和卸载动态库。
检索与任何装入的目标文件相关的信息。
跳过过程链接表项。
启用目标文件填充。
要检查和处理目标进程, rtld-debugger 接口需要使用导出接口、导入接口以及代理在这些接口之间进行通信。
控制进程与 librtld_db.so.1 所提供的 rtld-debugger 接口链接,并会请求从该库导出的接口。此接口在 /usr/include/rtld_db.h 中定义。与此相反,librtld_db.so.1 会请求从控制进程导入的接口。通过此交互,rtld-debugger 接口可以执行以下操作:
在目标进程中查找符号。
在目标进程中读写内存。
导入接口由许多 proc_service 例程组成,大多数调试器已经使用这些例程来分析进程。这些例程将在调试器导入接口中进行介绍。
rtld-debugger 接口假定请求 rtld-debugger 接口时会停止进程分析。如果未停止分析,则目标进程的运行时链接程序内的数据结构在检查时可能处于不一致状态。
下图中显示了 librtld_db.so.1、控制进程(调试器)和目标进程(动态可执行文件)之间的信息流程。
rtld-debugger 接口依赖于 proc_service 接口 /usr/include/proc_service.h,后者被视为实验接口。rtld-debugger 接口可能必须跟踪 proc_service 接口在发展中的变化。
/usr/demo/librtld_db 下的 SUNWosdem 软件包中提供了使用 rtld-debugger 接口的控制进程的实现样例。 此调试器 rdb 提供了使用 proc_service 导入接口的示例,并说明了所有 librtld_db.so.1 导出接口所需的调用顺序。以下各节介绍 rtld-debugger 接口。可以查看调试器样例,获取更多详细信息。
代理提供了可以描述内部接口结构的不透明处理方式,还提供了导出接口与导入接口之间的通信机制。rtld-debugger 接口旨在供可以同时处理多个进程的调试器使用,这些代理用于标识进程。
控制进程创建的不透明结构,用于标识在导出接口与导入接口之间传递的目标进程。
Is an opaque structure created by thertld-debugger 接口创建的不透明结构,用于标识在导出接口与导入接口之间传递的目标进程。
本节介绍 /usr/lib/librtld_db.so.1 审计库所导出的各种接口。可以将这些接口分为不同的功能组。
此函数可确定 rtld-debugger 的版本要求。基本 version 会定义为 RD_VERSION1。当前 version 始终由 RD_VERSION 定义。
rd_err_e rd_init(int version);
Solaris 8 10/00 发行版中添加的版本 RD_VERSION2 扩展了 rd_loadobj_t 结构。请参见扫描可装入目标文件中的 rl_flags、rl_bend 和 rl_dynamic 字段。
Solaris 8 01/01 发行版中添加的版本 RD_VERSION3 扩展了 rd_plt_info_t 结构。请参见跳过过程链接表中的 pi_baddr 和 pi_flags 字段。
如果控制进程要求的版本高于可用的 rtld-debugger 接口版本,则会返回 RD_NOCAPAB。
此函数可创建新的导出接口代理。
rd_agent_t * rd_new(struct ps_prochandle * php);
php 是控制进程所创建的 cookie,用于标识目标进程。此 cookie 供控制进程提供的导入接口用于维护上下文,并且对于 rtld-debugger 接口是不透明的。
此函数可基于为 rd_new() 提供的相同 ps_prochandle 结构重置代理内的信息。
rd_err_e rd_reset(struct rd_agent * rdap);
void rd_delete(struct rd_agent * rdap);
rtld-debugger 接口(在 rtld_db.h 中定义)可以返回以下错误状态:
typedef enum { RD_ERR, RD_OK, RD_NOCAPAB, RD_DBERR, RD_NOBASE, RD_NODYNAM, RD_NOMAPS } rd_err_e;
以下接口可用于收集错误信息。
char * rd_errstr(rd_err_e rderr);
void rd_log(const int onoff); |
启用日志记录时,会使用更多详细诊断信息来调用控制进程所提供的导入接口函数 ps_plog()。
可以获取运行时链接程序中维护的每个目标文件的信息。通过使用 rtld_db.h 中定义的以下结构,可实现链接映射:
typedef struct rd_loadobj { psaddr_t rl_nameaddr; unsigned rl_flags; psaddr_t rl_base; psaddr_t rl_data_base; unsigned rl_lmident; psaddr_t rl_refnameaddr; psaddr_t rl_plt_base; unsigned rl_plt_size; psaddr_t rl_bend; psaddr_t rl_padstart; psaddr_t rl_padend; psaddt_t rl_dynamic; } rd_loadobj_t;
请注意,在此结构中提供的所有地址(包括字符串指针)都是目标进程中的地址,而不是控制进程本身的地址空间中的地址。
指向包含动态库名称的字符串的指针。
在修订版 RD_VERSION2 中,使用 RD_FLG_MEM_OBJECT 标识动态装入的可重定位目标文件。
动态库的基本地址。
动态库数据段的基本地址。
链接映射标识符(请参见建立名称空间)。
如果动态库是标准过滤器,则指向 filtee 的名称。
提供这些元素是为了向下兼容,当前未使用。
目标文件的结束地址 (text + data + bss)。在修订版 RD_VERSION2 中,动态装入的可重定位目标文件将导致此元素指向创建的目标文件(包括其节标题)的结尾。
动态库之前填充的基本地址(请参阅动态库填充)。
动态库之后填充的基本地址(请参阅动态库填充)。
添加了 RD_VERSION2 的此字段可提供目标文件动态节的基本地址,从而可允许引用 DT_CHECKSUM 之类的项(请参见表 7–32)。
rd_loadobj_iter() 例程使用此目标文件数据结构来访问运行时链接程序的链接映射列表中的信息:
会对当前在目标进程中装入的所有动态库重复执行此函数。
typedef int rl_iter_f(const rd_loadobj_t *, void *); rd_err_e rd_loadobj_iter(rd_agent_t * rap, rl_iter_f * cb, void * clnt_data);
每次重复时都会调用 cb 指定的导入函数。可以使用 clnt_data 将数据传递给 cb 调用。通过指向可变(已分配的栈)rd_loadobj_t 结构的指针可返回有关每个目标文件的信息。
cb 例程中的返回代码通过 rd_loadobj_iter() 进行检查,并具有以下含义:
1-继续处理链接映射。
0-停止处理链接映射并将控制权返回给控制进程。
rd_loadobj_iter() 运行成功时会返回 RD_OK。返回 RD_NOMAPS 表示运行时链接程序尚未装入初始链接映射。
控制进程可以跟踪运行时链接程序范围内发生的特定事件。这些事件包括:
运行时链接程序已经装入并重定位所有动态库,并且即将开始调用每个装入的目标文件的 .init 节。
运行时链接程序已经完成调用所有的 .init 节,并且即将会将控制权转交给主可执行文件。
已经调用运行时链接程序来装入或卸载动态库。
可以使用 sys/link.h 和 rtld_db.h 中定义的以下接口来监视这些事件:
typedef enum { RD_NONE = 0, RD_PREINIT, RD_POSTINIT, RD_DLACTIVITY } rd_event_e; /* * Ways that the event notification can take place: */ typedef enum { RD_NOTIFY_BPT, RD_NOTIFY_AUTOBPT, RD_NOTIFY_SYSCALL } rd_notify_e; /* * Information on ways that the event notification can take place: */ typedef struct rd_notify { rd_notify_e type; union { psaddr_t bptaddr; long syscallno; } u; } rd_notify_t;
以下函数可跟踪事件:
rd_err_e rd_event_enable(struct rd_agent * rdap, int onoff);
目前,由于性能原因,运行时链接程序会忽略事件禁用。控制进程应假定可以访问指定的断点,因为最后调用了此例程。
rd_err_e rd_event_addr(rd_agent_t * rdap, rd_event_e event, rd_notify_t * notify);
根据事件类型,通过调用 notify->u.syscallno 标识的运行正常的低成本系统调用或者在 notify->u.bptaddr 指定的地址执行断点可实现控制进程通知。控制进程负责跟踪系统调用或定位实际断点。
事件发生后,可以通过 rtld_db.h 中定义的此接口获取其他信息:
typedef enum { RD_NOSTATE = 0, RD_CONSISTENT, RD_ADD, RD_DELETE } rd_state_e; typedef struct rd_event_msg { rd_event_e type; union { rd_state_e state; } u; } rd_event_msg_t;
rd_state_e 值包括:
没有其他可用的状态信息。
链接映射处于稳定状态,可以对其进行检查。
正在装入动态库,链接映射未处于稳定状态。应该在达到 RD_CONSISTANT 状态之后再检查这些链接映射。
正在删除动态库,链接映射未处于稳定状态。应该在达到 RD_CONSISTANT 状态之后再检查这些链接映射。
rd_event_getmsg() 函数用于获取此事件状态信息。
下表显示了各种不同事件类型的可能状态。
RD_PREINIT |
RD_POSTINIT |
RD_DLACTIVITY |
---|---|---|
RD_NOSTATE |
RD_NOSTATE |
RD_CONSISTANT |
|
|
RD_ADD |
|
|
RD_DELETE |
通过使用 rtld-debugger 接口,控制进程可以跳过过程链接表项。第一次要求控制进程(如调试器)步入 (step into) 函数时,通过过程链接表处理可将控制权传递给运行时链接程序以搜索函数定义。
通过使用以下接口,控制进程可以跳过运行时链接程序的过程链接表处理。控制进程可以基于 ELF 文件中提供的外部信息来确定何时遇到过程链接表项。
目标进程步入过程链接表项之后,便会调用 rd_plt_resolution() 接口:
此函数可返回当前过程链接表项的解析状态以及有关如何跳过此状态的信息。
rd_err_e rd_plt_resolution(rd_agent_t * rdap, paddr_t pc, lwpid_t lwpid, paddr_t plt_base, rd_plt_info_t * rpi); |
pc 表示过程链接表项的第一条指令。lwpid 提供 lwp 标识符,plt_base 提供过程链接表的基本地址。这三个变量提供的信息足以供多个体系结构用于处理过程链接表。
rpi 提供有关以下数据结构(在 rtld_db.h 中定义)中定义的过程链接表项的详细信息:
typedef enum { RD_RESOLVE_NONE, RD_RESOLVE_STEP, RD_RESOLVE_TARGET, RD_RESOLVE_TARGET_STEP } rd_skip_e; typedef struct rd_plt_info { rd_skip_e pi_skip_method; long pi_nstep; psaddr_t pi_target; psaddr_t pi_baddr; unsigned int pi_flags; } rd_plt_info_t; #define RD_FLG_PI_PLTBOUND 0x0001
rd_plt_info_t 结构的元素包括:
标识遍历过程链接表项的方法。此方法可设置为 rd_skip_e 值之一。
标识返回 RD_RESOLVE_STEP 或 RD_RESOLVE_TARGET_STEP 时跳过的指令数。
指定返回 RD_RESOLVE_TARGET_STEP 或 RD_RESOLVE_TARGET 时设置断点的地址。
添加了 RD_VERSION3 的过程链接表的目标地址。设置 pi_flags 字段的 RD_FLG_PI_PLTBOUND 标志之后,此元素可标识已解析(绑定)的目标地址。
添加了 RD_VERSION3 的标志字段。标志 RD_FLG_PI_PLTBOUND 可将过程链接项标识为已解析(绑定)到其目标地址,此地址可用于 pi_baddr 字段。
rd_plt_info_t 返回值表明了以下可能的情况:
必须由运行时链接程序解析通过此过程链接表进行的首次调用。在这种情况下,rd_plt_info_t 包含以下内容:
{RD_RESOLVE_TARGET_STEP, M, <BREAK>, 0, 0} |
控制进程会在 BREAK 处设置断点,从而使目标进程继续运行。到达断点时,即会完成过程链接表项处理。然后,控制进程可以将 M 条指令转到目标函数。请注意,由于这是通过过程链接表项进行的首次调用,因此尚未设置绑定地址 (pi_baddr)。
通过此过程链接表第 N 次进行调用时,rd_plt_info_t 会包含以下内容:
{RD_RESOLVE_STEP, M, 0, <BoundAddr>, RD_FLG_PI_PLTBOUND} |
过程链接表项已经过解析,并且控制进程可以将 M 条指令转到目标函数。过程链接表项绑定到的地址为 <BoundAddr>,并且已在标志字段中设置了 RD_FLG_PI_PLTBOUND 位。
运行时链接程序的缺省行为取决于要装入动态库的操作系统(可以在其中最有效地引用这些目标文件)。如果能够对装入目标进程内存的目标文件执行填充,有些控制进程会从中受益。控制进程可以使用此接口请求此填充。
此函数可启用或禁用对目标进程的任何随后装入的目标文件的填充。可以在装入目标文件的两端进行填充。
rd_err_e rd_objpad_enable(struct rd_agent * rdap, size_t padsize);
padsize 指定将任何目标文件装入内存前后要保留的填充大小(以字节为单位)。使用具有 PROT_NONE 权限和 MAP_NORESERVE 标志的 mmap(2) 可将填充保留为内存映射。实际上,运行时链接程序可保留与任何装入目标文件相邻的目标进程虚拟地址空间区域。控制进程随后可以利用这些空间区域。
如果 padsize 为 0,则对于后续目标文件将禁用目标文件填充。
通过使用 proc(1) 工具并引用 rd_loadobj_t 中提供的链接映射信息,可报告使用 mmap(2) 从具有 MAP_NORESERVE 标志的 /dev/zero 中获取的预留空间。
控制进程必须提供给 librtld_db.so.1 的导入接口在 /usr/include/proc_service.h 中定义。可以在 rdb 演示调试器中找到这些 proc_service 函数的实现样例。rtld-debugger 接口仅使用一部分可用的 proc_service 接口。将来版本的 rtld-debugger 接口可能会利用其他 proc_service 接口,而不会创建不兼容的更改。
当前 rtld-debugger 接口会使用以下接口:
此函数可返回指向 auxv 向量副本的指针。
ps_err_e ps_pauxv(const struct ps_prochandle * ph, auxv_t ** aux);
此函数可从目标进程中读取数据。
ps_err_e ps_pread(const struct ps_prochandle * ph, paddr_t addr, char * buf, int size); |
此函数可将数据写入目标进程。
ps_err_e ps_pwrite(const struct ps_prochandle * ph, paddr_t addr, char * buf, int size);
此函数通过 rtld-debugger 接口中的其他诊断信息调用。
void ps_plog(const char * fmt, ...);
控制进程会确定在何处或者是否记录此诊断信息。ps_plog() 的参数采用 printf(3C) 格式。
此函数可在目标进程中搜索符号。
ps_err_e ps_pglobal_lookup(const struct ps_prochandle * ph, const char * obj, const char * name, ulong_t * sym_addr);
在目标进程 ph 的名为 obj 的目标文件内搜索名为 name 的符号。如果找到此符号,则将符号地址存储在 sym_addr 中。
此函数可在目标进程中搜索符号。
ps_err_e ps_pglobal_sym(const struct ps_prochandle * ph, const char * obj, const char * name, ps_sym_t * sym_desc);
在目标进程 ph 的名为 obj 的目标文件内搜索名为 name 的符号。如果找到此符号,则将符号描述符存储在 sym_desc 中。
如果在创建任何链接映射之前, rtld-debugger 接口需要在应用程序或运行时链接程序内查找符号,则可以使用 obj 的以下保留值:
#define PS_OBJ_EXEC ((const char *)0x0) /* application id */ #define PS_OBJ_LDSO ((const char *)0x1) /* runtime linker id */
控制进程可以使用以下伪代码将 procfs 文件系统用于这些目标文件:
ioctl(.., PIOCNAUXV, ...) - obtain AUX vectors ldsoaddr = auxv[AT_BASE]; ldsofd = ioctl(..., PIOCOPENM, &ldsoaddr); /* process elf information found in ldsofd ... */ execfd = ioctl(.., PIOCOPENM, 0); /* process elf information found in execfd ... */
找到文件描述符之后,控制程序即可检查 ELF 文件来查找其符号信息。