共享库载入时重定位

Posted wuhui_gdnt

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了共享库载入时重定位相关的知识,希望对你有一定的参考价值。

共享库载入时重定位

原作者: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应用程序),归结于共享库被载入的方式,需要重定位从这些共享库获取的符号。

载入共享库

不像可执行文件,在构建共享库时,链接器不能对它们的代码假设一个已知的载入地址。这样的原因很简单。每个程序可以使用任意多的共享库,没有一个简单的方法预先知道给定的共享库将被载入虚拟内存的什么位置。多年来,对这个问题发明了很多方法,但在本文里我只关注当前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的文章,我觉得以载入时重定位开始会更容易解释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有几个优势,特别在性能方面。其次,恕我直言,在没有预备知识时载入时重定位更容易理解,这使得将来解释PIC更容易。

无论动机如何,我希望本文能有助于揭开一点现代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页。

 

以上是关于共享库载入时重定位的主要内容,如果未能解决你的问题,请参考以下文章

Windows 程序启动性能优化(先载入EXE,后载入DLL,只取有限的代码载入内存,将CPU的IP指向程序的入口点)

操作系统 内存使用与分段--10

为啥共享库可以定位为一个用户,而不是另一个?

我可以让共享库构造函数在重定位之前执行吗?

静态库共享库

linux 静态库共享库