共享库加载时重定位

原作者: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页。

时间: 2024-10-13 12:00:09

共享库加载时重定位的相关文章

共享库载入时重定位

原作者:Eli Bendersky http://eli.thegreenplace.net/2011/08/25/load-time-relocation-of-shared-libraries 本文的目的是解释现代操作系统如何使得共享库载入时重定位成为可能.它关注运行在32位x86的LinuxOS,但通用的原则也适用于其他OS与CPU. 共享库有许多名字--共享库,共享对象,动态共享对象(DSO),动态链接库(DLL--如果你有Windows背景).为了统一起见,我将尽量在本文里使用"共享库

使用Redirector插件解决googleapis公共库加载的问题

最近访问一些面向国外的网站总是会出现ajax.googleaips.com无法加载的情况.以下为加载stackoverflow时的情境: 图1 -无法加载的google公共库 问题的原因是谷歌没有在国内开放ajax公共库服务 使用Redirector解决公共库加载问题 Redirector这个插件的功能正如它的名字, 可以对网页中的内容进行重定向 外部样式表,外部脚本, 图片等均可以添加至作用范围 安装 Redirector目前支持Firefox.Chrome.Opera三款浏览器,可以前往对应

ARCServer加载专题图不显示——前端加载时强行修改地图服务请求的坐标系

问题描述:这是比较奇葩的一个问题,发布的服务坐标系是2385的,但是通过前端加载代码加载后,得到的请求是4549的,显示不了地图 解决思路:通过调试窗口,查看发送的请求,发现其对应的空间参考是4549.所以应该修改此请求,但是用户放大缩小平移是自动发送的请求,一般来说,地图的空间参考是根据第一次加载的地图来确定的,因而,想到可以修改地图map的空间参考. 解决方法: map.on("load", function () {                map.spatialRefer

Linux程序动态库加载优化

作者:zhanhailiang 日期:2014-10-26 linux程序动态库加载流程简介 linux从程序(program或对象)变成进程(process或进程),简单说来需要经过三步: fork进程,在内核创建进程相关内核项,加载进程可执行文件: 查找依赖的.so,逐一加载映射虚拟地址: 初始化程序变量: 如下例通过strace查看pwd命令执行过程: [root@~/wade/codeReview/learningc]# strace pwd execve("/bin/pwd"

Example018主页加载时获取焦点

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>主页加载时获取焦点</title> </head> <body > <form action="" name="frm"> Username:<input type="

DLL动态加载时调用类成员函数小结

//dll 动态加载  调用 类 函数小结: 静态加载时,调用类成员函数,很简单.此次研究了下动态加载. 首先困难点:The first problem is that C++ member function names are decorated names (Specifying extern "C" does not help).The second problem is that C++ language specifications do not allow pointer

如何自动在html页面加载时动态改变div等元素的高度和宽度

这里需要用到jquery + css.原理是在页面加载时用javascript去动态改变一个class的高度和宽度.这样结合javascript能动态获取浏览器/页面的高度和宽度,从而使得div能动态的跟随浏览页面的大小变化而变化并且不影响高宽比.下面的代码创建一个手机页面,每一行三个图片分占33%,每个图片div的高和宽会随着浏览器的大小变化而自适应. <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"

Java误区: 静态代码块,会在类被加载时自动执行?

JAVA静态代码块会在类被加载时自动执行? 很多Java开发者的思想,被这个思想深深的轮奸了n遍,传播这个错误思想的博客,在网上一堆,越来越多的人被轮奸. 如:http://blog.csdn.net/leeyu35/article/details/7755304 那么我们程序来证明这句话是错误的: class MyClass1 { static {//静态块 System.out.println("static block "); } } public class Main { Cl

iOS 7.1的Safari为meta标签新增minimal-ui属性,在网页加载时隐藏地址栏与导航栏

iOS 7.1的Safari为meta标签新增minimal-ui属性,在网页加载时隐藏地址栏与导航栏 在过去,用 Safari 打开一个网页后是这样的: 是不是十分不爽?因为顶部的地址栏与底部的导航栏让页面显得非常拥挤,所以用户常常会马上伸手触屏滑动让它们隐藏起来. 始终强调人性化设计的苹果当然注意到了这点,于是在 iOS 7.1 的 Safari 中为 meta 标签新增 minimal-ui 属性,让网页在加载时便可隐藏顶部的地址栏与底部的导航栏. 如何实现?你只需将“minimal-ui