共享库是一种由链接编辑器创建并通过指定 -G 选项生成的输出形式。在以下示例中,共享库 libfoo.so.1 根据输入文件 foo.c 生成。
$ cc -o libfoo.so.1 -G -K pic foo.c
共享库是一个不可分割的单元,通过一个或多个可重定位的目标文件生成。共享库可以与动态可执行文件绑定在一起以形成可运行进程。顾名思义,共享库可供多个应用程序共享。由于这种潜在的深远影响,因此与先前章节相比,本章更深入地介绍了这种链接编辑器输出形式。
对于要绑定到动态可执行文件或其他共享库的共享库,首先它必须可用于链接编辑所需的输出文件。在此链接编辑过程中,会解释所有输入共享库,就像已将这些共享库添加到要生成的输出文件的逻辑地址空间。共享库的所有功能均可用于输出文件。
所有输入共享库都变成此输出文件的依赖项。此输出文件中维护了少量簿记信息以描述这些依赖项。运行时链接程序将在创建可运行进程的过程中解释此信息并完成这些共享库的处理。
以下各节详述了如何在编译环境和运行时环境中使用共享库。这些环境在运行时链接中介绍。
链接编辑器和运行时链接程序都不根据文件名解释文件。将检查所有文件以确定其 ELF 类型(请参见ELF 头)。链接编辑器使用此信息来推导文件的处理要求。但是,共享库通常遵循两种命名约定之一,具体取决于这些目标文件是用作编译环境的一部分还是用作运行时环境的一部分。
当共享库用作编译环境的一部分时,链接编辑器将读取和处理这些共享库。虽然可以在传递到链接编辑器的命令中根据显式文件名指定这些共享库,但是通常使用 -l 选项来利用链接编辑器的库搜索功能。 请参见共享库处理。
应该使用前缀 lib 和后缀 .so 来指定适用于此链接编辑器处理的共享库。例如,/lib/libc.so 便是可用于编译环境的标准 C 库的共享库。根据约定,64 位共享库位于 lib 目录名为 64 的子目录中。例如,/lib/libc.so.1 的对应 64 位名称是 /lib/64/libc.so.1。
当共享库用作运行时环境的一部分时,运行时链接程序将读取和处理这些共享库。要允许在一系列软件发行版中对共享库的导出接口进行更改,请将共享库作为版本化文件名提供。
版本化文件名通常采用 .so 后缀后跟版本号的形式。例如,/lib/libc.so.1 便是可用于运行时环境的第一版标准 C 库的共享库。
如果从不打算在编译环境中使用共享库,则可能会从共享库名称中删除常规的 lib 前缀。仅用于 dlopen(3C) 的共享库便是此类共享库。仍然建议使用后缀 .so 来表明实际文件类型。此外,强烈建议使用版本号以便在一系列软件发行版中提供正确的共享库绑定。第 5 章,应用程序二进制接口与版本控制更详细地介绍了版本控制。
dlopen(3C) 中使用的共享库名称通常表示为不包含 "/" 的简单文件名。然后,运行时链接程序可以使用一组规则来查找实际文件。 有关更多详细信息,请参见装入其他目标文件。
缺省情况下,动态可执行文件或共享库中的依赖项记录将是链接编辑器所引用的关联共享库的文件名。例如,根据同一共享库 libfoo.so 生成的以下动态可执行文件会导致对同一依赖项具有不同的解释。
$ cc -o ../tmp/libfoo.so -G foo.o $ cc -o prog main.o -L../tmp -lfoo $ dump -Lv prog | grep NEEDED [1] NEEDED libfoo.so $ cc -o prog main.o ../tmp/libfoo.so $ dump -Lv prog | grep NEEDED [1] NEEDED ../tmp/libfoo.so $ cc -o prog main.o /usr/tmp/libfoo.so $ dump -Lv prog | grep NEEDED [1] NEEDED /usr/tmp/libfoo.so |
如这些示例所示,这种记录依赖项机制会因编译技术的不同而出现不一致性。此外,在链接编辑过程中引用的共享库的位置也可能会与已安装系统上的共享库的最终位置有所不同。为了提供更一致的指定依赖项的方法,共享库可以在自身中记录运行时引用共享库应依据的文件名。
在链接编辑共享库过程中,可以使用 -h 选项在共享库自身中记录其运行时名称。在以下示例中,将在文件自身中记录共享库的运行时名称 libfoo.so.1。此标识称为 soname。
$ cc -o ../tmp/libfoo.so -G -K pic -h libfoo.so.1 foo.c |
以下示例说明如何使用 dump(1) 并引用具有 SONAME 标记的项来显示 soname 记录。
$ dump -Lvp ../tmp/libfoo.so ../tmp/libfoo.so: [INDEX] Tag Value [1] SONAME libfoo.so.1 ......... |
当链接编辑器处理包含 soname 的共享库时,此名称便是作为要生成的输出文件中的依赖项记录的名称。
如果在上一示例的创建动态可执行文件 prog 的过程中使用此新版本的 libfoo.so,则所有三种创建可执行文件的方法都会记录同一依赖项。
$ cc -o prog main.o -L../tmp -lfoo $ dump -Lv prog | grep NEEDED [1] NEEDED libfoo.so.1 $ cc -o prog main.o ../tmp/libfoo.so $ dump -Lv prog | grep NEEDED [1] NEEDED libfoo.so.1 $ cc -o prog main.o /usr/tmp/libfoo.so $ dump -Lv prog | grep NEEDED [1] NEEDED libfoo.so.1 |
在上述示例中,使用 -h 选项来指定不包含 "/" 的简单文件名。借助此约定,运行时链接程序可以使用一组规则来查找实际文件。 有关更多详细信息,请参见查找共享库依赖项。
如果共享库始终通过归档库进行处理,则在共享库中记录 soname 是基本机制。
归档文件可以根据一个或多个共享库生成,并可用于生成动态可执行文件或共享库。可以从归档文件中提取共享库来满足链接编辑的要求。与处理可重定位的目标文件(串联成要创建的输出文件)不同,从归档文件中提取的任何共享库都将记录为依赖项。有关归档文件提取条件的更多详细信息,请参见归档处理。
归档成员的名称由链接编辑器构造,并且由归档文件名称和归档文件中的目标文件串联而成。 例如:
$ cc -o libfoo.so.1 -G -K pic foo.c $ ar -r libfoo.a libfoo.so.1 $ cc -o main main.o libfoo.a $ dump -Lv main | grep NEEDED [1] NEEDED libfoo.a(libfoo.so.1) |
由于具有此串联名称的文件无法在运行时存在,因此,在共享库中提供 soname 是生成依赖项有意义运行时文件名的唯一方法。
运行时链接程序不会从归档文件中提取目标文件。因此,在上一示例中,必须从归档文件中提取所需的共享库依赖项并使其可用于运行时环境。
使用共享库创建动态可执行文件或其他共享库时,链接编辑器会执行多项一致性检查。这些检查可确保输出文件中记录的任何依赖项名称都是唯一的。
如果用作链接编辑的输入文件的两个共享库包含相同的 soname,则会出现依赖项名称冲突。 例如:
$ cc -o libfoo.so -G -K pic -h libsame.so.1 foo.c $ cc -o libbar.so -G -K pic -h libsame.so.1 bar.c $ cc -o prog main.o -L. -lfoo -lbar ld: fatal: recording name conflict: file `./libfoo.so' and \ file `./libbar.so' provide identical dependency names: libsame.so.1 ld: fatal: File processing errors. No output written to prog |
如果某个没有已记录 soname 的共享库的文件名与同一链接编辑过程使用的其他共享库的 soname 匹配,则也会出现类似的错误情况。
如果要生成的共享库的运行时名称与它的某个依赖项匹配,则链接编辑器也会报告名称冲突。
$ cc -o libbar.so -G -K pic -h libsame.so.1 bar.c -L. -lfoo ld: fatal: recording name conflict: file `./libfoo.so' and \ -h option provide identical dependency names: libsame.so.1 ld: fatal: File processing errors. No output written to libbar.so |
共享库可以具有自己的依赖项。运行时链接程序查找共享库依赖项使用的搜索规则在运行时链接程序搜索的目录中介绍。如果共享库没有位于其中一个缺省搜索目录中,则必须将查找位置明确告知运行时链接程序。 对于 32 位目标文件,缺省搜索目录为 /lib 和 /usr/lib。对于 64 位目标文件,缺省搜索目录为 /lib/64 和 /usr/lib/64。指明非缺省搜索路径要求的首选机制是在具有依赖项的目标文件中记录运行路径。可以使用链接编辑器的 -R 选项来记录运行路径。
在以下示例中,共享库 libfoo.so 依赖于 libbar.so,而在运行时预期后者位于目录 /home/me/lib 中,如果在该目录中未找到该项,则该项位于缺省位置中。
$ cc -o libbar.so -G -K pic bar.c $ cc -o libfoo.so -G -K pic foo.c -R/home/me/lib -L. -lbar $ dump -Lv libfoo.so libfoo.so: **** DYNAMIC SECTION INFORMATION **** .dynamic: [INDEX] Tag Value [1] NEEDED libbar.so [2] RUNPATH /home/me/lib ......... |
共享库负责指定查找其依赖项所需的所有运行路径。所有在动态可执行文件中指定的运行路径只用于查找动态可执行文件的依赖项。不能使用这些运行路径来查找共享库的任何依赖项。
环境变量 LD_LIBRARY_PATH 的范围更具全局性。运行时链接程序使用借助此变量指定的所有路径名来搜索任意共享库依赖项。虽然此环境变量可用作影响运行时链接程序搜索路径的临时机制,但是强烈建议不要在生产软件中使用此变量。有关更全面的介绍,请参见运行时链接程序搜索的目录。
当动态可执行文件和共享库都依赖于相同的常用共享库时,这些目标文件的处理顺序可能会变得有些难以预测。
例如,假设共享库开发者生成的 libfoo.so.1 具有以下依赖项:
$ ldd libfoo.so.1 libA.so.1 => ./libA.so.1 libB.so.1 => ./libB.so.1 libC.so.1 => ./libC.so.1 |
如果使用此共享库创建动态可执行文件 prog,并定义显式依赖 libC.so.1,则生成的共享库顺序如下:
$ cc -o prog main.c -R. -L. -lC -lfoo $ ldd prog libC.so.1 => ./libC.so.1 libfoo.so.1 => ./libfoo.so.1 libA.so.1 => ./libA.so.1 libB.so.1 => ./libB.so.1 |
动态可执行文件 prog 的构造将会影响对共享库 libfoo.so.1 依赖项处理顺序的任何要求。
专门致力于符号插入和 .init 节处理的开发者应意识到共享库处理顺序中存在这种潜在的更改。
可以定义共享库以将其用作过滤器。此技术涉及将过滤器提供的接口与备用共享库进行关联。在运行时,此备用共享库可提供过滤器所提供的一个或多个接口。此备用共享库称为 filtee。filtee 的生成方式与任意共享库的生成方式相同。
过滤提供了一种从运行时环境中提取编译环境的机制。在链接编辑时,将绑定到过滤器接口的符号引用解析为过滤器符号定义。在运行时,可以将绑定到过滤器接口的符号引用重定向到备用共享库。
使用 mapfile 指令 FILTER 或 AUXILIARY,可以将在共享库中定义的单个接口定义为过滤器。或者,共享库可以使用链接编辑器的 -F 或 -f 标志将共享库提供的所有接口定义为过滤器。通常单独使用这些技术。请参见生成标准过滤器和生成辅助过滤器。还可以在同一共享库中组合使用这些技术。 请参见过滤组合。
存在两种过滤形式。
此过滤只需要一个用于要过滤的接口的符号表项。在运行时,必须通过 filtee 实现过滤器符号定义。
使用链接编辑器的 mapfile 指令 FILTER 或链接编辑器的 -F 标志,可以定义接口以将其用作标准过滤器。此 mapfile 指令或标志使用必须在运行时提供符号定义的一个或多个 filtee 的名称进行限定。
将跳过在运行时无法处理的 filtee。如果在 filtee 中找不到标准过滤器符号,则也将导致跳过此 filtee。在这两种情况下,无法使用过滤器提供的符号定义来实现此符号查找。
此过滤提供类似于标准过滤的机制,而且该过滤器还提供一种对应于辅助过滤器接口的回退实现。在运行时,可以通过 filtee 实现符号定义。
使用链接编辑器的 mapfile 指令 AUXILIARY 或链接编辑器的 -f 标志,可以定义接口以将其用作辅助过滤器。此 mapfile 指令或标志使用在运行时提供符号定义的一个或多个 filtee 的名称进行限定。
将跳过在运行时无法处理的 filtee。如果在 filtee 中找不到辅助过滤器符号,则也将导致跳过此 filtee。在这两种情况下,无法使用过滤器提供的符号定义来实现此符号查找。
要生成标准过滤器,应先定义要应用过滤的 filtee。以下示例将生成 filtee filtee.so.1,并提供符号 foo 和 bar。
$ cat filtee.c char * bar = "defined in filtee"; char * foo() { return("defined in filtee"); } $ cc -o filtee.so.1 -G -K pic filtee.c |
可以通过以下两种方法之一提供标准过滤。要将共享库提供的所有接口都声明为过滤器,请使用链接编辑器的 -F 标志。要将共享库的单个接口声明为过滤器,请使用链接编辑器 mapfile 和 FILTER 指令。
在以下示例中,将共享库 filter.so.1 定义为过滤器。filter.so.1 提供符号 foo 和 bar,并且是 filtee filtee.so.1 的过滤器。在本示例中,使用环境变量 LD_OPTIONS 禁止编译器驱动程序解释 -F 选项。
$ cat filter.c char * bar = 0; char * foo() { return (0); } $ LD_OPTIONS='-F filtee.so.1' \ cc -o filter.so.1 -G -K pic -h filter.so.1 -R. filter.c $ elfdump -d filter.so.1 | egrep "SONAME|FILTER" [2] SONAME 0xee filter.so.1 [3] FILTER 0xfb filtee.so.1 |
创建动态可执行文件或共享库时,链接编辑器可将标准过滤器 filter.so.1 引用为依赖项。链接编辑器使用此过滤器符号表中的信息来实现任何符号解析。但是,在运行时,对该过滤器符号的任何引用都会导致对此 filtee filtee.so.1 的额外装入。运行时链接程序使用此 filtee 来解析 filter.so.1 定义的所有符号。如果未找到此 filtee,或者在此 filtee 中未找到过滤器符号,则查找该符号时会跳过此过滤器。
例如,以下动态可执行文件 prog 引用符号 foo 和 bar,这两个符号在链接编辑过程中通过过滤器 filter.so.1 进行解析。执行 prog 会导致从 filtee filtee.so.1 中获取 foo 和 bar,而不是从过滤器 filter.so.1 中获取。
$ cat main.c extern char * bar, * foo(); main() { (void) printf("foo is %s: bar is %s\n", foo(), bar); } $ cc -o prog main.c -R. filter.so.1 $ prog foo is defined in filtee: bar is defined in filtee |
在以下示例中,共享库 filter.so.2 将它的一个接口 foo 定义为 filtee filtee.so.1 的过滤器。
由于未提供 foo() 的源代码,因此,使用 mapfile 指令 FUNCTION 以确保创建 foo 的符号表项。
$ cat filter.c char * bar = "defined in filter"; $ cat mapfile { global: foo = FUNCTION FILTER filtee.so.1; }; $ cc -o filter.so.2 -G -K pic -h filter.so.2 -M mapfile -R. filter.c $ elfdump -d filter.so.2 | egrep "SONAME|FILTER" [2] SONAME 0xd8 filter.so.2 [3] SUNW_FILTER 0xfb filtee.so.1 $ elfdump -y filter.so.2 | egrep "foo|bar" [1] F [3] filtee.so.1 foo [10] D <self> bar |
在运行时,对过滤器符号 foo 的任何引用都会导致对 filtee filtee.so.1 的额外装入。运行时链接程序使用此 filtee 仅解析 filter.so.2 定义的符号 foo。对符号 bar 的引用始终使用 filter.so.2 中的符号,因为没有为此符号定义 filtee 处理。
例如,以下动态可执行文件 prog 引用符号 foo 和 bar,这两个符号在链接编辑过程中通过过滤器 filter.so.2 进行解析。执行 prog 会导致从 filtee filtee.so.1 中获取 foo,从过滤器 filter.so.2 中获取 bar。
$ cc -o prog main.c -R. filter.so.2 $ prog foo is defined in filtee: bar is defined in filter |
在这些示例中,filtee filtee.so.1 仅与过滤器关联。不能使用此 filtee 从任何其他可能作为 prog 执行结果而装入的目标文件中实现符号查找。
标准过滤器提供了一种用于定义现有共享库的子集接口的便捷机制。使用标准过滤器,可以跨多个现有共享库创建接口组。标准过滤器还提供一种将接口重定向到实现的方法。在 Solaris OS 中,可以使用多个标准过滤器。
/usr/lib/libsys.so.1 过滤器提供标准 C 库 /usr/lib/libc.so.1 的子集。此子集表示位于必须由兼容应用程序导入的 C 库中兼容 ABI 的函数和数据项。
/lib/libxnet.so.1 过滤器使用多个 filtee。 此库提供来自 /lib/libsocket.so.1、/lib/libnsl.so.1 和 /lib/libc.so.1 的套接字接口和 XTI 接口。
libc.so.1 定义运行时链接程序的接口过滤器。这些接口在以下两者之间提供了抽象:libc.so.1 的编译环境中引用的符号,以及 ld.so.1(1) 的运行时环境中生成的实际实现绑定。
libnsl.so.1 针对 libc.so.1 定义标准过滤器 strerror(3C)。以前,libnsl.so.1 和 libc.so.1 针对此符号提供相同的实现。通过将 libnsl.so.1 建立为过滤器,只需要存在一种 gethostname() 实现。当 libnsl.so.1 继续导出 gethostname() 时,此库接口将一直兼容早期发行版。
由于在运行时从不引用标准过滤器中的代码,因此,无需向任何定义为过滤器的函数中添加内容。任何过滤器代码都可能需要重定位,这样会在运行时处理过滤器期间导致不必要的开销。建议将函数定义为空例程,或者直接从 mapfile 进行定义。请参见定义其他符号。
链接编辑器使用所处理的第一个可重定位文件的 ELF 类来控制所创建的目标文件类。请使用链接编辑器的 -64 选项以仅通过 mapfile 创建 64 位过滤器。
在过滤器中生成数据符号时,始终会初始化数据项。生成的数据定义可确保从动态可执行文件正确地建立引用。链接编辑器执行某些更为复杂的符号解析时,需要了解符号的属性(包括符号大小)。因此,应该在过滤器中生成符号,以便符号属性与 filtee 中的符号属性匹配。维护属性一致性可确保链接编辑过程使用与运行时所用的符号定义兼容的方式来分析过滤器。请参见符号解析。
要生成辅助过滤器,应先定义要应用过滤的 filtee。以下示例将生成 filtee filtee.so.1,并提供符号 foo。
$ cat filtee.c char * foo() { return("defined in filtee"); } $ cc -o filtee.so.1 -G -K pic filtee.c |
可以通过以下两种方法之一提供辅助过滤。要将共享库提供的所有接口都声明为辅助过滤器,请使用链接编辑器的 -f 标志。要将共享库的单个接口声明为辅助过滤器,请使用链接编辑器 mapfile 和 AUXILIARY 指令。
在以下示例中,会将共享库 filter.so.1 定义为辅助过滤器。filter.so.1 提供符号 foo 和 bar,并且是 filtee filtee.so.1 的辅助过滤器。在本示例中,使用环境变量 LD_OPTIONS 禁止编译器驱动程序解释 -f 选项。
$ cat filter.c char * bar = "defined in filter"; char * foo() { return ("defined in filter"); } $ LD_OPTIONS='-f filtee.so.1' \ cc -o filter.so.1 -G -K pic -h filter.so.1 -R. filter.c $ elfdump -d filter.so.1 | egrep "SONAME|AUXILIARY" [2] SONAME 0xee filter.so.1 [3] AUXILIARY 0xfb filtee.so.1 |
创建动态可执行文件或共享库时,链接编辑器可将辅助过滤器 filter.so.1 引用为依赖项。链接编辑器使用此过滤器符号表中的信息来实现任何符号解析。但是,在运行时,对此过滤器符号的任何引用都会导致搜索 filtee filtee.so.1。如果找到此 filtee,则运行时链接程序会使用此 filtee 来解析 filter.so.1 定义的所有符号。 如果未找到此 filtee,或者在此 filtee 中未找到过滤器符号,则会使用过滤器中的初始符号。
例如,以下动态可执行文件 prog 引用符号 foo 和 bar,这两个符号在链接编辑过程中通过过滤器 filter.so.1 进行解析。执行 prog 会导致从 filtee filtee.so.1 中获取 foo,而不是从过滤器 filter.so.1 中获取。但是,从过滤器 filter.so.1 中获取 bar,因为此符号在 filtee filtee.so.1 中没有备选定义。
$ cat main.c extern char * bar, * foo(); main() { (void) printf("foo is %s: bar is %s\n", foo(), bar); } $ cc -o prog main.c -R. filter.so.1 $ prog foo is defined in filtee: bar is defined in filter |
在以下示例中,共享库 filter.so.2 将它的一个接口 foo 定义为 filtee filtee.so.1 的辅助过滤器。
$ cat filter.c char * bar = "defined in filter"; char * foo() { return ("defined in filter"); } $ cat mapfile { global: foo = AUXILIARY filtee.so.1; }; $ cc -o filter.so.2 -G -K pic -h filter.so.2 -M mapfile -R. filter.c $ elfdump -d filter.so.2 | egrep "SONAME|AUXILIARY" [2] SONAME 0xd8 filter.so.2 [3] SUNW_AUXILIARY 0xfb filtee.so.1 $ elfdump -y filter.so.2 | egrep "foo|bar" [1] A [3] filtee.so.1 foo [10] D <self> bar |
在运行时,对过滤器符号 foo 的任何引用都会导致搜索 filtee filtee.so.1。如果找到此 filtee,则会装入此 filtee。然后,使用此 filtee 来解析 filter.so.2 定义的符号 foo。如果未找到此 filtee,则使用 filter.so.2 定义的符号 foo。对符号 bar 的引用始终使用 filter.so.2 中的符号,因为没有为此符号定义 filtee 处理。
例如,以下动态可执行文件 prog 引用符号 foo 和 bar,这两个符号在链接编辑过程中通过过滤器 filter.so.2 进行解析。如果 filtee filtee.so.1 存在,则执行 prog 会导致从 filtee filtee.so.1 中获取 foo,从过滤器 filter.so.2 中获取 bar。
$ cc -o prog main.c -R. filter.so.2 $ prog foo is defined in filtee: bar is defined in filter |
如果 filtee filtee.so.1 不存在,则执行 prog 会导致从过滤器 filter.so.2 中获取 foo 和 bar。
$ prog foo is defined in filter: bar is defined in filter |
在这些示例中,filtee filtee.so.1 仅与过滤器关联。不能使用此 filtee 从任何其他可能作为 prog 执行结果而装入的目标文件中实现符号查找。
辅助过滤器提供了一种用于定义现有共享库的备用接口的机制。在 Solaris OS 中使用此机制可以提供优化的硬件功能以及平台特定的共享库。有关示例,请参见特定于硬件功能的共享库、特定于指令集的共享库和特定于系统的共享库。
可以设置环境变量 LD_NOAUXFLTR 以禁用运行时链接程序辅助过滤器处理。由于通常使用辅助过滤器来提供平台特定的优化,因此,该选项在评估 filtee 用法及其性能影响方面很有用。
可以在同一共享库中同时定义用于定义标准过滤器的单个接口以及用于定义辅助过滤器的单个接口。通过使用 mapfile 指令 FILTER 和 AUXILIARY 指定所需的 filtee,可以实现这种过滤器定义组合。
使用 -F 或 -f 选项将其所有接口都定义为过滤器的共享库可以是标准过滤器,也可以是辅助过滤器。
共享库可以定义单个接口以将其用作过滤器,同时还可以将目标文件的所有接口都定义为过滤器。在这种情况下,首先处理针对接口定义的单个过滤。如果无法针对单个接口过滤器建立 filtee,则针对过滤器的所有接口定义的 filtee 会在适用时提供回退。
例如,请考虑过滤器 filter.so.1。此过滤器使用链接编辑器的 -f 标志,针对 filtee filtee.so.1 将所有接口都定义为辅助过滤器。 filter.so.1 还使用 mapfile 指令 FILTER,针对 filtee foo.so.1 将单个接口 foo 定义为标准过滤器。filter.so.1 还使用 mapfile 指令 AUXILIARY,针对 filtee bar.so.1 将单个接口 bar 定义为辅助过滤器。
对 foo 的外部引用会导致处理 filtee foo.so.1。如果在 foo.so.1 中未找到 foo,则不会对过滤器执行进一步处理。在这种情况下,不会执行回退处理,因为已将 foo 定义为标准过滤器。
对 bar 的外部引用会导致处理 filtee bar.so.1。如果在 bar.so.1 中未找到 bar,则处理会回退到 filtee filtee.so.1。在这种情况下,会执行回退处理,因为已将 bar 定义为辅助过滤器。如果在 filtee.so.1 中未找到 bar,则最终会使用过滤器 filter.so.1 中的 bar 定义来解析外部引用。
运行时链接程序处理过滤器时会延迟装入 filtee,直到引用过滤器符号。 这种实现类似于过滤器在需要 filtee 时,使用模式 RTLD_LOCAL 对每个 filtee 执行 dlopen(3C)。这种实现考虑了由诸如 ldd(1) 的工具生成的依赖项报告中存在的差异。
创建过滤器时,可以使用链接编辑器的 -z loadfltr 选项以便在运行时立即处理 filtee。此外,通过将 LD_LOADFLTR 环境变量设置为任意值,可触发在进程中立即处理所有的 filtee。
一个共享库可供同一系统中的多个应用程序使用。共享库的性能会影响使用此共享库的应用程序,并且会影响整个系统。
虽然共享库中的代码会直接影响运行进程的性能,但此处讨论的性能问题则涉及共享库的运行时处理。本节通过考虑各个方面(如文本大小和纯度)以及重定位开销,更详细地介绍了这种处理。
有多种工具可用来分析 ELF 文件的内容。要显示文件的大小,请使用 size(1) 命令。
$ size -x libfoo.so.1 59c + 10c + 20 = 0x6c8 $ size -xf libfoo.so.1 ..... + 1c(.init) + ac(.text) + c(.fini) + 4(.rodata) + \ ..... + 18(.data) + 20(.bss) ..... |
第一个示例指明共享库文本、数据以及 bss(SunOS 操作系统早期发行版中使用的一种分类)的大小。
ELF 格式通过将数据组织到多个节中,为表示文件中的数据提供了更佳的粒度。第二个示例显示了文件的每个可装入节的大小。
分配给单元的各节称为段,某些段描述如何将文件的各部分映射到内存。请参见 mmap(2)。可以使用 dump(1) 命令并检查 LOAD 项来显示这些可装入段。
$ dump -ov libfoo.so.1 libfoo.so.1: ***** PROGRAM EXECUTION HEADER ***** Type Offset Vaddr Paddr Filesz Memsz Flags Align LOAD 0x94 0x94 0x0 0x59c 0x59c r-x 0x10000 LOAD 0x630 0x10630 0x0 0x10c 0x12c rwx 0x10000 |
在共享库 libfoo.so.1 中存在两种可装入段,通常称为文本段和数据段。将映射文本段以允许读取和执行其内容 (r-x),同时将映射数据段以允许修改其内容 (rwx)。数据段的内存大小 (Memsz) 不同于文件大小 (Filesz)。该差异说明存在 .bss 节,此节属于数据段并在装入数据段时动态创建。
程序员通常根据定义其代码中的函数和数据元素的符号来考虑文件。可以使用 nm(1) 显示这些符号。 例如:
$ nm -x libfoo.so.1 [Index] Value Size Type Bind Other Shndx Name ......... [39] |0x00000538|0x00000000|FUNC |GLOB |0x0 |7 |_init [40] |0x00000588|0x00000034|FUNC |GLOB |0x0 |8 |foo [41] |0x00000600|0x00000000|FUNC |GLOB |0x0 |9 |_fini [42] |0x00010688|0x00000010|OBJT |GLOB |0x0 |13 |data [43] |0x0001073c|0x00000020|OBJT |GLOB |0x0 |16 |bss ......... |
可以通过引用符号表中的节索引 (Shndx) 字段并使用 dump(1) 显示文件各节来确定包含符号的节。 例如:
$ dump -hv libfoo.so.1 libfoo.so.1: **** SECTION HEADER TABLE **** [No] Type Flags Addr Offset Size Name ......... [7] PBIT -AI 0x538 0x538 0x1c .init [8] PBIT -AI 0x554 0x554 0xac .text [9] PBIT -AI 0x600 0x600 0xc .fini ......... [13] PBIT WA- 0x10688 0x688 0x18 .data [16] NOBI WA- 0x1073c 0x73c 0x20 .bss ......... |
上述 nm(1) 和 dump(1) 示例的输出显示函数 _init、foo 和 _fini 与节 .init、.text 和 .fini 关联。这些节由于具有只读性质,因此属于文本段。
同样,数据数组 data 和 bss 分别与节 .data 和 .bss 关联。这些节由于具有可写性质,因此属于数据段。
先前的 dump(1) 显示已针对本示例进行了简化。
使用共享库生成应用程序时,会在运行时将此目标文件的全部可装入内容映射到此进程的虚拟地址空间。每个使用共享库的进程通过引用内存中此共享库的单个副本来启动。
处理共享库中的重定位以将符号引用绑定到相应的定义。这会导致计算那些无法在链接编辑器生成共享库时得到的实际虚拟地址。通常,这些重定位会导致更新进程数据段中的项。
基于动态共享库链接的内存管理方案将按照页粒度在各进程之间共享内存。只要在运行时未修改内存页,便可共享这些内存页。如果某个进程在写入数据项或在重定位对共享库的引用时写入一个共享库页,则会生成此页的专用副本。此专用副本不会影响此共享库的其他用户。但是,其他进程无法共享此页。通过此方式修改的文本页称为不纯文本页。
映射到内存的共享库段分为两种基本类别,分别是只读的文本段和可读写的数据段。有关如何从 ELF 文件中获取此信息,请参见分析文件。开发共享库时的最重要目标是最大化文本段以及最小化数据段。这样可优化代码共享量,同时减少初始化和使用共享库所需的处理量。本节介绍有助于实现此目标的机制。
通过将共享库建立为延迟可装入目标文件,可以延迟装入该目标文件依赖项,直到首次引用依赖项。请参见延迟装入动态依赖项。
对于小型应用程序,典型的执行线程可以引用所有的应用程序依赖项。应用程序将装入所有的依赖项,而无论是否将这些依赖项定义为延迟可装入依赖项。但是,使用延迟装入,会延迟依赖项处理,从进程启动一直延迟到整个进程执行过程。
对于具有许多依赖项的应用程序,延迟装入通常会导致根本没有装入某些依赖项。仅装入针对特定执行线程引用的依赖项。
动态可执行文件中的代码通常与位置相关,并且与内存中的固定地址关联。相反,共享库可装入不同进程中的不同地址。位置无关代码不与特定地址关联。这种无关性允许在每个使用此类代码的进程中的不同地址有效地执行代码。建议在创建共享库时使用与位置无关的代码。
使用 -K pic 选项编译器可以生成与位置无关的代码。
如果共享库根据位置相关代码生成,则在运行时可能需要修改文本段。通过此修改,可以为已装入目标文件的位置指定可重定位引用。文本段的重定位需要将此段重映射为可写段。这种修改需要预留交换空间,并且会形成此进程的文本段专用副本。此文本段不再供多个进程共享。通常,位置相关代码比相应的与位置无关的代码需要更多的运行时重定位。总体而言,处理文本重定位的开销可能会严重降低性能。
根据与位置无关的代码生成共享库时,会通过共享库数据段中的数据间接生成可重定位引用。文本段中的代码不需要进行任何修改。所有重定位更新都会应用于数据段中的相应项。有关特定间接技术的更多详细信息,请参见全局偏移表(特定于处理器)和过程链接表(特定于处理器)。
如果存在文本重定位,运行时链接程序便会尝试处理这些重定位。但是,某些重定位无法在运行时实现。
通常,x64 位置相关代码序列生成的代码只能装入内存的低 32 位。任何地址的高 32 位必须全部为零。由于共享库通常装入内存高位,因此需要地址的高 32 位。这样,x64 共享库中位置相关代码便无法满足重定位要求。在共享库中使用此类代码会导致出现运行时重定位错误。
$ prog ld.so.1: prog: fatal: relocation error: R_AMD64_32: file \ libfoo.so.1: symbol (unknown): value 0xfffffd7fff0cd457 does not fit |
与位置无关的代码可以装入内存中的任何区域,从而可以满足 x64 共享库的要求。
这种情况不同于用于 64 位 SPARCV9 代码的缺省 ABS64 模式。这种位置相关代码通常兼容整个 64 位地址范围。 因此,位置相关代码序列可以存在于 SPARCV9 共享库中。针对 64 位 SPARCV9 代码使用 ABS32 模式或 ABS44 模式仍会导致无法在运行时解析的重定位。但是,这两种模式都需要运行时链接程序对文本段进行重定位。
无论运行时链接程序功能如何,也无论重定位要求的差异如何,共享库都应该使用与位置无关的代码生成。
可以根据文本段确定需要重定位的共享库。以下示例使用 dump(1) 确定是否存在 TEXTREL 项动态项。
$ cc -o libfoo.so.1 -G -R. foo.c $ dump -Lv libfoo.so.1 | grep TEXTREL [9] TEXTREL 0 |
TEXTREL 项的值无关紧要。共享库中存在此项表示存在文本重定位。
要防止创建包含文本重定位的共享库,请使用链接编辑器的 -z text 标志。此标志会导致链接编辑器生成指示将位置相关代码源用作输入的诊断。以下示例显示位置相关代码如何导致无法生成共享库。
$ cc -o libfoo.so.1 -z text -G -R. foo.c Text relocation remains referenced against symbol offset in file foo 0x0 foo.o bar 0x8 foo.o ld: fatal: relocations remain against allocatable but \ non-writable sections |
将根据文本段生成两个重定位,因为通过文件 foo.o 生成了位置相关代码。如有可能,这些诊断会指明执行重定位所需的任何符号引用。在这种情况下,将根据符号 foo 和 bar 进行重定位。
如果包括手写汇编程序代码,但不包括相应的位置无关原型,则在共享库中也会出现文本重定位。
可能需要使用一些简单的源文件进行实验,以确定启用位置无关性的编码序列。请使用编译器功能来生成中间汇编程序输出。
对于 SPARC 二进制文件,-K pic 选项与备用 -K PIC 选项之间的细微差异会影响对全局偏移表项的引用。请参见全局偏移表(特定于处理器)。
全局偏移表是一个指针数组,对于 32 位(4 个字节)和 64 位(8 个字节)目标文件其项大小为常量。以下代码序列使用 -K pic 引用项:
ld [%l7 + j], %o0 ! load &j into %o0 |
其中,%l7 是执行引用的目标文件的 _GLOBAL_OFFSET_TABLE_ 符号的预计算值。
此代码序列为全局偏移表项提供了 13 位位移常量。因此,此位移为 32 位目标文件提供了 2048 个唯一项,为 64 位目标文件提供了 1024 个唯一项。如果创建目标文件需要的项数多于可用项数,则链接编辑器会生成以下致命错误:
$ cc -K pic -G -o lobfoo.so.1 a.o b.o ... z.o ld: fatal: too many symbols require `small' PIC references: have 2050, maximum 2048 -- recompile some modules -K PIC. |
要克服这种错误情况,请使用 -K PIC 选项编译某些输入可重定位目标文件。此选项为全局偏移表项提供了 32 位常量:
sethi %hi(j), %g1 or %g1, %lo(j), %g1 ! get 32–bit constant GOT offset ld [%l7 + %g1], %o0 ! load &j into %o0 |
可以使用带有 -G 选项的 elfdump(1) 查看目标文件的全局偏移表要求。还可以使用链接编辑器调试标记 -D got,detail 在链接编辑过程中检查这些项的处理。
理论上,使用 -K pic 模型对经常访问的数据项有益。可以使用这两种模型引用单个项。但是,确定哪些可重定位目标文件应该使用其中一个选项进行编译可能会相当耗时,并且不会显著改善性能。通常,使用 -K PIC 选项可轻松重新编译所有的可重定位目标文件。
包含要生成的目标文件未使用的函数和数据是一种浪费。此类材料使得目标文件变得过大,从而导致不必要的重定位开销以及关联的换页活动。对未使用的依赖项的引用也是一种浪费。这些引用会导致不必要地装入和处理其他共享库。
使用链接编辑器调试标记 -D unused 时,会在链接编辑过程中显示未使用的节。应该从链接编辑中删除标识为未使用的节。可以使用链接编辑器 -z ignore 选项删除未使用的节。
在以下情况下,链接编辑器会将可重定位目标文件中的节标识为未使用:
此节可分配
没有其他节绑定(重定位)到此节
此节不提供任何全局符号
通过定义共享库的外部接口可以改进链接编辑器删除节的功能。通过定义接口,可以将未定义为此接口一部分的全局符号降级为局部符号。现在,可以将未从其他目标文件引用的降级后符号明确标识为删除目标文件。
如果将单个函数和数据变量指定给其自己的节,则使用链接编辑器可以删除这些项。可以使用诸如 -xF 的编译器选项完善此节。较早的编译器仅可用于将函数指定给其自己的节。较新的编译器已扩展了 -xF 语法,可以将数据变量指定给其自己的节。较早的编译器要求在使用 -xF 时禁用 C++ 异常处理。较新的编译器中已删除了此限制。
如果可以删除可重定位目标文件中的所有可分配节,则在链接编辑时会放弃整个文件。
除了删除输入文件之外,链接编辑器还可标识未使用的依赖项。如果要生成的目标文件未绑定某个依赖项,则会将此依赖项视为未使用。可以使用 -z ignore 选项生成目标文件,以避免记录未使用的依赖项。
-z ignore 选项仅应用于链接编辑命令行中该选项后的文件。可以使用 -z record 取消 -z ignore 选项。
如基础系统中所述,只有共享库的文本段才可供所有使用此目标文件的进程共享。目标文件的数据段通常无法共享。在数据段中写入数据项时,每个使用共享库的进程都会生成一个其完整数据段的专用内存副本。可以通过把永远不会修改的数据元素移到文本段或者完全删除数据项来减小数据段。
本节介绍了几种可用于减小数据段大小的机制。
应该使用 const 声明将只读数据元素移动到文本段中。例如,以下字符串位于 .data 节中,此节属于可写数据段:
char * rdstr = "this is a read-only string"; |
相反,以下字符串位于 .rodata 节中,此节是文本段中的只读数据节:
const char * rdstr = "this is a read-only string"; |
通过将只读元素移动到文本段中来减小数据段是一种极好的方法。但是,移动需要重定位的数据元素可能会达不到预期目标。例如,请查看以下字符串数组:
char * rdstrs[] = { "this is a read-only string", "this is another read-only string" }; |
更佳定义可能如下:
const char * const rdstrs[] = { ..... }; |
此定义可确保将字符串以及指向这些字符串的指针数组放在 .rodata 节中。遗憾的是,虽然用户将地址数组视为只读,但是在运行时必须重定位这些地址。因此,此定义会导致创建文本重定位。将此定义表示为:
const char * rdstrs[] = { ..... }; |
将确保在可重定位数组指针的可写数据段中维护这些指针。数组字符串将在只读文本段中维护。
某些编译器在生成与位置无关的代码时可以检测到会导致运行时重定位的只读指定。这些编译器会安排将此类项放在可写段中。例如,.picdata。
可以通过折叠多重定义数据来减小数据大小。多次出现相同错误消息的程序可以通过定义全局数据来加以改进,并可使所有其他实例都引用此全局数据。 例如:
const char * Errmsg = "prog: error encountered: %d"; foo() { ...... (void) fprintf(stderr, Errmsg, error); ...... |
进行此类数据缩减的主要目标文件是字符串。可以使用 strings(1) 查看共享库中的字符串用法。以下示例在文件 libfoo.so.1 中生成数据字符串的有序表。此列表中的每项都使用字符串的出现次数作为前缀。
$ strings -10 libfoo.so.1 | sort | uniq -c | sort -rn |
如果将关联的功能设计为使用自动(栈)变量,则可以完全删除数据项的永久性存储。通常,任何永久性存储删除操作都会导致所需运行时重定位数的相应地减少。
大型数据缓冲区通常应该动态分配,而不是使用永久性存储进行定义。通常,这样会从整体上节省内存,因为只分配当前调用应用程序所需的那些缓冲区。动态分配还可在不影响兼容性的情况下通过允许更改缓冲区大小来提供更大的灵活性。
任何访问新页的进程都会导致页面错误,这是一种开销很大的操作。由于共享库可供许多进程使用,因此,减少由于访问共享库而生成的页面错误数会对进程和整个系统有益。
将常用例程及其数据组织到一组相邻页中通常会改善性能,因为这样改善了引用的邻近性。当进程调用其中一个函数时,此函数可能已在内存中,因为它与其他常用函数邻近。同样,将相互关联的函数组织在一起也会改善引用的邻近性。例如,如果每次调用函数 foo() 都会导致调用函数 bar(),则应将这些函数放在同一页中。可以使用诸如 cflow(1)、tcov(1)、prof(1) 和 gprof(1) 的工具来确定代码适用范围和配置。
应将相关功能与其共享库隔离开来。以前,生成的标准 C 库包含许多无关函数。仅在极少数情况下,某个可执行文件才可能会使用此库中的所有函数。由于这些函数用途广泛,因此,确定实际上最常用的函数组也具有一定的难度。相反,刚开始设计共享库时,只在此共享库中维护相关函数。这样会改善引用的邻近性,并会产生减小目标文件总体大小的负面影响。
在重定位处理中,介绍了运行时链接程序重定位动态可执行文件和共享库以创建可运行进程所依据的机制。重定位符号查找和执行重定位的时间将此重定位处理分为两类,以简化和帮助说明所涉及的机制。理论上,考虑重定位对性能的影响时也要区分这两种类别。
当运行时链接程序需要查找符号时,缺省情况下它会通过搜索每个目标文件进行查找。运行时链接程序首先搜索动态可执行文件,然后按照共享库的装入顺序搜索每个共享库。在多数情况下,会找出需要符号重定位的共享库以提供符号定义。
在这种情况下,如果不需要此重定位所用的符号成为共享库接口的一部分,则首选将此符号转换为静态或自动变量。还可以应用符号缩减以从共享库接口中删除符号。 有关更多详细信息,请参见缩减符号范围。通过进行上述转换,链接编辑器在创建共享库过程中,会产生针对这些符号处理符号重定位的开销。
应在共享库中可见的全局数据项只是那些属于此共享库用户界面的数据项。以前,这是要实现的硬性目标,因为通常将全局数据定义为允许从两个或多个位于不同源文件中的函数进行引用。通过应用符号缩减,可以删除不必要的全局符号。请参见缩减符号范围。减少从共享库导出的全局符号数会降低重定位成本,并全面改善性能。
在具有许多符号重定位和依赖项的动态进程中,使用直接绑定也可以显著降低符号查找开销。请参见直接绑定。
在应用程序获得控制权之前,必须在进程初始化过程中执行所有立即引用重定位。但是,可以延迟所有延迟引用重定位,直到调用第一个函数实例。立即重定位通常由于数据引用而产生。因此,减少数据引用数也会缩短进程的运行时初始化时间。
将数据引用转换为函数引用也可延迟初始化重定位成本。例如,可以通过功能接口返回数据项。此转换通常会显著改善性能,因为初始化重定位成本有效分布在整个进程执行过程中。特定的进程调用可能从不调用某些功能接口,因此完全避免了这些接口的重定位开销。
复制重定位一节中介绍了使用功能接口的优点。该节介绍了一种在动态可执行文件与共享库之间使用的开销稍大的特殊重定位机制。此外,还提供了如何避免此重定位开销的示例。
缺省情况下,按照应用重定位的节对重定位进行分组。但是,当使用 -z combreloc 选项生成目标文件时,会将过程链接表重定位之外的所有重定位都放在名为 .SUNW_reloc 的单个公用节中。请参见过程链接表(特定于处理器)。
通过此方式组合重定位记录会将所有的 RELATIVE 重定位组织在一起。所有符号重定位均按符号名称进行排序。组织 RELATIVE 重定位可允许使用 DT_RELACOUNT/DT_RELCOUNT .dynamic 项优化运行时处理。有序符号项有助于缩短运行时符号查找时间。
共享库通常使用与位置无关的代码生成。对此类型代码的外部数据项的引用通过一组表实现间接寻址。 有关更多详细信息,请参见与位置无关的代码。在运行时,将使用数据项的实际地址更新这些表。使用这些已更新的表,无需修改代码本身即可访问数据。
但是,动态可执行文件通常并不使用与位置无关的代码创建。它们所执行的任何外部数据引用看似只能在运行时通过修改执行引用的代码来实现。应避免修改只读文本段。可以使用复制重定位技术来解决此引用。
假设使用链接编辑器创建动态可执行文件,并且发现对数据项的引用位于其中一个相关共享库中。将在动态可执行文件的 .bss 中分配空间,空间的大小等于共享库中的数据项的大小。还为此空间指定在共享库中定义的符号名称。分配此数据时,链接编辑器会生成特殊的复制重定位记录,指示运行时链接程序将数据从共享库复制到动态可执行文件中的已分配空间。
由于指定给此空间的符号为全局符号,因此,使用此符号可以实现任何共享库引用。动态可执行文件可继承数据项。进程中对此项进行引用的任何其他目标文件都绑定到此副本。生成此副本所依据的原始数据实际上变成了未使用的数据。
此机制的以下示例使用一组在标准 C 库中维护的系统错误消息。在 SunOS 操作系统早期发行版中,通过两个全局变量 sys_errlist[] 和 sys_nerr 提供此信息接口。第一个变量提供错误消息字符串数组,而第二个变量告知数组本身的大小。这些变量通常按照以下方式用于应用程序中:
$ cat foo.c extern int sys_nerr; extern char * sys_errlist[]; char * error(int errnumb) { if ((errnumb < 0) || (errnumb >= sys_nerr)) return (0); return (sys_errlist[errnumb]); } |
应用程序使用函数 error 提供焦点以获取与编号 errnumb 关联的系统错误消息。
对使用此代码生成的动态可执行文件进行检查,可以更详细地显示复制重定位的实现:
$ cc -o prog main.c foo.c $ nm -x prog | grep sys_ [36] |0x00020910|0x00000260|OBJT |WEAK |0x0 |16 |sys_errlist [37] |0x0002090c|0x00000004|OBJT |WEAK |0x0 |16 |sys_nerr $ dump -hv prog | grep bss [16] NOBI WA- 0x20908 0x908 0x268 .bss $ dump -rv prog **** RELOCATION INFORMATION **** .rela.bss: Offset Symndx Type Addend 0x2090c sys_nerr R_SPARC_COPY 0 0x20910 sys_errlist R_SPARC_COPY 0 .......... |
链接编辑器已在动态可执行文件的 .bss 中分配了空间,以便接收由 sys_errlist 和 sys_nerr 表示的数据。这些数据是运行时链接程序在进程初始化时从 C 库中复制的。因此,每个使用这些数据的应用程序都在其自己的数据段中获取数据的专用副本。
此技术存在两个缺点。第一,每个应用程序都会由于运行时产生的复制数据开销而降低了性能。第二,数据数组 sys_errlist 的大小现在已成为 C 库接口的一部分。假设要更改此数组的大小,则可能是因为添加了新的错误消息。任何引用此数组的动态可执行文件都必须进行新的链接编辑,以便可以访问所有新错误消息。如果不进行这种新的链接编辑,则动态可执行文件中的已分配空间不足以包含新的数据。
如果动态可执行文件所需的数据由功能接口提供,则不会存在这些缺点。ANSI C 函数 strerror(3C) 基于提供给它的错误号返回指向相应错误字符串的指针。此函数的一种实现可能如下所示:
$ cat strerror.c static const char * sys_errlist[] = { "Error 0", "Not owner", "No such file or directory", ...... }; static const int sys_nerr = sizeof (sys_errlist) / sizeof (char *); char * strerror(int errnum) { if ((errnum < 0) || (errnum >= sys_nerr)) return (0); return ((char *)sys_errlist[errnum]); } |
现在,可以将 foo.c 中的错误例程简化为使用此功能接口。通过这种简化,无需在进程初始化时执行原始复制重定位。
此外,由于数据现在对于共享库而言是本地数据,因此数据不再是其接口的一部分。因此,共享库可以灵活地更改数据,而不会对任何使用此数据的动态可执行文件造成不良影响。通常,从共享库接口中删除数据项会改善性能,同时使得共享库接口和代码更易于维护。
ldd(1) 与 -d 或 -r 选项一起使用时,可以检验动态可执行文件中存在的任何复制重定位。
例如,假设动态可执行文件 prog 最初根据共享库 libfoo.so.1 生成,并且已记录以下两个复制重定位:
$ nm -x prog | grep _size_ [36] |0x000207d8|0x40|OBJT |GLOB |15 |_size_gets_smaller [39] |0x00020818|0x40|OBJT |GLOB |15 |_size_gets_larger $ dump -rv size | grep _size_ 0x207d8 _size_gets_smaller R_SPARC_COPY 0 0x20818 _size_gets_larger R_SPARC_COPY 0 |
以下是此共享库的新版本,其中包含这些符号的不同数据大小:
$ nm -x libfoo.so.1 | grep _size_ [26] |0x00010378|0x10|OBJT |GLOB |8 |_size_gets_smaller [28] |0x00010388|0x80|OBJT |GLOB |8 |_size_gets_larger |
针对此动态可执行文件运行 ldd(1) 将显示以下内容:
$ ldd -d prog libfoo.so.1 => ./libfoo.so.1 ........... copy relocation sizes differ: _size_gets_smaller (file prog size=40; file ./libfoo.so.1 size=10); ./libfoo.so.1 size used; possible insufficient data copied copy relocation sizes differ: _size_gets_larger (file prog size=40; file ./libfoo.so.1 size=80); ./prog size used; possible data truncation |
ldd(1) 显示此动态可执行文件将复制此共享库必须提供的所有数据,但只接受其已分配空间所允许的数据量。
通过根据与位置无关的代码生成应用程序可以删除复制重定位。请参见与位置无关的代码。
使用链接编辑器的 -B symbolic 选项,可以将符号引用绑定到共享库中的相应全局定义。此选项由来已久,因为设计它是为了在创建运行时链接程序本身时使用。
定义目标文件接口并将非公共符号降级为局部符号时,首选使用 -B symbolic 选项。请参见缩减符号范围。使用 -B symbolic 通常会产生某些非直观的负面影响。
如果插入以符号形式绑定的符号,则从以符号形式绑定的目标文件外部对此符号的引用将绑定到插入项。目标文件本身已在内部绑定。实际上,现在可以从进程中引用两个同名符号。导致复制重定位的以符号形式绑定的数据符号的插入情况同上。请参见复制重定位。
以符号形式绑定的共享库由 .dynamic 标志 DF_SYMBOLIC 标识。此标志仅用于提供信息。运行时链接程序在这些目标文件中处理符号查找的方式与在任何其他目标文件中的方式相同。假设任一符号绑定均已在链接编辑阶段创建。
运行时链接程序可以针对任何在运行应用程序时处理的共享库生成配置信息。运行时链接程序负责将共享库绑定到应用程序,因此它可以拦截任何全局函数绑定。这些绑定通过 .plt 项执行。有关此机制的详细信息,请参见执行重定位的时间。
LD_PROFILE 环境变量将指定配置文件的共享库名称。可以使用此环境变量分析单个共享库。可以使用此环境变量的设置来分析一个或多个应用程序使用此共享库的方式。在以下示例中,将分析命令 ls(1) 的单次调用如何使用 libc:
$ LD_PROFILE=libc.so.1 ls -l |
在以下示例中,将在配置文件中记录此环境变量设置。此设置会使用应用程序的 libc 用法累积分析信息:
# crle -e LD_PROFILE=libc.so.1 $ ls -l $ make $ ... |
启用配置时,便会创建配置数据文件(如果尚未存在)。此文件由运行时链接程序进行映射。在上述示例中,此数据文件为 /var/tmp/libc.so.1.profile。64 位库需要扩展配置文件格式,并使用 .profilex 后缀写入。还可以指定备用目录,以便使用 LD_PROFILE_OUTPUT 环境变量存储配置数据。
此配置数据文件用于存储 profil(2) 数据,并调用与使用指定共享库相关的计数信息。使用 gprof(1) 可以直接检查此配置数据。
gprof(1) 最常用于分析由可执行文件(已使用 cc(1) 的 -xpg 选项进行编译)创建的 gmon.out 配置数据。运行时链接程序的配置文件分析不要求使用此选项编译任何代码。其相关共享库正在配置的应用程序不应该调用 profil(2),因为此系统调用不会在同一进程中提供多次调用。由于相同的原因,不能使用 cc(1) 的 -xpg 选项编译这些应用程序。这种编译器生成的配置机制也会在 profil(2) 的顶部生成。
此配置机制最强大的功能之一就是可以分析由多个应用程序使用的共享库。通常,使用一个或两个应用程序执行配置分析。但是,共享库就其本质而言可供多个应用程序使用。分析这些应用程序使用共享库的方式,可以了解应在何处投入更多精力以改善共享库的整体性能。
以下示例给出了在某个源分层结构中创建多个应用程序时对 libc 进行的性能分析。
$ LD_PROFILE=libc.so.1 ; export LD_PROFILE $ make $ gprof -b /lib/libc.so.1 /var/tmp/libc.so.1.profile ..... granularity: each sample hit covers 4 byte(s) .... called/total parents index %time self descendents called+self name index called/total children ..... ----------------------------------------------- 0.33 0.00 52/29381 _gettxt [96] 1.12 0.00 174/29381 _tzload [54] 10.50 0.00 1634/29381 <external> 16.14 0.00 2512/29381 _opendir [15] 160.65 0.00 25009/29381 _endopen [3] [2] 35.0 188.74 0.00 29381 _open [2] ----------------------------------------------- ..... granularity: each sample hit covers 4 byte(s) .... % cumulative self self total time seconds seconds calls ms/call ms/call name 35.0 188.74 188.74 29381 6.42 6.42 _open [2] 13.0 258.80 70.06 12094 5.79 5.79 _write [4] 9.9 312.32 53.52 34303 1.56 1.56 _read [6] 7.1 350.53 38.21 1177 32.46 32.46 _fork [9] .... |
特殊名称 <external> 指示要从正在配置的共享库的地址范围之外进行引用。因此,在上一示例中,从可执行文件,或者从其他共享库(正在进行配置分析时绑定到 libc)对 libc 中的 open(2) 函数进行了 1634 次调用。
共享库配置具有多线程安全性,但以下情况除外:一个线程调用 fork(2),而另一个线程正在更新配置数据信息。可以使用 fork(2) 删除此限制。