共享库通常使用与位置无关的代码生成。对此类型代码的外部数据项的引用通过一组表实现间接寻址。 有关更多详细信息,请参见与位置无关的代码。在运行时,将使用数据项的实际地址更新这些表。使用这些已更新的表,无需修改代码本身即可访问数据。
但是,动态可执行文件通常并不使用与位置无关的代码创建。它们所执行的任何外部数据引用看似只能在运行时通过修改执行引用的代码来实现。应避免修改只读文本段。可以使用复制重定位技术来解决此引用。
假设使用链接编辑器创建动态可执行文件,并且发现对数据项的引用位于其中一个相关共享库中。将在动态可执行文件的 .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) 显示此动态可执行文件将复制此共享库必须提供的所有数据,但只接受其已分配空间所允许的数据量。
通过根据与位置无关的代码生成应用程序可以删除复制重定位。请参见与位置无关的代码。