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

news/2024/11/28 1:32:26/

深入了解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第一次执行和后续执行,行为是不一样的。

动态链接的过程

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

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

总结

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


http://www.ppmy.cn/news/48828.html

相关文章

SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式,系统详解springcloud微服务技术栈(nacos)

Nacos注册中心 &#xff08;一&#xff09;认识和安装Nacos 1、认识Nacos 2、安装nacos 这里下载1.4.1版本 默认端口是8848 下载解压后&#xff0c;终端进入到nacos/bin下&#xff0c;bash startup.sh -m standalone 然后查看start.out文件得到一个网址就可以查看nacos的服…

UEFI开发探索13 – 访问PCI/PCI-E设备1

我所用的测试卡是PCI-E设备,公司商用的产品也是PCI-E设备。所以,我很早就“被迫”去读那些PCI spec。 从软件工程师的角度,我觉得只要解决几个问题就行了,其余的细节不妨碍编程。 PCI/PCI-E设备是如何定位的,也即程序如何找到设备;系统把它认作什么设备;如何访问设备的…

【服务器数据恢复】Storwize存储上的Oracle数据库数据恢复案例

服务器数据恢复环境&#xff1a; IBM Storwize某型号存储&#xff0c;共10块磁盘&#xff0c;组建了2组Mdisk加入到一个存储池中&#xff0c;创建了一个通用卷存放数据&#xff0c;存放的数据包含oracle数据库。 服务器故障&#xff1a; 存储中其中一组Mdisk有两块磁盘出现故障…

【推荐】1657- 灵活可扩展,2023年值得尝试的13款富文本编辑器

作为前端开发人员&#xff0c;我们经常需要为网站和应用程序添加文本内容。与传统的文本编辑器不同&#xff0c;富文本编辑器可让您轻松创建各种类型的文本内容&#xff0c;包括加粗字体、斜体字、框架、列表、图片和视频等。 本文我将向大家推荐 13 款开源的灵活可拓展的富文本…

服务(第八篇)location和rewrite

常用的Nginx正则表达式: 从功能看&#xff0c;rewrite和location似乎有点像&#xff0c;都能实现跳转&#xff0c;主要区别在于rewrite是在同一域名内更改获取资源的路径&#xff0c;而location是对一类路径做控制访问或反向代理&#xff0c;还可以proxy_pass到其他机器。 rew…

CSS基础——盒子模型

目录 简介 盒子模型组成 内容区 内边距 边框 border-width border-color border-style border 外边距 负值 auto 简写属性 垂直外边距的重叠 浏览器默认设置 内联元素的盒子 简介 在网页中&#xff0c;一切都是可以看作为“盒子”。 在css处理网页的时候&…

COCO数据集格式介绍

COCO是微软构建的一个目标检测大型基准数据集&#xff0c;非常非常著名&#xff0c;包括检测、分割、关键点估计等任务&#xff0c;目前用的比较多的是COCO2017数据集&#xff0c;其他如COCO2014数据集格式类似&#xff0c;所以我这里以COCO2017为例。 首先我们来看目录结构&am…

1.18 从0开始学习Unity游戏开发--资源加载

在上一篇文章中&#xff0c;我们大约是开始接触到资源加载的事情了&#xff0c;场景资源则是一个比较特殊的资源&#xff0c;我们只要添加到Build Settings里面&#xff0c;那么我们就可以通过API直接加载。 但是其他类型的资源怎么办呢&#xff1f;比如我们制作一个网络游戏&…