阅读本文前需要阅读 https://www.zhihu.com/question/21249496 和回答后面的四个链接。

plt && got 表

之前提到有些函数实现的代码是在 libc 中,但是编译时和运行时,libc 可能是不同的版本,函数的偏移位置也不一样,这种情况下,操作系统是怎么找到对应的函数的呢?

以下面的代码为例

#include <stdio.h>
#include <stdlib.h>

void world() {
    printf("World");
}

int main(int argc, char* argv[]) {
    printf("Hello ");
    world();
    return 0;
}

main 函数的汇编代码可以看到

gdb-peda$ pdisas main
Dump of assembler code for function main:
   0x000000000040053c <+0>:	push   rbp
   0x000000000040053d <+1>:	mov    rbp,rsp
   0x0000000000400540 <+4>:	sub    rsp,0x10
   0x0000000000400544 <+8>:	mov    DWORD PTR [rbp-0x4],edi
   0x0000000000400547 <+11>:	mov    QWORD PTR [rbp-0x10],rsi
   0x000000000040054b <+15>:	mov    edi,0x4005fa
   0x0000000000400550 <+20>:	mov    eax,0x0
   0x0000000000400555 <+25>:	call   0x400400 <printf@plt>
   0x000000000040055a <+30>:	mov    eax,0x0
   0x000000000040055f <+35>:	call   0x400526 <world>
   0x0000000000400564 <+40>:	mov    eax,0x0
   0x0000000000400569 <+45>:	leave
   0x000000000040056a <+46>:	ret

在两个 printf 那里下断点,然后 si 跟进第一个 printf@plt 看下。

=> 0x400400 <printf@plt>:	jmp    QWORD PTR [rip+0x200c12]        # 0x601018
 | 0x400406 <printf@plt+6>:	push   0x0
 | 0x40040b <printf@plt+11>:	jmp    0x4003f0
 | 0x400410 <__libc_start_main@plt>:	jmp    QWORD PTR [rip+0x200c0a]        # 0x601020
 | 0x400416 <__libc_start_main@plt+6>:	push   0x1
 |->   0x400406 <printf@plt+6>:	push   0x0
       0x40040b <printf@plt+11>:	jmp    0x4003f0
       0x400410 <__libc_start_main@plt>:	jmp    QWORD PTR [rip+0x200c0a]        # 0x601020
       0x400416 <__libc_start_main@plt+6>:	push   0x1

这时候 0x601018 存储的地址就是 0x400406,对应的是 printf@plt+6,跳转过去之后,接下来又是 jmp 0x4003f0,之后就是 _dl_runtime_resolve_avx 函数了。

   0x4003f0:	push   QWORD PTR [rip+0x200c12]        # 0x601008
=> 0x4003f6:	jmp    QWORD PTR [rip+0x200c14]        # 0x601010
 | 0x4003fc:	nop    DWORD PTR [rax+0x0]
 | 0x400400 <printf@plt>:	jmp    QWORD PTR [rip+0x200c12]        # 0x601018
 | 0x400406 <printf@plt+6>:	push   0x0
 | 0x40040b <printf@plt+11>:	jmp    0x4003f0
 |->   0x7ffff7dee870 <_dl_runtime_resolve_avx>:	push   rbx
       0x7ffff7dee871 <_dl_runtime_resolve_avx+1>:	mov    rbx,rsp
       0x7ffff7dee874 <_dl_runtime_resolve_avx+4>:	and    rsp,0xffffffffffffffe0
       0x7ffff7dee878 <_dl_runtime_resolve_avx+8>:	sub    rsp,0x180

接下来就是一大堆指令,没耐心看了,直接按 c 到了第二个 printf@plt 那里,这时候发现第一个 jmp 的行为已经不一样了。

=> 0x400400 <printf@plt>:	jmp    QWORD PTR [rip+0x200c12]        # 0x601018
 | 0x400406 <printf@plt+6>:	push   0x0
 | 0x40040b <printf@plt+11>:	jmp    0x4003f0
 | 0x400410 <__libc_start_main@plt>:	jmp    QWORD PTR [rip+0x200c0a]        # 0x601020
 | 0x400416 <__libc_start_main@plt+6>:	push   0x1
 |->   0x7ffff7a62800 <__printf>:	sub    rsp,0xd8
       0x7ffff7a62807 <__printf+7>:	test   al,al
       0x7ffff7a62809 <__printf+9>:	mov    QWORD PTR [rsp+0x28],rsi
       0x7ffff7a6280e <__printf+14>:	mov    QWORD PTR [rsp+0x30],rdx

这时候 0x601018 指向的地址已经是 0x7ffff7a62800,在 __printf 中,而之前是在 printf@plt 中。

上面的流程可以看出,对于动态库来说,可执行文件在运行前并不知道它的真实地址,而是需要运行时动态的去解析才能知道,专业的说法是运行时重定位。

上面已经有 plt 的概念了

PLT(Procedure Linkage Table)作用是将位置无关的符号转移到绝对地址。当一个外部符号被调用时,PLT 去引用 GOT 中的其符号对应的绝对地址,然后转入并执行。

GOT(Global Offset Table)用于记录在 ELF 文件中所用到的共享库中符号的绝对地址。在程序刚开始运行时,GOT 表项是空的,当符号第一次被调用时会动态解析符号的绝对地址然后转去执行,并将被解析符号的绝对地址记录在 GOT 中,第二次调用同一符号时,由于 GOT 中已经记录了其绝对地址,直接转去执行即可(不用重新解析)。

plt 表的信息也可以通过 readelf -r exe_file 看到

Relocation section '.rela.plt' at offset 0x398 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000601018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0

利用

## 练习题