> 文章列表 > 实例了解GOT,PLT和动态链接

实例了解GOT,PLT和动态链接

实例了解GOT,PLT和动态链接

深入了解GOT,PLT和动态链接

我们使用一个简单的例子来了解动态链接库的链接过程,以及在这个过程中使用到的GOT和PLT的作用是什么。

文件准备

代码结构如下所示:

[root@localhost test]# tree .
.
├── main.c
└── symbol.c

symbol.c的内容如下:

// symbol.c
int my_var = 42;
int my_func(int a, int b) {return a + b;
}

使用如下脚本进行编译:

gcc -g -m32 -masm=intel -shared -fPIC symbol.c -o libsymbol.so

如果编译不成功,使用如下命令安装32位的版本的c/c++库。

yum install glibc-devel.i686
yum install libstdc++-devel.i686

另一个文件是main.c, 调用该动态链接库,代码如下:

// main.c
int var = 10;
extern int my_var;
extern int my_func(int, int);int main() {int a, b;a = var;b = my_var;return my_func(a, b);
}

使用下面的代码进行编译。

# 位置相关
gcc -g -m32 -masm=intel -L. -lsymbol -no-pie -fno-pic main.c libsymbol.so -o main
# 位置无关
gcc -g -m32 -masm=intel -L. -lsymbol main.c libsymbol.so -o main_pi

当目录中生成了main、main_pi、libsymbol.so时,准备工作结束。

动态链接分析

我们使用readelf -S main |egrep '.plt|.got'查看可执行文件中的plt和got相关的段。

[Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
[12] .plt              PROGBITS        080483d0 0003d0 000030 04  AX  0   0 16
[21] .got              PROGBITS        08049ff4 000ff4 00000c 04  WA  0   0  4
[22] .got.plt          PROGBITS        0804a000 001000 000014 04  WA  0   0  4

这里需要对.got.plt的地址0804a000有个印象。

进入gdb调用该程序:

(gdb) disass main
Dump of assembler code for function main:0x0804853d <+0>:     lea    0x4(%esp),%ecx0x08048541 <+4>:     and    $0xfffffff0,%esp0x08048544 <+7>:     pushl  -0x4(%ecx)0x08048547 <+10>:    push   %ebp0x08048548 <+11>:    mov    %esp,%ebp0x0804854a <+13>:    push   %ecx0x0804854b <+14>:    sub    $0x14,%esp0x0804854e <+17>:    mov    0x804a018,%eax0x08048553 <+22>:    mov    %eax,-0xc(%ebp)0x08048556 <+25>:    mov    0x804a01c,%eax0x0804855b <+30>:    mov    %eax,-0x10(%ebp)0x0804855e <+33>:    sub    $0x8,%esp0x08048561 <+36>:    pushl  -0x10(%ebp)0x08048564 <+39>:    pushl  -0xc(%ebp)0x08048567 <+42>:    call   0x80483e0 <my_func@plt>0x0804856c <+47>:    add    $0x10,%esp0x0804856f <+50>:    mov    -0x4(%ebp),%ecx0x08048572 <+53>:    leave0x08048573 <+54>:    lea    -0x4(%ecx),%esp0x08048576 <+57>:    ret
End of assembler dump.

我们在call 0x80483e0 <my_func@plt>这一句上下一个断点。

(gdb) b *0x08048567
Breakpoint 1 at 0x8048567: file main.c, line 9.

使用run运行程序到断点处。

(gdb) r
Starting program: /home/work/cpp_proj/test/main
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-164.el8.i686Breakpoint 1, 0x08048567 in main () at main.c:9
9           return my_func(a, b);

查看一下main,确实停在了断点处:

(gdb) disass main
Dump of assembler code for function main:0x0804853d <+0>:     lea    0x4(%esp),%ecx0x08048541 <+4>:     and    $0xfffffff0,%esp0x08048544 <+7>:     pushl  -0x4(%ecx)0x08048547 <+10>:    push   %ebp0x08048548 <+11>:    mov    %esp,%ebp0x0804854a <+13>:    push   %ecx0x0804854b <+14>:    sub    $0x14,%esp0x0804854e <+17>:    mov    0x804a018,%eax0x08048553 <+22>:    mov    %eax,-0xc(%ebp)0x08048556 <+25>:    mov    0x804a01c,%eax0x0804855b <+30>:    mov    %eax,-0x10(%ebp)0x0804855e <+33>:    sub    $0x8,%esp0x08048561 <+36>:    pushl  -0x10(%ebp)0x08048564 <+39>:    pushl  -0xc(%ebp)
=> 0x08048567 <+42>:    call   0x80483e0 <my_func@plt>0x0804856c <+47>:    add    $0x10,%esp0x0804856f <+50>:    mov    -0x4(%ebp),%ecx0x08048572 <+53>:    leave0x08048573 <+54>:    lea    -0x4(%ecx),%esp0x08048576 <+57>:    ret

使用单步,进入my_func@plt中

(gdb) si
0x080483e0 in my_func@plt ()

查看my_func@plt的内容:

(gdb) x/3i $pc
=> 0x80483e0 <my_func@plt>:     jmp    *0x804a00c0x80483e6 <my_func@plt+6>:   push   $0x00x80483eb <my_func@plt+11>:  jmp    0x80483d0

第一步是一个地址跳转,我们查看一下0x804a00c的内容

(gdb) x/4xw 0x804a00c
0x804a00c <my_func@got.plt>:    0x080483e6      0xf7e2a0f0      0x00000000      0x0000000a

我们发现0x804a00c处的内容是0x080483e6,即jmp *0x804a00c的下一行,即跳转到下一行执行。

接着是执行push $0x0,接着又跳转到0x80483d0执行。

我们查看jmp 0x80483d0处的内容:

(gdb) x/2i 0x80483d00x80483d0:   pushl  0x804a0040x80483d6:   jmp    *0x804a008

我们看到后面会跳转到0x804a008处执行,在这之前我们提到过0x804a000是.got.plt的地址。

.plt.got表项前三个位置, 分别是:

  • got[0]: 本ELF动态段(.dynamic段)的装载地址
  • got1: 本ELF的link_map数据结构描述符地址
  • got2: _dl_runtime_resolve函数的地址

0x804a004则是调用该函数的参数, 且值为got1, 即本ELF的link_map的地址。

0x804a008正好是第三项got2, 即_dl_runtime_resolve函数的地址。

因此jmp *0x804a008作用是跳转到_dl_runtime_resolve执行加载

下面我们打印一下,验证一下分析。首先0x804a008处存储的是0xf7fe5090。

(gdb) x/4xw 0x804a000
0x804a000:      0x08049f04      0xf7ffd9a0      0xf7fe5090      0x080483e6

打印0xf7fe5090处的内容,确实是进入了_dl_runtime_resolve中。

(gdb) x/12i 0xf7fe50900xf7fe5090 <_dl_runtime_resolve>:    endbr320xf7fe5094 <_dl_runtime_resolve+4>:  push   %eax0xf7fe5095 <_dl_runtime_resolve+5>:  push   %ecx0xf7fe5096 <_dl_runtime_resolve+6>:  push   %edx0xf7fe5097 <_dl_runtime_resolve+7>:  mov    0x10(%esp),%edx0xf7fe509b <_dl_runtime_resolve+11>: mov    0xc(%esp),%eax0xf7fe509f <_dl_runtime_resolve+15>: call   0xf7fdec10 <_dl_fixup>0xf7fe50a4 <_dl_runtime_resolve+20>: pop    %edx0xf7fe50a5 <_dl_runtime_resolve+21>: mov    (%esp),%ecx0xf7fe50a8 <_dl_runtime_resolve+24>: mov    %eax,(%esp)0xf7fe50ab <_dl_runtime_resolve+27>: mov    0x4(%esp),%eax0xf7fe50af <_dl_runtime_resolve+31>: ret    $0xc

_dl_runtime_resolve实际上做了两件事:

  • 解析出my_func的地址并将值填入.got.plt中
  • 跳转执行真正的my_func函数.

验证前后过程,确实将0x804a00c处的值修改成了my_func的值。

我们可以在0x80483d6: jmp *0x804a008语句上下一个断点,打印0x804a00c前后的值的变化,可以看到确实发生了变化。可以看到, 在_dl_runtime_resolve之前, 0x804a00c地址的值为0x080483e6,即下一条指令。而运行之后, 该地址的值变为0xf7fb845d, 正是my_func的加载地址!

也就是说, my_func函数的地址是在第一次调用时, 才通过连接器动态解析并加载到.got.plt中的. 而这个过程, 也称之为延时加载或者惰性加载

(gdb) x/xw 0x804a00c
0x804a00c <my_func@got.plt>:    0x080483e6
(gdb) x/xw 0x804a00c
0x804a00c <my_func@got.plt>:    0xf7fb845d

最后打印一下0xf7fb845d的值,看看是不是my_func。

(gdb) x/4i 0xf7fb845d0xf7fb845d <my_func>:        push   %ebp0xf7fb845e <my_func+1>:      mov    %esp,%ebp0xf7fb8460 <my_func+3>:      call   0xf7fb8474 <__x86.get_pc_thunk.ax>0xf7fb8465 <my_func+8>:      add    $0x1b9b,%eax

整个过程可以参考下图,对于my_func第一次执行和后续执行,行为是不一样的。

实例了解GOT,PLT和动态链接

这个过程是不是似曾相识,通常我们在写后台的接口时,当查询完数据后,通常会将数据以插入到redis中,以便下一次访问时可以快速访问到。这里也是这样的机制。

而对于.plt段,就类似于一个后台查询接口,对于.got段,就类似于数据库,对于.plt.got段,就类似于redis缓存。

总结

动态库的加载过程相比于静态库是非常复杂的,其中使用到了.got,.plt和.plt.got段。对于这三个段,可以将其与我们熟悉的CRUD接口进行类比,.plt段,就类似于一个后台查询接口,.got段,就类似于数据库, plt.got段,就类似于redis缓存,是给.plt段查询做的缓存。