JavaScript is required to for searching.
跳过导航链接
退出打印视图
链接程序和库指南     Oracle Solaris 10 8/11 Information Library (简体中文)
search filter icon
search icon

文档信息

前言

1.  Oracle Solaris 链接编辑器介绍

2.  链接编辑器

3.  运行时链接程序

4.  共享目标文件

命名约定

记录共享目标文件名称

在归档中包含共享目标文件

已记录名称冲突

具有依赖项的共享目标文件

依赖项排序

作为过滤器的共享目标文件

生成标准过滤器

生成辅助过滤器

过滤组合

filtee 处理

性能注意事项

使用 elfdump 分析文件

底层系统

延迟装入动态依赖项

与位置无关的代码

SPARC: -K pic-K PIC 选项

删除未使用的材料

最大化可共享性

将只读数据移动到文本中

折叠多重定义数据

使用自动变量

动态分配缓冲区

最小化分页活动

重定位

符号查找

执行重定位的时间

组合重定位节

复制重定位

使用 -B symbolic 选项

配置共享目标文件

5.  应用程序二进制接口与版本控制

6.  支持接口

7.  目标文件格式

8.  线程局部存储

9.  Mapfile

A.  链接编辑器快速参考

B.  版本控制快速参考

C.  使用动态字符串标记建立依赖性

D.  直接绑定

E.  System V 发行版 4(版本 1)Mapfile

F.  链接程序和库的更新及新增功能

索引

性能注意事项

一个共享目标文件可供同一系统中的多个应用程序使用。共享目标文件的性能会影响使用此共享目标文件的应用程序,并且会影响整个系统。

虽然共享目标文件中的代码会直接影响运行进程的性能,但此处讨论的性能问题则涉及共享目标文件的运行时处理。本节通过考虑各个方面(如文本大小和纯度)以及重定位开销,更详细地介绍了这种处理。

使用 elfdump 分析文件

可以使用各种工具分析 ELF 文件的内容,包括标准的 Unix 实用程序 dump(1)nm(1)size(1)。在 Oracle Solaris 中,这些工具大都已被 elfdump(1) 取代。

ELF 格式可以将数据组织到多个 中。各节会被依次指定给称为的单元。某些段可以描述如何将文件的各部分映射到内存。请参见 mmap(2)。对它使用 elfdump(1) 命令并检查 LOAD 项,可以显示这些可装入段。

$ elfdump -p -NPT_LOAD libfoo.so.1
Program Header[0]:
    p_vaddr:      0           p_flags:    [ PF_X PF_R ]
    p_paddr:      0           p_type:     [ PT_LOAD ]
    p_filesz:     0x53c       p_memsz:    0x53c
    p_offset:     0           p_align:    0x10000

Program Header[1]:
    p_vaddr:      0x1053c     p_flags:    [ PF_X PF_W PF_R ]
    p_paddr:      0           p_type:     [ PT_LOAD ]
    p_filesz:     0x114       p_memsz:    0x13c
    p_offset:     0x53c       p_align:    0x10000
....

libfoo.so.1 共享目标文件中存在两种可装入段,通常称为文本段和数据段。文本段的映射的目的是允许读取并执行其内容 PF_X PF_R。数据段映射的目的是允许同时修改其内容 PF_W。数据段的内存大小 p_memsz 不同于文件大小 p_filesz。该差异说明存在 .bss 节,此节属于数据段并在装入数据段时动态创建。

程序员通常根据定义其代码中的函数和数据元素的符号来考虑文件。通过在 elfdump 命令中使用 -s 选项可显示这些符号。例如:

$ elfdump -sN.symtab libfoo.so.1

Symbol Table Section:  .symtab
     index    value      size      type bind oth ver shndx          name
      ....
      [36]  0x00010628 0x00000028  OBJT GLOB  D    0 .data          data
      ....
      [38]  0x00010650 0x00000028  OBJT GLOB  D    0 .bss           bss

      ....
      [40]  0x00000520 0x0000000c  FUNC GLOB  D    0 .init          _init
      ....
      [44]  0x00000508 0x00000014  FUNC GLOB  D    0 .text          foo
      ....
      [46]  0x0000052c 0x0000000c  FUNC GLOB  D    0 .fini          _fini

elfdump 显示的符号表信息包括与符号关联的节。elfdump -c 选项可用于显示与这些节有关的信息。

$ elfdump -c libfoo.so.1
....
Section Header[6]:  sh_name: .text
    sh_addr:      0x4f8           sh_flags:   [ SHF_ALLOC SHF_EXECINSTR ]
    sh_size:      0x28            sh_type:    [ SHT_PROGBITS ]
    sh_offset:    0x4f8           sh_entsize: 0
    sh_link:      0               sh_info:    0
    sh_addralign: 0x8       

Section Header[7]:  sh_name: .init
    sh_addr:      0x520           sh_flags:   [ SHF_ALLOC SHF_EXECINSTR ]
    sh_size:      0xc             sh_type:    [ SHT_PROGBITS ]
    sh_offset:    0x520           sh_entsize: 0
    sh_link:      0               sh_info:    0
    sh_addralign: 0x4       

Section Header[8]:  sh_name: .fini
    sh_addr:      0x52c           sh_flags:   [ SHF_ALLOC SHF_EXECINSTR ]
    sh_size:      0xc             sh_type:    [ SHT_PROGBITS ]
    sh_offset:    0x52c           sh_entsize: 0
    sh_link:      0               sh_info:    0
    sh_addralign: 0x4       
....
Section Header[12]:  sh_name: .data
    sh_addr:      0x10628         sh_flags:   [ SHF_WRITE SHF_ALLOC ]
    sh_size:      0x28            sh_type:    [ SHT_PROGBITS ]
    sh_offset:    0x628           sh_entsize: 0
    sh_link:      0               sh_info:    0
    sh_addralign: 0x4       
....
Section Header[14]:  sh_name: .bss
    sh_addr:      0x10650         sh_flags:   [ SHF_WRITE SHF_ALLOC ]
    sh_size:      0x28            sh_type:    [ SHT_NOBITS ]
    sh_offset:    0x650           sh_entsize: 0
    sh_link:      0               sh_info:    0
    sh_addralign: 0x4       
....

以上示例中的 elfdump(1) 输出显示 _initfoo_fini 函数与 .init.text.fini 节的关联关系。这些节由于具有只读性质,因此属于文本段。

同样,数据数组 databss 分别与节 .data.bss 关联。这些节由于具有可写性质,因此属于数据段。

底层系统

使用共享目标文件生成应用程序时,会在运行时将此目标文件的全部可装入内容映射到此进程的虚拟地址空间。每个使用共享目标文件的进程通过引用内存中此共享目标文件的单个副本来启动。

处理共享目标文件中的重定位以将符号引用绑定到相应的定义。这会导致计算那些无法在链接编辑器生成共享目标文件时得到的实际虚拟地址。通常,这些重定位会导致更新进程数据段中的项。

基于动态共享目标文件链接的内存管理方案将按照页粒度在各进程之间共享内存。只要在运行时未修改内存页,便可共享这些内存页。如果某个进程在写入数据项或在重定位对共享目标文件的引用时写入一个共享目标文件页,则会生成此页的专用副本。此专用副本不会影响此共享目标文件的其他用户。但是,其他进程无法共享此页。通过此方式修改的文本页称为不纯文本页。

映射到内存的共享目标文件中的段分为两种基本类别:只读的文本段和可读写的数据段。有关如何从 ELF 文件获取信息,请参见使用 elfdump 分析文件。开发共享目标文件时的最重要目标是最大化文本段以及最小化数据段。这样可优化代码共享量,同时减少初始化和使用共享目标文件所需的处理量。本节介绍有助于实现此目标的机制。

延迟装入动态依赖项

通过将共享目标文件建立为延迟可装入目标文件,可以延迟装入该共享目标文件依赖项,直到首次引用依赖项。请参见延迟装入动态依赖项

对于小型应用程序,典型的执行线程可以引用所有的应用程序依赖项。应用程序将装入所有的依赖项,而无论是否将这些依赖项定义为延迟可装入依赖项。但是,使用延迟装入,会使依赖项处理从进程启动一直延迟到整个进程执行过程。

对于具有许多依赖项的应用程序,延迟装入通常会导致根本没有装入某些依赖项。仅装入针对特定执行线程引用的依赖项。

与位置无关的代码

动态可执行文件中的代码通常与位置相关,并且与内存中的固定地址关联。相反,共享目标文件可装入不同进程中的不同地址。位置无关代码不与特定地址关联。这种无关性允许在每个使用此类代码的进程中的不同地址有效地执行代码。建议在创建共享目标文件时使用与位置无关的代码。

使用 -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 模式仍会导致无法在运行时解析的重定位。但是,这两种模式都需要运行时链接程序对文本段进行重定位。

无论运行时链接程序功能如何,也无论重定位要求的差异如何,共享目标文件都应该使用与位置无关的代码生成。

可以根据文本段确定需要重定位的共享目标文件。以下示例使用 elfdump(1) 确定是否存在 TEXTREL 项动态项。

$ cc -o libfoo.so.1 -G -R. foo.c
$ elfdump -d 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 文件生成了位置相关代码,因此将根据文本段生成两个重定位。如有可能,这些诊断会指明执行重定位所需的任何符号引用。在这种情况下,将根据符号 foobar 进行重定位。

如果包括手写汇编程序代码,但不包括相应的位置无关原型,则在共享目标文件中也会出现文本重定位。


注 - 可能需要使用一些简单的源文件进行实验,以确定启用位置无关性的编码序列。请使用编译器功能来生成中间汇编程序输出。


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 库包含许多无关函数。仅在极少数情况下,某个可执行文件才可能会使用此库中的所有函数。由于这些函数用途广泛,因此,确定实际上最常用的函数组也具有一定的难度。相反,刚开始设计共享目标文件时,只在此共享目标文件中维护相关函数。这样会改善引用的邻近性,并会产生减小目标文件总体大小的负面影响。

重定位

重定位处理中,介绍了运行时链接程序重定位动态可执行文件和共享目标文件以创建可运行进程所依据的机制。重定位符号查找执行重定位的时间将此重定位处理分为两类,以简化和帮助说明所涉及的机制。理论上,考虑重定位对性能的影响时也要区分这两种类别。

符号查找

当运行时链接程序需要查找符号时,缺省情况下它会搜索每个目标文件进行查找。运行时链接程序首先搜索动态可执行文件,然后按照共享目标文件的装入顺序搜索每个共享目标文件。在多数情况下,需要符号重定位的共享目标文件最终都是符号定义的提供者。

在这种情况下,如果不需要此重定位所用的符号成为共享目标文件接口的一部分,则首选将此符号转换为静态自动变量。还可以应用符号缩减以从共享目标文件接口中删除符号。有关更多详细信息,请参见缩减符号作用域。通过进行上述转换,链接编辑器在创建共享目标文件过程中,会产生针对这些符号处理符号重定位的开销。

应在共享目标文件中可见的全局数据项只是那些属于此共享目标文件用户接口的数据项。以前,这是要实现的硬性目标,因为通常将全局数据定义为允许从两个或多个位于不同源文件中的函数进行引用。通过应用符号缩减,可以删除不必要的全局符号。请参见缩减符号作用域。减少从共享目标文件导出的全局符号数会降低重定位成本,并全面改善性能。

在具有许多符号重定位和依赖项的动态进程中,使用直接绑定也可以显著降低符号查找开销。请参见附录 D

执行重定位的时间

在应用程序获得控制权之前,必须在进程初始化过程中执行所有立即引用重定位。但是,可以延迟所有延迟引用重定位,直到调用第一个函数实例。立即重定位通常由于数据引用而产生。因此,减少数据引用数也会缩短进程的运行时初始化时间。

将数据引用转换为函数引用也可延迟初始化重定位成本。例如,可以通过功能接口返回数据项。此转换通常会显著改善性能,因为初始化重定位成本有效分布在整个进程执行过程中。特定的进程调用可能从不调用某些功能接口,因此完全避免了这些接口的重定位开销。

复制重定位一节中介绍了使用功能接口的优势。该节介绍了一种在动态可执行文件与共享目标文件之间使用的开销稍大的特殊重定位机制。此外,还提供了如何避免此重定位开销的示例。

组合重定位节

可重定位目标文件中的重定位节通常与重定位必须应用到的节有一对一的关系。但是,当链接编辑器创建可执行文件或共享目标文件时,会将过程链接表重定位之外的所有重定位都放在名为 .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
$ elfdump -sN.dynsym prog | grep ' sys_'
      [24]  0x00021240 0x00000260  OBJT GLOB  D    1 .bss           sys_errlist
      [39]  0x00021230 0x00000004  OBJT GLOB  D    1 .bss           sys_nerr
$ elfdump -c prog
....
Section Header[19]:  sh_name: .bss
    sh_addr:      0x21230         sh_flags:   [ SHF_WRITE SHF_ALLOC ]
    sh_size:      0x270           sh_type:    [ SHT_NOBITS ]
    sh_offset:    0x1230          sh_entsize: 0
    sh_link:      0               sh_info:    0
    sh_addralign: 0x8
....
$ elfdump -r prog

Relocation Section:  .SUNW_reloc
    type                       offset     addend  section        symbol
....
  R_SPARC_COPY                0x21240          0  .SUNW_reloc    sys_errlist
  R_SPARC_COPY                0x21230          0  .SUNW_reloc    sys_nerr
....

链接编辑器已在动态可执行文件的 .bss 中分配了空间,以便接收由 sys_errlistsys_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 生成,并且已记录以下两个复制重定位。

$ cat foo.c
int _size_gets_smaller[16];
int _size_gets_larger[16];
$ cc -o libfoo.so -G foo.c
$ cc -o prog main.c -L. -R. -lfoo
$ elfdump -sN.symtab prog | grep _size
      [49]  0x000211d0 0x00000040  OBJT GLOB  D    0 .bss           _size_gets_larger
      [59]  0x00021190 0x00000040  OBJT GLOB  D    0 .bss           _size_gets_smaller
$ elfdump -r prog | grep _size
  R_SPARC_COPY                0x211d0          0  .SUNW_reloc    _size_gets_larger
  R_SPARC_COPY                0x21190          0  .SUNW_reloc    _size_gets_smaller

以下是此共享目标文件的新版本,其中包含这些符号的不同数据大小。

$ cat foo2.c
int _size_gets_smaller[4];
int _size_gets_larger[32];
$ cc -o libfoo.so -G foo2.c
$ elfdump -sN.symtab libfoo.so | grep _size
      [37]  0x000105cc 0x00000010  OBJT GLOB  D    0 .bss           _size_gets_smaller
      [41]  0x000105dc 0x00000080  OBJT GLOB  D    0 .bss           _size_gets_larger

针对此动态可执行文件运行 ldd(1) 将显示以下内容:

$ ldd -d prog
    libfoo.so.1 =>   ./libfoo.so.1
    ....
      relocation R_SPARC_COPY sizes differ: _size_gets_larger
          (file prog size=0x40; file ./libfoo.so size=0x80)
          prog size used; possible data truncation
      relocation R_SPARC_COPY sizes differ: _size_gets_smaller
          (file prog size=0x40; file ./libfoo.so size=0x10)
          ./libfoo.so size used; possible insufficient data copied
....

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     <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) 可取消这个限制。