ELF动态链接

当一个程序运行时,依赖于另一个共享库,那么动态链接器会修改可执行文件中的GOT(Global Offset Table 全局偏移表)。GOT 位于数据段(.got.plt节)中,因为GOT必须是可写的。

动态链接器会使用解析好的共享库地址来修改GOT。随后会延迟链接的过程。

延迟链接:比如当一个程序需要调用一个共享库的函数,在改程序运行时动态链接器不会立马解析出该函数的地址,而是在等到第一次调用该函数时再去解析修改GOT表


PLT/GOT

PLT即过程链接表,我们知道当一个程序需要调用共享库里面的一个函数,那么只有在运行时才能知道这些函数的地址,那么一定存在一种机制来解析。

比如如下main.c

int _start()
{

  int a1 = add(1, 2);
    int a2 = sub(3, 4);

  return a1;
}

里面的 add函数,sub函数都定义在动态库 liboutsym.so,将main.c编译并链接liboutsym.so

[root@izbp1irxwqt7ei21awv6wvz elf_write]# objdump -d main

main:     file format elf64-x86-64


Disassembly of section .plt:

0000000000400300 <add@plt-0x10>:
  400300: ff 35 02 0d 20 00     pushq  0x200d02(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  400306: ff 25 04 0d 20 00     jmpq   *0x200d04(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  40030c: 0f 1f 40 00           nopl   0x0(%rax)

0000000000400310 <add@plt>:
  400310: ff 25 02 0d 20 00     jmpq   *0x200d02(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  400316: 68 00 00 00 00        pushq  $0x0
  40031b: e9 e0 ff ff ff        jmpq   400300 <add@plt-0x10>

0000000000400320 <sub@plt>:
  400320: ff 25 fa 0c 20 00     jmpq   *0x200cfa(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  400326: 68 01 00 00 00        pushq  $0x1
  40032b: e9 d0 ff ff ff        jmpq   400300 <add@plt-0x10>

Disassembly of section .text:

0000000000400330 <_start>:
  400330: 55                    push   %rbp
  400331: 48 89 e5              mov    %rsp,%rbp
  400334: 48 83 ec 10           sub    $0x10,%rsp
  400338: be 02 00 00 00        mov    $0x2,%esi
  40033d: bf 01 00 00 00        mov    $0x1,%edi
  400342: b8 00 00 00 00        mov    $0x0,%eax
  400347: e8 c4 ff ff ff        callq  400310 <add@plt>
  40034c: 89 45 fc              mov    %eax,-0x4(%rbp)
  40034f: be 04 00 00 00        mov    $0x4,%esi
  400354: bf 03 00 00 00        mov    $0x3,%edi
  400359: b8 00 00 00 00        mov    $0x0,%eax
  40035e: e8 bd ff ff ff        callq  400320 <sub@plt>
  400363: 89 45 f8              mov    %eax,-0x8(%rbp)
  400366: 8b 45 fc              mov    -0x4(%rbp),%eax
  400369: c9                    leaveq 
  40036a: c3                    retq

当第一次调用add函数时,会跳转到 0x400310 的plt条目,位于 .plt 节中,该条目的第一条指令是跳转指令,跳转的地址即为 GOT 条目的地址(0x601018)。

上面所有的跳转指令都是基于 %rip(存放的是下一条指令地址)找到的
比如 400347: e8 c4 ff ff ff 跳转指令,此时下一条指令地址(%rip)为 40034c,那么最终地址为 ffffffc4+40034c = 40034c - 3c = 400310
400300: ff 35 02 0d 20 00 跳转指令,此时下一条指令地址(%rip)为400306,那么最终地址为 200d02+400306=   601008

我们也可以通过查看下PLT条目表得到

[root@izbp1irxwqt7ei21awv6wvz elf_write]# readelf -r main

Relocation section '.rela.plt' at offset 0x2c8 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000601018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 add + 0
000000601020  000500000007 R_X86_64_JUMP_SLO 0000000000000000 sub + 0

可以看出地址为 0x601018,而此时由于是第一次调用,所以偏移量为 0x601018 的GOT条目里存放的地址为  0x400316 即下一条指令

通过 objdump -D main可以查看到

Disassembly of section .got.plt:

0000000000601000 <_GLOBAL_OFFSET_TABLE_>:
  601000:   f0 0e                   lock (bad)
  601002:   60                      (bad)
    ...
  601017:   00 16                   add    %dl,(%rsi)
  601019:   03 40 00                add    0x0(%rax),%eax
  60101c:   00 00                   add    %al,(%rax)
  60101e:   00 00                   add    %al,(%rax)
  601020:   26 03 40 00             add    %es:0x0(%rax),%eax
  601024:   00 00                   add    %al,(%rax)

pushq $0x0 指的是将第4个GOT条目入栈,即GOT[3],前3个是为其他用途保留的,GOT每个条目大小为8个字节

GOT[0]:存放指向可执行文件动态段的地址,动态链接器利用它提取动态链接相关信息
GOT[1]:存放 link_map 结构的地址,动态链接器利用它来对符号进行解析
GOT[2]:存放指向动态链接器_dl_runtime_resolve()函数的地址,该函数用来解析共享库函数的实际符号地址

接下来跳转到 0x400300 即第一个PLT条目,将 0x601008 压入栈中,即 GOT[1]地址,然后跳转到 0x601010,0x601010即GOT[2]里面存放了 _dl_runtime_resolve() 函数的地址,此时控制权转给了链接器

_dl_runtime_resolve函数会通过把 add() 函数符号值加到 .got.plt 节对应的 GOT 条目中来处理重定位。

下次调用 add() 时,PLT条目会直接跳转到函数本身地址,而无需再走一遍上面的重定位过程


参考:

linux二进制分析

上一篇: ELF程序头
下一篇: 分析修改ELF目标文件(1)
作者邮箱: 203328517@qq.com