当已装入到进程中的不同动态目标文件中存在同一符号的多个同名实例时可能会发生插入。在缺省搜索模型下,符号引用被绑定到在已装入的依赖项系列中发现的第一个定义。这第一个符号被称为在同名的其他符号上的插入。
直接绑定可以禁用任何隐式插入。因为将在与引用关联的依赖项中搜索直接绑定的引用,所以会跳过启用了插入的缺省符号搜索模型。在直接绑定环境中,可以建立到具有相同名称的同一符号的不同定义的绑定。
能够绑定到具有相同名称的同一符号的不同定义是直接绑定的一个非常有用的功能。然而,如果某个应用程序依赖于一个插入实例,则使用直接绑定可能会破坏应用程序的预期执行。在决定为现有应用程序使用直接绑定之前,应当对应用程序进行分析以确定是否存在插入。
要确定应用程序内是否可能存在插入,请使用 lari(1)。缺省情况下,lari 输出受关注的信息。该信息源自同一符号定义的多个实例,这些实例可能会依次导致插入。
只有在绑定到符号的一个实例时才会发生插入。插入中可能不会涉及 lari 收集的同一符号的多个实例。其他多个实例符号可以存在,但可能不会被引用。这些未引用的符号仍然用作插入的候选者,因为将来的代码开发可能会引用这些符号。在考虑使用直接绑定时,应当对多次定义的符号的所有实例进行分析。
如果存在同一符号的多个同名实例,特别是已观察到插入时,应当执行下列操作之一。
对符号实例进行本地化以消除名称空间冲突。
删除多个实例以便只留下一个符号定义。
显式定义任何插入需求。
识别可以作为插入基础的符号以阻止直接绑定到该符号。
以下几节更详细地讨论了这些操作。
应当对提供不同实现的已多次定义的同名符号进行隔离以避免意外的插入。从目标文件导出的接口中删除符号的最简单方法是将符号降级为局部符号。可以通过将符号定义为 "static" 或使用编译器提供的符号属性将符号降级为局部符号。
还可以通过使用链接编辑器和 mapfile 将符号降级为局部符号。以下示例显示了一个 mapfile,它通过使用 local 作用域指令将全局函数 error() 降级为一个局部符号。
$ cc -o A.so.1 -G -Kpic error.c a.c b.c ... $ elfdump -sN.symtab A.so.1 | fgrep error [36] 0x000002d0 0x00000014 FUNC GLOB D 0 .text error $ cat mapfile $mapfile_version 2 SYMBOL_SCOPE { local: error; }; $ cc -o A.so.2 -G -Kpic -M mapfile error.c a.c b.c ... $ elfdump -sN.symtab A.so.2 | fgrep error [24] 0x000002c8 0x00000014 FUNC LOCL H 0 .text error
虽然可以通过使用显式的 mapfile 定义将各个符号降级为局部符号,但建议通过符号版本控制来定义整个接口系列。请参见第 5 章。
版本控制是一项通常用于标识从共享目标文件导出的接口的有用技术。类似地,可以对动态可执行文件进行版本控制以定义其导出的接口。动态可执行文件只需要导出必须可供要绑定到的目标文件的依赖项使用的接口。通常,您添加到动态可执行文件的代码不需要导出接口。
从动态可执行文件删除导出的接口时应考虑已由编译器驱动程序建立的任何符号定义。这些定义源自编译器驱动程序添加到最终链接编辑的辅助文件。请参见使用编译器驱动程序。
以下示例 mapfile 导出了编译器驱动程序可以建立的一组常见的符号定义,同时将所有其他全局定义降级为局部定义。
$ cat mapfile $mapfile_version 2 SYMBOL_SCOPE { global: __Argv; __environ_lock; _environ; _lib_version; environ; local: *; };
您应当决定您的编译器驱动程序建立的符号定义。在动态可执行文件内使用的这些定义中的任何一个都应保留为全局定义。
通过从动态可执行文件删除任何导出的接口,可以防止可执行文件出现比目标文件依赖项进化时更多的插入问题。
如果由与符号关联的实现维护自身状态,则在直接绑定环境内多次定义的同名符号可能会出现问题。从这一点来说,数据符号通常是违规者,然而维护自身状态的函数也可能会出现问题。
在直接绑定环境中,可以绑定到同一符号的多个实例。因此,不同的绑定实例可以在一个进程内操纵原本打算成为单个实例的不同状态变量。
例如,假设两个共享目标文件包含相同的数据项 errval。另外,假设两个函数 action() 和 inspect() 存在于不同的共享目标文件中。这些函数预期分别读取和写入 errval 值。
使用缺省搜索模型时,errval 的一个定义将插入到另一个定义上。函数 action() 和 inspect() 都将绑定到 errval 的同一实例。因此,如果 action() 向 errval 写入了一个错误代码,则 inspect() 可以读取和处理该错误状态。
但是,假设包含 action() 和 inspect() 的目标文件分别被绑定到了各自定义了 errval 的不同依赖项。在直接绑定环境内,这些函数将被绑定到 errval 的不同定义。action() 可以向 errval 的一个实例写入错误代码,而 inspect() 读取 errval 的另一个未初始化的定义。结果是,inspect() 未检测到要处理的错误状态。
如果符号是在标头中声明的,则通常会出现数据符号的多个实例。
int bar;
该数据声明会导致包含该标头的每个编译单元生成一个数据项。此暂定结果数据项会导致在不同的动态目标文件中定义符号的多个实例。
但是,通过将数据项显式定义为外部数据项,可以为包含该标头的每个编译单元生成对数据项的引用。
extern int bar;
然后,在运行时可以将这些引用解析为一个数据实例。
有时候,应当保留您要删除的符号实现的接口。同一接口的多个实例可以向量化到一个实现,同时保留任何现有接口。通过使用 FILTER mapfile 关键字创建单个符号过滤器,可以实现此模型。SYMBOL_SCOPE / SYMBOL_VERSION 指令中描述了此关键字。
当依赖项期望在已删除了某个符号实现的目标文件中找到该符号时,创建单个符号过滤器很有用。
例如,假设函数 error() 存在于两个共享目标文件 A.so.1 和 B.so.1 中。为消除符号重复,您希望从 A.so.1 中删除该实现。不过,其他依赖项正依赖于从 A.so.1 提供的 error()。以下示例显示了 A.so.1 中 error() 的定义。然后,使用一个 mapfile 来允许删除 error() 实现,同时为定向到 B.so.1 的该符号保留一个过滤器。
$ cc -o A.so.1 -G -Kpic error.c a.c b.c ... $ elfdump -sN.dynsym A.so.1 | fgrep error [3] 0x00000300 0x00000014 FUNC GLOB D 0 .text error $ cat mapfile $mapfile_version 2 SYMBOL_SCOPE { global: error { TYPE=FUNCTION; FILTER=B.so.1 }; }; $ cc -o A.so.2 -G -Kpic -M mapfile a.c b.c ... $ elfdump -sN.dynsym A.so.2 | fgrep error [3] 0x00000000 0x00000000 FUNC GLOB D 0 ABS error $ elfdump -y A.so.2 | fgrep error [3] F [0] B.so.1 error
error() 函数是全局的,并保留 A.so.2 的一个导出接口。但是,到该符号的任何运行时绑定都将向量化到 filtee B.so.1。字母 "F" 表示该符号是过滤器。
该模型在向量化到一个实现时保留现有接口,多个 Oracle Solaris 库中均使用该模型。例如,曾经在 libc.so.1 中定义的许多数学接口现已向量化到 libm.so.2 中函数的首选实现。
缺省搜索模型可能会导致同名符号的实例插入到同名的后续实例上。即使没有任何显式标签,仍然会发生插入,以便从所有引用绑定到同一个符号定义。发生该隐式插入是符号搜索的结果,而不是因为向运行时链接程序发出了任何显式指令。使用直接绑定可以禁用该隐式插入。
虽然直接绑定能够将符号引用直接解析到关联的符号定义,但是显式插入是在任何直接绑定搜索之前处理的。因此,即使是在直接绑定环境内,也可以对插入项进行设计并预期它在任意直接绑定关联上进行插入。可以使用下列技术显式定义插入项。
LD_PRELOAD 环境变量的插入功能和 -z interpose 选项已经提供了一段时间。请参见运行时插入。因为这些目标文件是显式定义为插入项的,因此运行时链接程序将在处理任何直接绑定之前检查这些目标文件。
为一个共享目标文件建立的插入将应用于该动态目标文件的所有接口。在使用 LD_PRELOAD 环境变量装入目标文件时,会建立该目标文件插入。在装入已使用 -z interpose 选项生成的目标文件时,也会建立目标文件插入。当使用具有特殊句柄 RTLD_NEXT 的 dlsym(3C) 等技术时,该目标文件模型很重要。插入目标文件应当始终具有下一个目标文件的一致视图。
动态可执行文件具有额外的灵活性,因为该可执行文件可以使用 INTERPOSE mapfile 关键字定义单个插入符号。因为动态可执行文件是进程中装入的第一个目标文件,所以可执行文件的下一个目标文件视图始终是一致的。
以下示例显示了要显式插入到 exit() 函数的应用程序。
$ cat mapfile $mapfile_version 2 SYMBOL_SCOPE { global: exit { FLAGS = INTERPOSE }; }; $ cc -o prog -M mapfile exit.c a.c b.c ... $ elfdump -y prog | fgrep exit [6] DI <self> exit
字母 "I" 表示该符号是插入符号。实现此 exit() 函数可以直接引用系统函数 _exit(),也可以使用带有 RTLD_NEXT 句柄的 dlsym() 调用系统函数 exit()。
最初,您可能考虑使用 -z interpose 选项识别此目标文件。但是,此技术的开销相当大,因为应用程序导出的所有接口都将用作插入项。较好的替代方法是,结合使用 -z interpose 选项将应用程序提供的所有符号(插入项除外)本地化。
但是,使用 INTERPOSE mapfile 关键字会提供更大的灵活性。通过使用此关键字,应用程序可以导出多个接口,同时选择应当用作插入项的接口。
指定了 STV_SINGLETON 可见性的符号可以有效地提供一种插入形式。请参见表 7-20。编译系统可将这些符号指定给一个实现,该实现可能会在进程内的多个目标文件中多次实例化。所有的单件符号引用都绑定到进程中第一次出现的单件符号。