GOT表与PLT表的作用

2020-12-10  本文已影响0人  骑猪满天飞

地址无关代码(Position-Independent Code)

如果共享文件(.so文件)需要加载到一个特定的地址才能运行,将造成.so文件的地址冲突问题。因此共享对象的最终装载地址在编译时是不确定的。

如下所示:

readelf --headers libc.so 
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  [...]
  LOAD           0x000000 0x00000000 0x00000000 0x1aef0c 0x1aef0c R E 0x1000
  LOAD           0x1af23c 0x001b023c 0x001b023c 0x02c98 0x057e0 RW  0x1000

共享库的装载地址是从地址0x00000000开始的,这是一个无效地址,当使用此共享库的程序运行时,可以在进程虚拟空间分布看到最终的装载地址。

GOT表--绑定全局变量地址

让我们通过观察地址无关代码具体实现方式,来看看GOT表在其中的作用。

使用如下代码生成地址无关共享库文件:

$ cat test.c 
static int a;
extern int b;
extern void ext();

void bar()
{
  a=1;
  b=2;
}

void foo()
{
  bar();
  ext();
}
gcc -shared -fPIC -o libtest.so test.c

模块内部数据访问

上述代码中静态变量a为模块内部数据,为生成地址无关代码指令不能直接包含a的绝对地址,bar函数汇编代码如下:

0000000000000710 <bar>:
 710:   55                      push   %rbp
 711:   48 89 e5                mov    %rsp,%rbp
 714:   c7 05 16 09 20 00 01    movl   $0x1,0x200916(%rip)        # 201034 <a>
 71b:   00 00 00 
 71e:   48 8b 05 b3 08 20 00    mov    0x2008b3(%rip),%rax        # 200fd8 <_DYNAMIC+0x1c8>

访问数据a的对应指令为:

 714:   c7 05 16 09 20 00 01    movl   $0x1,0x200916(%rip)        # 201034 <a>

此时%rip指向当前指令的下一条指令地址:71e处。a的地址为0x200916+0x71e= 0x201034,然后将1赋值给a。

如果该共享库被加载到0x1000000000000000,那么a的实际地址为0x1000000000000000+0x200916+0x71e= 0x1000000000201034

模块间数据访问

变量b定义在其他模块中,跟模块装载地址有关,根据把地址相关的部分放到数据段的思想,ELF在数据段建立了一个指向这些变量的指针数组,也称为全局变量表(GOT Global Offset Table)

当指令访问变量b时先找到GOT表,再根据GOT表找到变量地址。

71e:    48 8b 05 b3 08 20 00    mov    0x2008b3(%rip),%rax        # 200fd8 <_DYNAMIC+0x1c8>
725:    c7 00 02 00 00 00       movl   $0x2,(%rax)
`

b的偏移地址为0x200fd8

 $objdump -h libtest.so
Sections:
Idx Name          Size      VMA               LMA               File off  Algn
[...]
 19 .got          00000030  0000000000200fd0  0000000000200fd0  00000fd0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
[...]

0x200fd8属于got表范围,再看一下libtest.so的重定位项:

$objdump -R libtest.so 
libtest.so:     file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE
[...] 
0000000000200fd8 R_X86_64_GLOB_DAT  b
[...]

可以看到b的地址需要重定位,位于0x200fd8,GOT偏移8。
所以,当动态加载器加载该so文件时,会先去看它的relocations,然后找到b的值,然后把它重定位到.got部分的0×200828地址处。当访问变量b时,直接取.got中对应地址0x200fd8处,做到了地址无关。

PLT表--延迟绑定函数

在一个程序运行过程中,可能很多函数在程序执行完都不会用到,例如,错误处理函数、很少用到的功能模块等。所以ELF引入了延迟绑定概念,当函数第一次使用时才进行绑定(符号查找、重定位等)

当我们调用外部函数时按照通常做法使用GOT表进行跳转,为实现延迟绑定,在这个过程中通过PLT表又增加一层间接跳转。

模块间调用、跳转

函数通过PLT项进行跳转,还是使用上述的libtest.so进行说明。

00000000000005f0 <ext@plt>:
 5f0:   ff 25 2a 0a 20 00       jmpq   *0x200a2a(%rip)        # 201020 <_GLOBAL_OFFSET_TABLE_+0x20>
 5f6:   68 01 00 00 00          pushq  $0x1
 5fb:   e9 d0 ff ff ff          jmpq   5d0 <_init+0x20>

ext@plt的第一条指令跳转到0x201020,此地址为GOT表中保存ext函数的表项,由于延迟绑定的原因,第一次使用此函数时ext()的地址还未填入此表项:

0000000000201000 <_GLOBAL_OFFSET_TABLE_>:
  201000:   10 0e                   
  201002:   20 00                   
    ...
  201018:   e6 05                   
  20101a:   00 00                   
  20101c:   00 00                   
  20101e:   00 00                   
  201020:   f6 05 00 00 00 00 00            # 201027 <_GLOBAL_OFFSET_TABLE_+0x27>

此时0x201020的值为0x5f6,就是执行exp@plt的第二条指令:
5f6: 68 01 00 00 00 pushq $0x1
其中1代表ext这个符号在重定位表".rel.plt"中的下标。
后续指令:
5fb: e9 d0 ff ff ff jmpq 5d0 <_init+0x20>
此指令跳转到执行符号解析和重定位工作的函数.
经过一系列过程,ext()真正的地址填入到ext@plt中。

当我们再次调用ext()时第一条指令就将跳转到真正的ext()函数中,而不再执行后续指令。

ELF将GOT表分为了两个表".got" 和 ".got.plt",其中".got"用来保存全局变量引用的地址,".got.plt"用来保存函数引用地址。

参考

https://www.freebuf.com/articles/system/135685.html

《程序员的自我修养》

上一篇下一篇

猜你喜欢

热点阅读