一、概述
在如下示例程序print_banner中,调用了glibc动态库中的函数printf,在编译和链接阶段,链接器无法知道进程运行起来之后printf函数的加载地址,所以示例中call printf的地址只有在进程运行起来以后才能确定。
080483cc :
80483cc:push %ebp
80483cd:mov%esp, %ebp
80483cf:sub$0x8, %esp
80483d2:sub$0xc, %esp
80483d5:push $0x80484a8
80483da:call ****
80483df:add $0x10, %esp
80483e2:nop
80483e3:leave
80483e4:ret
那么进程运行起来之后,glibc动态库也装载了,printf函数地址也确定了,上述call指令中的地址是如何获得的呢。call地址主要是在运行时/链接时进行重定位。运行时重定位和链接时重定位相关知识见背景知识模块。
背景知识 1、现代操作系统不允许修改代码段,只能修改数据段 2、编译阶段和运行阶段汇编的变化 【LINUX下可执行文件逆向分析基础】参考文档:https://blog.csdn.net/linyt/a...
编译阶段是将.c源码翻译成汇编指令的中间件.o
查看使用objdump -d test.o ,可以看到printf的地址暂时使用fc ff ff ff替代,这个地址在链接/运行时会进行修正。
00000000 :
0:55push %ebp
1:89 e5mov %esp, %ebp
3:83 ec 08sub$0x8, %esp
6:c7 04 24 00 00 00 00 movl$0x0, (%esp)
d:e8 fc ff ff ffcalle 12:c9leave
13:c3ret
链接阶段是将一个或多个中间文件(.o)通过链接器链接成一个可执行文件,链接主要完成
各个中间文件之间的同名section合并
对代码段,数据段以及各符号进行地址分配
链接时重定位修正
3、运行时重定位和链接时重定位
- 链接时重定位:如果函数在其他.o文件中定义,则链接时printf地址即可确定,直接重定位
- 运行时重定位:如果函数在动态库内(链接阶段是可以知道printf在哪定义的,只是如果定义在动态库内不知道它的地址而已),则会在运行时进行重定位。运行时重定位是无法修改代码段的,只能将printf重定位到数据段。那在编译阶段就已生成好的call指令,怎么感知这个已重定位好的数据段内容呢。
答案是:链接器生成一段额外的小代码片段,通过这段代码支获取printf函数地址,并完成对它的调用。伪代码如下。
链接阶段发现printf定义在动态库时,链接器生成一段小代码print_stub,然后printf_stub地址取代原来的printf。因此转化为链接阶段对printf_stub做链接重定位,而运行时才对printf做运行时重定位。
.text ...// 调用printf的call指令 call printf_stub ...printf_stub: mov rax, [printf函数的储存地址] // 获取printf重定位之后的地址 jmp rax // 跳过去执行printf函数.data ... printf函数的储存地址: 这里储存printf函数重定位后的地址
- PLT和GOT表
动态链接主要有2个因素
需要存放外部函数的数据段 获取数据段存放函数地址的一小段额外代码
如果可执行文件中调用多个动态库函数,那每个函数都需要这两样东西,这样每样东西就形成一个表,每个函数使用中的一项。
总不能每次都叫这个表那个表,于是得正名。存放函数地址的数据表,称为重局偏移表(GOT, Global Offset Table),而那个额外代码段表,称为程序链接表(PLT,Procedure Link Table)。它们两姐妹各司其职,联合出手上演这一出运行时重定位好戏。
那么PLT和GOT长得什么样子呢?前面已有一些说明,下面以一个例子和简单的示意图来说明PLT/GOT是如何运行的。
假设最开始的示例代码test.c增加一个write_file函数,在该函数里面调用glibc的write实现写文件操作。根据前面讨论的PLT和GOT原理,test在运行过程中,调用方(如print_banner和write_file)是如何通过PLT和GOT穿针引线之后,最终调用到glibc的printf和write函数的?
下面是PLT和GOT雏形图,供参考。
文章图片