从中可以看到,目标文件里的 .txt 段地址从 0 开始,其中地址为7的指令用于把参数 a 放到寄存器 %eax 中,而地址 a 处的指令则把 %eax 中的内容与 g_share 相加,注意这里 g_share 的地址为:0(%rip). 显然这个地址是错的,编译器当前并不知道 g_share 这个变量后会被分配到哪个地址上,因此在这儿只是随便用一个假的来代替,等着到接下来链接时,再把该处地址进行修正。那么,链接器怎么知道目标文件中哪些地方需要修正呢?很简单,编译器编译文件时时,会建立一系列表项,用来记录哪些地方需要在重定位时进行修正,这些表项叫作“重定位表”(relocatioin table):
  -bash-3.00$ objdump -r a.o
  a.o:     file format elf64-x86-64
  RELOCATION RECORDS FOR [.text]:
  OFFSET           TYPE              VALUE
  000000000000000c R_X86_64_PC32     g_share+0xfffffffffffffffc
  如上后一行,这条记录记录了在当前编译单元中,哪儿对 g_share 进行了引用,其中 offset 用于指明需要修改的位置在该段中的偏移,TYPE 则指明要怎样去修改,因为 cpu 的寻址方式不是的,寻址方式不同,地址的形式也有所不同,这个 type 用于指明怎么去修改, value 则是配合 type 来后计算该符号地址的。
  有了如上信息,链接器在把目标文件合并成一个可执行文件并分配好各段的加载地址后,可以重新计算那些需要重定位的符号的具体地址了, 如下我们可以看到在可执行文件中,对 g_share(0x40496处), g_func(0x4047a处)的访问已经被修改成了具体的地址:
-bash-3.00$ gcc -o am a.o main.o
-bash-3.00$ objdump -S am
// skip some of the ouput
extern int g_func(int a);
int main()
{
400468:       55                      push   %rbp
400469:       48 89 e5                mov    %rsp,%rbp
40046c:       48 83 ec 10             sub    $0x10,%rsp
int a = 42;
400470:       c7 45 fc 2a 00 00 00    movl   $0x2a,0xfffffffffffffffc(%rbp)
a = g_func(a);
400477:       8b 7d fc                mov    0xfffffffffffffffc(%rbp),%edi
40047a:       e8 0d 00 00 00          callq  40048c <g_func>
40047f:       89 45 fc                mov    %eax,0xfffffffffffffffc(%rbp)
return 0;
400482:       b8 00 00 00 00          mov    $0x0,%eax
}
400487:       c9                      leaveq
400488:       c3                      retq
400489:       90                      nop
40048a:       90                      nop
40048b:       90                      nop
000000000040048c <g_func>:
int g_share = 1;
int g_func(int a)
{
40048c:       55                      push   %rbp
40048d:       48 89 e5                mov    %rsp,%rbp
400490:       89 7d fc                mov    %edi,0xfffffffffffffffc(%rbp)
g_share += a;
400493:       8b 45 fc                mov    0xfffffffffffffffc(%rbp),%eax
400496:       01 05 dc 03 10 00       add    %eax,1049564(%rip)        # 500878 <g_share>
return a * 2;
40049c:       8b 45 fc                mov    0xfffffffffffffffc(%rbp),%eax
40049f:       01 c0                   add    %eax,%eax
}
4004a1:       c9                      leaveq
4004a2:       c3                      retq
// skip some of the ouput
  当然,重定位时修改指令的具体方式还牵涉到比较多的细节很?嗦,这里不细说了。
  加载时符号重定位
  前面描述了静态链接时,怎么解决符号重定位的问题,那么当我们使用动态链接来构建程序时,这些符号重定位问题是怎么解决的呢?目前来说,Linux 下 ELF 主要支持两种方式:加载时符号重定位及地址无关代码。地址无关代码接下来会讲,对于加载时重定位,其原理很简单,它与链接时重定位是一致的,只是把重定位的时机放到了动态库被加载到内存之后,由动态链接器来进行。
int g_share = 1;
int g_func(int a)
{
g_share += a;
return a * 2;
}
int g_func2()
{
int a = 2;
int b = g_func(3);
return a + b;
}
// compile on 32bit linux OS
-bash-3.00$ gcc -c a.c main.c
-bash-3.00$ gcc -shared -o liba.so a.o
-bash-3.00$ gcc -o am main.o -L. -la
-bash-3.00$ objdump -S liba.so
// skip some of the output
000004f4 <g_func>:
int g_share = 1;
int g_func(int a)
{
4f4: 55                    push   %ebp
4f5: 89 e5                 mov    %esp,%ebp
g_share += a;
4f7: 8b 45 08              mov    0x8(%ebp),%eax
4fa: 01 05 00 00 00 00     add    %eax,0x0
return a * 2;
500: 8b 45 08              mov    0x8(%ebp),%eax
503: d1 e0                 shl    %eax
}
505: c9                    leave
506: c3                    ret
00000507 <g_func2>:
int g_func2()
{
507: 55                    push   %ebp
508: 89 e5                 mov    %esp,%ebp
50a: 83 ec 08              sub    $0x8,%esp
int a = 2;
50d: c7 45 fc 02 00 00 00  movl   $0x2,0xfffffffc(%ebp)
int b = g_func(3);
514: 6a 03                 push   $0x3
516: e8 fc ff ff ff        call   517 <g_func2+0x10>
51b: 83 c4 04              add    $0x4,%esp
51e: 89 45 f8              mov    %eax,0xfffffff8(%ebp)
return a + b;
521: 8b 45 f8              mov    0xfffffff8(%ebp),%eax
524: 03 45 fc              add    0xfffffffc(%ebp),%eax
}
527: c9                    leave
// skip some of the output
  注意其中地址 4fa 及 516 处的指令:此两处分别对 g_share 及 g_func 进行了访问,显然此时它们的地址仍然是假地址,这些地址在动态库加载完成后会被动态链接器进行重定位,终修改为正确的地址,这看起来与静态链接时进行重定位是一样的过程,但实现上有几个关键的不同之处:
  因为不允许对可执行文件的代码段进行加载时符号重定位,因此如果可执行文件引用了动态库中的数据符号,则在该可执行文件内对符号的重定位必须在链接阶段完成,为做到这一点,链接器在构建可执行文件的时候,会在当前可执行文件的数据段里分配出相应的空间来作为该符号真正的内存地址,等到运行时加载动态库后,再在动态库中对该符号的引用进行重定位:把对该符号的引用指向可执行文件数据段里相应的区域。
  ELF 文件对动态库中的函数调用采用了所谓的"延迟绑定”(lazy binding), 动态库中的函数在其第一次调用发生时才去查找其真正的地址,因此我们不需要在调用动态库函数的地方直接填上假的地址,而是使用了一些跳转地址作为替换,这里先不细说。
  至此,我们可以发现加载时重定位实际上是一个重新修改动态库代码的过程,但我们知道,不同的进程即使是对同一个动态库也很可能是加载到不同地址上,因此当以加载时重定位的方式来使用动态库时,该动态库没法做到被各个进程所共享,而只能在每个进程中 copy 一份:因为符号重定位后,该动态库与在别的进程中不同了,可见此时动态库节省内存的优势不复存在了。