学逆向论坛

找回密码
立即注册

只需一步,快速开始

发新帖

111

积分

0

好友

1

主题
发表于 2021-5-4 23:39:57 | 查看: 4930| 回复: 1
elf文件的延迟绑定机制,第一次调用plt的时候,跳转到got表中的相应地址,然后再跳回plt中执行相应的汇编指令,然后调用了一个函数去寻找对应的libc函数的地址,经过第一次调用之后,got表中被填上了该函数的地址,因此下一次调用的时候got表直接指向函数进行调用。(上面那的是我根据自己的理解总结出来的,如果有问题还请一起指正)那么在这里我也很想知道一下几点:
1.寻找libc函数的函数是怎么来的,它是怎么找的,如果我预先知道偏移,那么在编译的时候直接写进got表中又有何不可呢,但是不通过偏移又如何寻找呢?
2.plt表和got表在没有装载任何函数的时候,它有一个固定的值吗?比如got表默认跳转回plt对应的位置,因为我找的资料有说got表和plt表一一对应的。
3.plt中一个函数的对应指令只调用一次吗?
(PS:以上三个问题都是建立在我以上理解没问题的情况下提出的,如果以上理解都有很大问题的话,还请师傅能为我详细讲解一下延迟绑定机制和plt,got表的调用以及它们二表存在的意义,感激不尽)
roger已获得悬赏 1 荣耀+5 学币

最佳答案

什么是动态链接 随着系统中可执行文件的增加,静态链接带来的磁盘和内存空间浪费问题愈发严重。例如大部分可执行文件都需要glibc,那么在静态链接时就要把libc.a和编写的代码链接进去,单个libc.a文件的大小为5M左右 ...
温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的认可,还可以获得学币奖励,请尊重他人的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
发表于 2021-5-5 00:38:40
什么是动态链接
随着系统中可执行文件的增加,静态链接带来的磁盘和内存空间浪费问题愈发严重。例如大部分可执行文件都需要glibc,那么在静态链接时就要把libc.a和编写的代码链接进去,单个libc.a文件的大小为5M左右,那么1000个就是5G。如图2-6左半部所示,两个静态链接的可执行文件都包含testLib.o,那么在装载入内存时,两个相同的库也会被装载进去,造成内存空间的浪费。静态链接另一个明显的缺点是,如果对标准函数做了哪怕一点很微小的改动,都需要重新编译整个源文件,使得开发和维护很艰难。如果不把系统库和自己编写的代码链接到一个可执行文件,而是分割成两个独立的模块,等到程序真正运行时,再把这两个模块进行链接,就可以节省硬盘空间,并且内存中的一个系统库可以被多个程序共同使用,还节省了物理内存空间。这种在运行或加载时,在内存中完成链接的过程叫作动态链接,这些用于动态链接的系统库称为共享库,或者共享对象,整个过程由动态链接器完成。
如图右半部所示,func1.ELF和func2.ELF中不再包含单独的testLib.o,当运行func1.ELF时,系统将func1.o和依赖的testLib.o装载入内存,然后进行动态链接。完成后系统将控制权交给程序入口点,程序开始执行。接下来,当func2.ELF想要执行时,由于内存中已经有testLib.o,因此不再重复加载,直接进行链接即可。
10
GCC默认使用动态链接编译,通过下面的命令我们将func.c编译为共享库,然后使用这个库编译main.c。参数-shared表示生成共享库,-fpic表示生成与位置无关的代码。这样可执行文件func.ELF2就会在加载时与func.so进行动态链接。需要注意的是,动态加载器ld-linux.so本身就是一个共享库,因此加载器会加载并运行动态加载器,并由动态加载器来完成其他共享库以及符号的重定位。
10
位置无关代码
可以加载而无须重定位的代码称为位置无关代码(Position-Independent Code,PIC),它是共享库必须具有的属性,通过给GCC传递-fpic参数可以生成PIC。通过PIC,一个共享库的代码可以被无限多个进程所共享,从而节约内存资源。由于一个程序(或者共享库)的数据段和代码段的相对距离总是保持不变的,因此,指令和变量之间的距离是一个运行时常量,与绝对内存地址无关。于是就有了全局偏移量表(Global Offset Table, GOT),它位于数据段的开头,用于保存全局变量和库函数的引用,每个条目占8个字节,在加载时会进行重定位并填入符号的绝对地址。实际上,为了引入RELRO保护机制,GOT被拆分为.got节和.got.plt节两个部分,不需要延迟绑定的前者用于保存全局变量引用,加载到内存后被标记为只读;需要延迟绑定的后者则用于保存函数引用,具有读写权限(详情请查看4.6节)。我们看一下func.so的情况,可以看到全局变量tmp位于GOT上,R_X86_64_GLOB_DAT表示需要动态链接器找到tmp的值并填充到0x200fd8。在func()函数需要取出tmp时,计算符号相对PC的偏移rip+0x20090f,也就是0x6c9+0x20090f=0x200fd8。
10

10

延迟绑定
由于动态链接是由动态链接器在程序加载时进行的,当需要重定位的符号(库函数)多了之后,势必会影响性能。延迟绑定(lazy binding)就是为了解决这一问题,其基本思想是当函数第一次被调用时,动态链接器才进行符号查找、重定位等操作,如果未被调用则不进行绑定。
ELF文件通过过程链接表(Procedure Linkage Table, PLT)和GOT的配合来实现延迟绑定,每个被调用的库函数都有一组对应的PLT和GOT。
位于代码段.plt节的PLT是一个数组,每个条目占16个字节。其中PLT[0]用于跳转到动态链接器,PLT[1]用于调用系统启动函数__libc_start_main(),我们熟悉的main()函数就是在这里面调用的,从PLT[2]开始就是被调用的各个函数条目。
位于数据段.got.plt节的GOT也是一个数组,每个条目占8个字节。其中GOT[0]和GOT[1]包含动态链接器在解析函数地址时所需要的两个地址(.dynamic和relor条目),GOT[2]是动态链接器ld-linux.so的入口点,从GOT[3]开始就是被调用的各个函数条目,这些条目默认指向对应PLT条目的第二条指令,完成绑定后才会被修改为函数的实际地址。
以func.ELF2调用库函数func()为例。可以看到,执行call指令会进入func@plt,第一条jmp指令找到对应的GOT条目,这时该位置保存的还是第二条指令的地址,于是执行第二条指令push,将对应的0x1(func在.rel.plt中的下标)压栈,然后进入PLT[0]。PLT[0]先将GOT[1]压栈,然后调用GOT[2],也就是动态链接器的_dl_runtime_resolve()函数,完成符号解析和重定位工作,并将func()的真实地址填入func@got.plt,也就是GOT[4],最后才把控制权交给func()。延迟绑定完成后,如果再调用func(),就可以由func@plt的第一条指令直接跳转到func@got.plt,将控制权交给func()。
10

10
最后,我们简单介绍一下运行时链接,即程序在运行时加载和链接共享库。Linux为此提供了一个简单的接口dlopen。传统的动态链接会生成一个GOT表,记录着可能用到的所有符号,并且这些符号在链接时都是可以找到的。运行时链接则需要在运行时定位这些符号。
10

小黑屋|手机版|站务邮箱|学逆向论坛 ( 粤ICP备2021023307号 )|网站地图

GMT+8, 2024-12-22 00:42 , Processed in 0.127519 second(s), 56 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表