首页 > 代码库 > 共享库加载时重定位

共享库加载时重定位

共享库加载时重定位

原作者:Eli Bendersky

http://eli.thegreenplace.net/2011/08/25/load-time-relocation-of-shared-libraries

本文的目的是解释现代操作系统怎样使得共享库加载时重定位成为可能。它关注执行在32位x86的LinuxOS。但通用的原则也适用于其它OS与CPU。

共享库有很多名字——共享库,共享对象,动态共享对象(DSO),动态链接库(DLL——假设你有Windows背景)。为了统一起见。我将尽量在本文里使用“共享库”这个名字。

加载可运行文件

Linux,类似于其它支持虚拟内存的OS。将可运行文件加载固定地址。假设我们随机检查某些可运行文件的ELF头,我们将看到一个入口点地址:

$ readelf -h /usr/bin/uptime

ELF Header:

  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 0000

  Class:                             ELF32

  [...] some header fields

  Entry pointaddress:               0x8048470

  [...] some header fields

这是由链接器放置来告诉OS在哪里開始运行该可运行文件的代码[1]

而假设我们使用GDB加载该可运行文件并检查地址0x804870。我们确实将看到该可运行文件.text节的第一条指令。

这意味着在链接可运行文件时,链接器能够将全部内部符号引用(函数及数据)全然解析到固定及终于的位置。链接器自己运行一些重定位[2]。终于产生的输出不包括不论什么重定位。

真的吗?注意到在前一段我强调内部。仅仅要该可运行文件不须要共享库[3],它不须要重定位。但假设它确实使用了共享库(就像绝大多数Linux应用程序),归结于共享库被加载的方式,须要重定位从这些共享库获取的符号。

加载共享库

不像可运行文件。在构建共享库时。链接器不能对它们的代码如果一个已知的加载地址。这种原因非常easy。每一个程序能够使用随意多的共享库,没有一个简单的方法预先知道给定的共享库将被加载虚拟内存的什么位置。

多年来,对这个问题发明了非常多方法,但在本文里我仅仅关注当前Linux使用的方法。

只是首先让我们简要地检查这个问题。

这里是一个C样例代码[4],我将它编译为一个共享库:

int myglob =42;

 

intml_func(int a,int b)

{

    myglob += a;

    return b + myglob;

}

注意ml_func怎样几次訪问myglob。

在编译为x86汇编时。这将涉及一条mov指令将myglob的值从内存位置加载寄存器。Mov要求绝对地址——这样链接器怎样知道它放在哪个地址?答案是——它不知道。正如我之前提到的,共享库没有提前定义的加载地址——这将在执行时确定。

在Linux里,动态加载器[5]是一段为准备执行的程序做准备的代码。它的当中一个任务是在执行程序要求时。将共享库从硬盘加载内存。

在一个共享库被加载内存后,依据新确定的加载地址调整它。解决前一段提到的问题是动态加载器的工作。

在Linux ELF共享库里,解决问题有两个基本的途径:

1.      加载时重定位

2.      位置无关代码(PIC)

虽然PIC更通用且是如今推荐方案,在本文我将关注加载时重定位。最后我计划涵盖这两个方法。写一篇单独关于PIC的文章,我认为以加载时重定位開始会更easy解释PIC。

加载时重定位链接的共享库

要创建加载时重定位的共享库,我将不使用-fPIC标记进行编译(否则将触发生成PIC):

gcc -g -c ml_main.c -o ml_mainreloc.o

gcc -shared -o libmlreloc.so ml_mainreloc.o

看到第一个有趣的事是libmlreloc.so的入口:

$ readelf -h libmlreloc.so

ELF Header:

  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 0000

  Class:                             ELF32

  [...] some header fields

  Entry pointaddress:               0x3b0

  [...] some header fields

为了简单起见,链接器知道加载器会到处移动这个共享对象。因此仅仅是从地址0x0链接它(.text节在0x3b0处開始)。记住这个事实——在本文后面这是实用的。

如今让我们看一下这个共享库的汇编。关注ml_func:

$ objdump -d -Mintel libmlreloc.so

 

libmlreloc.so:     fileformat elf32-i386

 

[...] skipping stuff

 

0000046c <ml_func>:

 46c: 55                      push   ebp

 46d: 89 e5                   mov    ebp,esp

 46f: a1 00 00 00 00          mov   eax,ds:0x0

 474: 03 45 08                add    eax,DWORD PTR [ebp+0x8]

 477: a3 00 00 00 00          mov   ds:0x0,eax

 47c: a1 00 00 00 00          mov   eax,ds:0x0

 481: 03 45 0c                add    eax,DWORD PTR [ebp+0xc]

 484: 5d                      pop    ebp

 485: c3                      ret

 

[...] skipping stuff

在作为prologue部分的头两条指令后[6]。我们看到myglob+= a的编译后结果[7]。从内存将myglob的值提取到eax,加上a(它在ebp+0x8),然后放回内存。

但等一下,mov获取了myglob?为什么?看起来mov实际的操作数仅仅是0x0[8]。出了什么事?链接器将一些暂时的提前定义值(这里是0x0)放入指令流,然后创建一个特殊的重定位项指向这个位置。让我们检查一下这个共享库的重定位项:

$ readelf -r libmlreloc.so

 

Relocation section ‘.rel.dyn‘ at offset 0x2fc contains 7entries:

 Offset     Info   Type            Sym.Value  Sym. Name

00002008  00000008R_386_RELATIVE

00000470  00000401R_386_32          0000200C   myglob

00000478  00000401R_386_32          0000200C   myglob

0000047d  00000401R_386_32          0000200C   myglob

[...] skipping stuff

ELF的rel.dyn节保留给动态(加载时)重定位。由动态加载器使用。在上面显示的节里myglob有3个重定位项,由于在反汇编代码里对myglob有3个引用。

让我们解释第一个。

它说:去到这个目标文件(共享库)偏移0x470处,对符号myglob应用R_386_32类型的重定位。

假设我们查询ELF规范,看到R_386_32类型重定位表示:在重定位项中获取指定偏移的值,加上符号的地址,并把它置入偏移。

在该目标文件偏移0x470处我们有什么?回顾ml_func反汇编代码的指令:

46f:  a1 00 00 00 00          mov   eax,ds:0x0

 

a1编码了指令mov,因此它的操作数在下一个地址。即0x470。

这是在反汇编代码里我们看到的0x0。因此回到重定位项,如今我们明确它说:将myglob的地址加上mov指令的操作数。换句话说它告诉动态加载器——一旦你运行实际的地址分配,将myglob的真实地址放入0x470,然后将正确的符号值替换mov的操作数。简洁,对吧?

注意重定位节的“Sym.value”列。对myglob它包括0x200C。这是myglob在这个共享库的虚拟内存映像里的偏移(还记得吗,链接器假定这个共享库在0x0处加载)。也能够通过查看这个库的符号表来检查这个值。比方使用nm:

$ nm libmlreloc.so

[...] skipping stuff

0000200c D myglob

这个输出也提供了myglob在这个库里的偏移。D表示该符号在初始化数据节(.data)。

执行中的加载时重定位

要看执行中的加载时重定位,我将使用来自一个简单启动可执行文件的共享库。

在执行这个可执行文件时,OS将加载该共享库并正确地重定位它。

有趣的是,由于Linux中启用的地址空间布局的随机化,尾随重定位相对困难。由于每次执行这个可执行文件,共享库libmreloc.so被放在不同的虚拟内存地址[9]。

只是这是一个相当弱的限制。这一切有一个方法让它变得合理。但首先,让我们讨论我们共享库包括的段:

$ readelf --segments libmlreloc.so

 

Elf file type is DYN (Shared object file)

Entry point 0x3b0

There are 6 program headers, starting at offset 52

 

Program Headers:

  Type           Offset   VirtAddr  PhysAddr   FileSiz MemSiz  Flg Align

  LOAD           0x000000 0x00000000 0x000000000x004e8 0x004e8 R E 0x1000

  LOAD           0x000f04 0x00001f04 0x00001f040x0010c 0x00114 RW  0x1000

  DYNAMIC        0x000f18 0x00001f18 0x00001f18 0x000d00x000d0 RW  0x4

  NOTE           0x0000f4 0x000000f4 0x000000f40x00024 0x00024 R   0x4

  GNU_STACK      0x000000 0x00000000 0x00000000 0x000000x00000 RW  0x4

  GNU_RELRO      0x000f04 0x00001f04 0x00001f04 0x000fc0x000fc R   0x1

 

 Section to Segmentmapping:

  Segment Sections...

   00     .note.gnu.build-id .hash .gnu.hash .dynsym.dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini.eh_frame

   01     .ctors .dtors .jcr .dynamic .got .got.plt.data .bss

   02     .dynamic

   03     .note.gnu.build-id

   04

   05     .ctors .dtors .jcr .dynamic .got

要追踪符号myglob,我们感兴趣的是这里列出的第二个段。

注意这些东西:

·        在底部的节到段的映射里。段01声称包括.data节,它是myglob的大本营

·        VirAddr列指明第二个段在0x1f04開始且大小为0x10c,表示它一直扩展到0x2010,因此包括0x200C处的myglob。

如今使用Linux提供给我们的一个好用的工具来检查加载时链接过程——dl_iterate_phdr方法。它同意应用程序在执行时查询加载了那些共享库。并且更重要的是——窥探它们的程序头。

因此我准备将以下的代码写入driver.c:

#define _GNU_SOURCE

#include <link.h>

#include <stdlib.h>

#include <stdio.h>

 

 

staticintheader_handler(struct dl_phdr_info* info, size_t size,void* data)

{

    printf("name=%s (%d segments) address=%p\n",

           info->dlpi_name, info->dlpi_phnum, (void*)info->dlpi_addr);

    for (int j =0; j <info->dlpi_phnum; j++) {

         printf("\t\t header %2d: address=%10p\n", j,

             (void*) (info->dlpi_addr+ info->dlpi_phdr[j].p_vaddr));

         printf("\t\t\t type=%u, flags=0x%X\n",

                info->dlpi_phdr[j].p_type, info->dlpi_phdr[j].p_flags);

    }

    printf("\n");

    return0;

}

 

 

externint ml_func(int,int);

 

 

intmain(int argc,constchar* argv[])

{

   dl_iterate_phdr(header_handler, NULL);

 

    int t = ml_func(argc,argc);

    return t;

}

header_handler实现了dl_iterate_phdr的回调。

对全部的库它都将得到调用,并报告它们的名字及加载地址,连同它们全部的段。它还会调用ml_func,这种方法来自libmlreloc.so共享库。

要以我们的共享库编译并链接driver。执行:

gcc -g -c driver.c -o driver.o

gcc -o driver driver.o -L. -lmlreloc

单独执行driver我们会得到信息,但每次执行的地址都是不同的。因此我要做的就是在gdb下执行它[10]。看它说了什么,然后使用gdb进一步查询进程的地址空间:

$ gdb -q driver

 Reading symbols fromdriver...done.

 (gdb) b driver.c:31

 Breakpoint 1 at0x804869e: file driver.c, line 31.

 (gdb) r

 Starting program: driver

 [...] skipping output

 name=./libmlreloc.so (6segments) address=0x12e000

               header  0: address=  0x12e000

                       type=1, flags=0x5

                header  1: address= 0x12ff04

                       type=1, flags=0x6

               header  2: address=  0x12ff18

                       type=2, flags=0x6

               header  3: address=  0x12e0f4

                       type=4, flags=0x4

               header  4: address=  0x12e000

                       type=1685382481, flags=0x6

               header  5: address=  0x12ff04

                       type=1685382482, flags=0x4

 

[...] skipping output

 Breakpoint 1, main(argc=1, argv=0xbffff3d4) at driver.c:31

 31    }

 (gdb)

由于driver报告它加载的全部库(甚至隐含的库,像libc或动态加载器自身),输出非常长。我将仅仅关注libmlreloc.so的报告。注意这6个段与readelf报告的同样,但这次重定位到了它们终于的内存位置。

让我们做一点算术。

输出说libmlreloc.so放在了虚拟地址0x12e000。

我们对第二个段感兴趣,正如我们在readelf中看到的,它在偏移0x1f04。

确实。在输出中我们看到它被加载到地址0x12ff04。由于myglob在文件的偏移为0x200C,我们期望它如今在地址0x13000C。

好,让我们问问GDB:

 (gdb) p &myglob

$1 = (int *) 0x13000c

棒极了!只是訪问myglob的dmml_func又怎么样了呢?再问问GDB:

 (gdb) setdisassembly-flavor intel

(gdb) disas ml_func

Dump of assembler code for function ml_func:

   0x0012e46c<+0>:   push   ebp

   0x0012e46d<+1>:   mov    ebp,esp

   0x0012e46f<+3>:   mov    eax,ds:0x13000c

   0x0012e474<+8>:   add    eax,DWORD PTR [ebp+0x8]

   0x0012e477<+11>:  mov    ds:0x13000c,eax

   0x0012e47c<+16>:  mov    eax,ds:0x13000c

   0x0012e481<+21>:  add    eax,DWORD PTR [ebp+0xc]

   0x0012e484<+24>:  pop    ebp

   0x0012e485<+25>:  ret

End of assembler dump.

正如期望的,myglob的真实地址放入了全部訪问它的mov指令里。就像重定位项指出的那样。

重定位函数调用

到眼下为止本文展示了数据訪问的重定位——以全局变量myglob的使用为例。还有一个须要重定位的是代码訪问——也就是函数调用。本节简要介绍这怎么做到。节奏要比本文的其它部分要快得多,由于我如今假定读者已经明确了重定位是什么。

言归正传。让我们開始吧。我已经将共享库的代码改动例如以下:

int myglob =42;

 

intml_util_func(int a)

{

    return a +1;

}

 

intml_func(int a,int b)

{

    int c = b +ml_util_func(a);

    myglob += c;

    return b + myglob;

}

加入了由ml_func使用的ml_util_func。以下是完毕链接的共享库里ml_func的反汇编代码:

000004a7 <ml_func>:

 4a7:   55                      push   ebp

 4a8:   89 e5                   mov    ebp,esp

 4aa:   83 ec 14                sub    esp,0x14

 4ad:   8b 45 08                mov    eax,DWORD PTR [ebp+0x8]

 4b0:   89 04 24                mov    DWORD PTR [esp],eax

 4b3:   e8 fc ff ff ff          call  4b4 <ml_func+0xd>

 4b8:   03 45 0c                add    eax,DWORD PTR [ebp+0xc]

 4bb:   89 45 fc                mov    DWORD PTR [ebp-0x4],eax

 4be:   a1 00 00 00 00          mov   eax,ds:0x0

 4c3:   03 45 fc                add    eax,DWORD PTR [ebp-0x4]

 4c6:   a3 00 00 00 00          mov   ds:0x0,eax

 4cb:   a1 00 00 00 00          mov   eax,ds:0x0

 4d0:   03 45 0c                add    eax,DWORD PTR [ebp+0xc]

 4d3:   c9                      leave

 4d4:   c3                      ret

这里有趣的是地址0x4b3处的指令——它是对ml_util_func的调用。让我们分解它:

e8是call的操作码。这个call的參数是相对于下一条指令的偏移。在上面的反汇编代码里,这个參数是0xfffffffc,或-4。

因此call当前指向自己。这显然是不正确的——但不要忘记重定位。

以下是共享库重定位节如今的样子:

$ readelf -r libmlreloc.so

 

Relocation section ‘.rel.dyn‘ at offset 0x324 contains 8entries:

 Offset     Info   Type            Sym.Value  Sym. Name

00002008  00000008R_386_RELATIVE

000004b4  00000502 R_386_PC32        0000049c   ml_util_func

000004bf  00000401R_386_32          0000200c   myglob

000004c7  00000401R_386_32          0000200c   myglob

000004cc  00000401R_386_32          0000200c   myglob

[...] skipping stuff

假设将它与前面readelf –r调用比較。我们会注意到为ml_util_func加入了一个新的项。

这个项指向call指令參数的地址0x4b4,而且它的类型是R_386_PC32。与R_386_32相比,这个重定位类型更复杂。但不是复杂得太多。

它表示:获取项中指定偏移处的值,加上符号的地址。减去偏移地址本身。把它放回偏移处的内存字。记住这个重定位是在加载时完毕的。那时符号及被重定位偏移本身的最后加载地址都是已知的。这些终于地址參与这个计算。

这由什么作用?基本上,它是相对重定位,考虑了它的位置,因此适用于相对寻址的指令參数(e8call就是)。

我保证一旦我们得到真实的数字,这会变得更清楚。

我如今准备再次编译driver代码并在GDB下执行它。看这个重定位怎样工作。以下是GDB节。跟着解释:

$ gdb -q driver

 Reading symbols fromdriver...done.

 (gdb) b driver.c:31

 Breakpoint 1 at0x804869e: file driver.c, line 31.

 (gdb) r

 Starting program: driver

 [...] skipping output

 name=./libmlreloc.so (6segments) address=0x12e000

               header  0: address= 0x12e000

                      type=1, flags=0x5

               header  1: address= 0x12ff04

                      type=1, flags=0x6

               header  2: address= 0x12ff18

                      type=2, flags=0x6

               header  3: address= 0x12e0f4

                      type=4, flags=0x4

               header  4: address= 0x12e000

                      type=1685382481, flags=0x6

               header  5: address= 0x12ff04

                      type=1685382482, flags=0x4

 

[...] skipping output

Breakpoint 1, main (argc=1, argv=0xbffff3d4) at driver.c:31

31    }

(gdb)  setdisassembly-flavor intel

(gdb) disas ml_util_func

Dump of assembler code for function ml_util_func:

   0x0012e49c<+0>:   push   ebp

   0x0012e49d<+1>:   mov    ebp,esp

   0x0012e49f<+3>:   mov    eax,DWORD PTR [ebp+0x8]

   0x0012e4a2<+6>:   add    eax,0x1

   0x0012e4a5<+9>:   pop    ebp

   0x0012e4a6<+10>:  ret

End of assembler dump.

(gdb) disas /r ml_func

Dump of assembler code for function ml_func:

   0x0012e4a7<+0>:    55     push  ebp

   0x0012e4a8<+1>:    89 e5  mov   ebp,esp

   0x0012e4aa<+3>:    83 ec 14       sub   esp,0x14

   0x0012e4ad <+6>:    8b 45 08       mov   eax,DWORD PTR [ebp+0x8]

   0x0012e4b0<+9>:    89 04 24       mov   DWORD PTR [esp],eax

   0x0012e4b3<+12>:   e8 e4 ff ff ff call   0x12e49c <ml_util_func>

   0x0012e4b8<+17>:   03 45 0c       add   eax,DWORD PTR [ebp+0xc]

   0x0012e4bb <+20>:   89 45 fc       mov   DWORD PTR [ebp-0x4],eax

   0x0012e4be<+23>:   a1 0c 00 13 00 mov    eax,ds:0x13000c

   0x0012e4c3<+28>:   03 45 fc       add   eax,DWORD PTR [ebp-0x4]

   0x0012e4c6<+31>:   a3 0c 00 13 00 mov    ds:0x13000c,eax

   0x0012e4cb<+36>:   a1 0c 00 13 00 mov    eax,ds:0x13000c

   0x0012e4d0<+41>:   03 45 0c       add   eax,DWORD PTR [ebp+0xc]

   0x0012e4d3<+44>:   c9     leave

   0x0012e4d4<+45>:   c3     ret

End of assembler dump.

(gdb)

这里重要的部分是:

1.      在driver的输出里我们看到libmlreloc.so第一个段(代码段)被映射到0x12e000[11]

2.      ml_util_func被加载地址0x0012e49c

3.      被重定位偏移的地址是0x0012e4b4

4.      0xfffffffe4被填充到ml_func里对ml_util_func调用的參数里(我以/r选项反汇编ml_func,在反汇编代码之外,显示原始的16进制数),它被解释为到ml_util_func的正确偏移。

显然我们最感兴趣4是怎样完毕的。

又到了做数学的时间。

如上述解释R_386_PC32重定位,我们有:

获取在项指定偏移处的值(0xfffffffc)。加上符号的地址(0x0023e49c),减去偏移本身的地址(0x0012e4b4)。把它放回偏移处的内存字。当然,全部这一切都如果使用32位2进制补码完毕。结果是0xffffffe4,正如预期。

额外的学分:为什么须要调用重定位?

这是一个讨论Linux中共享库加载实现的某些独特性的“奖励”章节。

假设你仅仅希望理解重定位怎样完毕。你全然能够跳过它。

在尝试理解ml_util_func的重定位时,我必须承认我为此挠头了一阵。回顾call的參数是相对偏移。

当然call与ml_util_func之间的偏移在加载库时是不会改变的——它们都在代码段里作为一个总体移动。

这样为什么须要这个重定位呢?

这是一个小的实验尝试:回到共享库的代码,向ml_util_func声明加入static。又一次编译看一下readelf–r的输出。

做完了?我会揭晓结果——重定位不见了!

检查ml_func的反汇编代码——如今一个正确的偏移被设置为call的參数——不须要重定位。

发生了什么?

在将全局符号引用绑定到它们实际的定义时,关于查找哪些共享库,动态加载器有某些规则。

用户也能够通过设置LD_PRELOAD环境变量来影响这个次序。

这里要涉及太多细节,因此如果你真正感兴趣你能够看一下ELF标准。动态加载器的man页以及google一下。

只是简而言之,当ml_util_func是全局时,它可能在该可运行文件或其它共享库里被覆盖,因此当链接我们的共享库时。链接器不能如果偏移是已知的而且写死它[12]。链接器使得对全局符号的全部訪问都是可重定位的,以同意动态加载器决定怎样解析它们。

这是为什么将函数声明为static会不同——由于它不再是全局或导出的。链接器能够在代码里写死它的偏移。

额外的学分#2:从可运行文件訪问共享库数据

相同。这是讨论一个进阶议题的“奖励”章节。假设你已经厌倦了,你能够跳过它。

在上面的样例里。myglob仅在共享库内部使用。假设我们从程序(driver.c)訪问它会发生什么?毕竟,myglob是一个全局变量,因此外部可见。

让我们将driver.c改动例如以下(注意我删除了段的遍历代码):

#include <stdio.h>

 

externint ml_func(int,int);

externint myglob;

 

intmain(int argc,constchar* argv[])

{

    printf("addr myglob = %p\n", (void*)&myglob);

    int t = ml_func(argc,argc);

    return t;

}

如今它打印出myglob的地址。

输出是:

addr myglob = 0x804a018

等一下。有一些东西这里没有计算。难道myglob不是在共享库地址空间里吗?0x804xxxxx看起来像程序地址空间。发生了什么?

回顾程序/可运行文件是不可重定位的,因此它的数据地址必须在链接时绑定。因此,链接器必须创建在程序地址空间里变量的拷贝,动态加载器将使用它作为重定位地址。

这类似于之前章节里的讨论——在某种意义上。在主程序里的myglob覆盖了共享库里的对象,依据全局符号查找规则,它替代了共享库的对象。假设我们在GDB里检查ml_func,我们将看到对myglob的正确訪问。

0x0012e48e <+23>:     a1 18 a0 04 08 mov   eax,ds:0x804a018

这非常合理,由于myglob的R_386_32重定位仍然存在于libmlreloc.so里,动态加载器使它指向myglob现存的正确位置。

这非常不错,但遗漏了一些东西。Myglob是在共享库里初始化(为42)的——这个初始化值怎样跑到程序地址空间里的?原来链接器为程序生成了一个特殊的重定位项(眼下为止我们仅在共享库里检查重定位项):

$ readelf -r driver

 

Relocation section ‘.rel.dyn‘ at offset 0x3c0 contains 2entries:

 Offset     Info   Type            Sym.Value  Sym. Name

08049ff0  00000206R_386_GLOB_DAT    00000000   __gmon_start__

0804a018  00000605R_386_COPY        0804a018   myglob

[...] skipping stuff

注意myglob的R_386_COPY重定位。它仅仅是表示:从符号地址处将值复制到这个偏移。

这在加载共享库时由动态加载器运行。它怎么知道要拷贝多少呢?符号表节包括了每一个符号的大小;比如在libmlreloc.so的.symtab节里myglob的大小是4。

我认为这是一个相当酷的样例。显示了可运行文件的链接与加载的过程怎样被精心安排。

链接器在输出里放入特殊的指令让加载器使用、运行。

总结

加载时重定位是Linux(及其它OS)用来解决。在将共享库加载内存时。在共享库里訪问内部数据与代码的问题。

时至今日,位置无关代码(PIC)是一个更流行的方法。一些现代系统(比方x86-64)已不再支持加载时重定位。

仍然,出于两个原因我决定写一篇关于加载时重定位的文章。

首先,在某些系统上加载时重定位对PIC有几个优势。特别在性能方面。其次,恕我直言,在没有预备知识时加载时重定位更easy理解。这使得将来解释PIC更easy。

不管动机怎样,我希望本文能有助于揭开一点现代OS中链接与加载共享库幕后的神奇面纱。

 



[1] 关于这个入口点的很多其它信息,參考这篇文章的 “离题 – 进程地址与入口点”一节。

[2] 链接时重定位发生在将多个目标文件合并到一个可运行文件(或共享库时)。它涉及解析目标文件间大量的重定位。相比加载时重定位,链接时重定位是一个更复杂的议题。我不会在本文里讨论它。

[3] 能够通过将你全部的库编译为静态库做到这一点(使用ar合并目标文件。而不是gcc –shared),并在链接可运行文件时向gcc提供-static——避免链接libc的共享库版本号。

[4] ml仅仅是代表“我的库”。

类似的,代码本身没有什么意义,仅用作展示的目的。

[5] 也称为“动态链接器”。它本身是一个共享对象(虽然它也能够作为可执行文件执行)。存身于/lib/ld-linux.so.2(最后一个数字是SO版本号,可能会不同)。

[6] 假设你不熟悉x86架构怎样组织它的栈帧,是时候看这篇文章了。.

[7] 你能够向objdump提供-l选项在汇编里加入C源码行,能够更清楚地知道什么编译成什么。

这里我忽略它是为了缩短篇幅。

[8] 我观察objdump输出的左側,那里是原始的内存字节。

a1 00 00 00 00表示mov将操作数0x0移动到eax。该操作数被反汇编器解释为ds:0x0。

[9] 因此在这个可执行文件上调用的ldd在每次执行时将报告不同的加载地址。

[10] 有经验的读者可能注意到我能够i shared询问GDB来得到共享库的加载地址。只是,i shared仅是指整个库的加载地址(或更准确些,它的入口点),而我的兴趣在段。

[11] 什么,又是0x12e000?我不是刚说过加载地址随机化吗? 原来出于调试目的。能够被操控动态加载器关闭这个特性。

这正是GDB的行为。

[12] 除非传入-Bsymbolic选项。參考ld的man页。

 

共享库加载时重定位