so库的性能注意事项
2010年09月07日
一个共享库可供同一系统中的多个应用程序使用。共享库的性能会影响使用此共享库的应用程序,并且会影响整个系统。
虽然共享库中的代码会直接影响运行进程的性能,但此处讨论的性能问题则涉及共享库的运行时处理。本节通过考虑各个方面(如文本大小和纯度)以及重定位开销,更详细地介绍了这种处理。
分析文件
有多种工具可用来分析 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 选项
对于 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 = 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 = 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 选项。请参见缩减符号范围。使用 -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
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] .... 特殊名称 指示要从正在配置的共享库的地址范围之外进行引用。因此,在上一示例中,从可执行文件,或者从其他共享库(正在进行配置分析时绑定到 libc)对 libc 中的 open(2) 函数进行了 1634 次调用。
-------------------------------------------------- ------------------------------
注
共享库配置具有多线程安全性,但以下情况除外:一个线程调用 fork(2),而另一个线程正在更新配置数据信息。可以使用 fork(2) 删除此限制。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/eroswang/archive/2007/12/31/2 006031.aspx
2010年09月07日
一个共享库可供同一系统中的多个应用程序使用。共享库的性能会影响使用此共享库的应用程序,并且会影响整个系统。
虽然共享库中的代码会直接影响运行进程的性能,但此处讨论的性能问题则涉及共享库的运行时处理。本节通过考虑各个方面(如文本大小和纯度)以及重定位开销,更详细地介绍了这种处理。
分析文件
有多种工具可用来分析 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 选项
对于 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 = 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 = 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 选项。请参见缩减符号范围。使用 -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
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] .... 特殊名称 指示要从正在配置的共享库的地址范围之外进行引用。因此,在上一示例中,从可执行文件,或者从其他共享库(正在进行配置分析时绑定到 libc)对 libc 中的 open(2) 函数进行了 1634 次调用。
-------------------------------------------------- ------------------------------
注
共享库配置具有多线程安全性,但以下情况除外:一个线程调用 fork(2),而另一个线程正在更新配置数据信息。可以使用 fork(2) 删除此限制。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/eroswang/archive/2007/12/31/2 006031.aspx