一、概述
在如下示例程序print_banner中,调用了glibc动静库中的函数printf,在编译和链接阶段,链接器无奈晓得过程运行起来之后printf函数的加载地址,所以示例中call printf的地址只有在过程运行起来当前能力确定。
080483cc <print_banner>: 80483cc: push %ebp 80483cd: mov %esp, %ebp 80483cf: sub $0x8, %esp 80483d2: sub $0xc, %esp 80483d5: push $0x80484a8 80483da: call **<printf函数的地址>** 80483df: add $0x10, %esp 80483e2: nop 80483e3: leave 80483e4: ret
那么过程运行起来之后,glibc动静库也装载了,printf函数地址也确定了,上述call指令中的地址是如何取得的呢。call地址次要是在运行时/链接时进行重定位。运行时重定位和链接时重定位相干常识见背景常识模块。
背景常识
1、古代操作系统不容许批改代码段,只能批改数据段
2、编译阶段和运行阶段汇编的变动
参考文档:https://blog.csdn.net/linyt/a...
编译阶段是将.c源码翻译成汇编指令的中间件.o
查看应用objdump -d test.o ,能够看到printf的地址临时应用fc ff ff ff代替,这个地址在链接/运行时会进行修改。
00000000 <print_banner>: 0: 55 push %ebp 1: 89 e5 mov %esp, %ebp 3: 83 ec 08 sub $0x8, %esp 6: c7 04 24 00 00 00 00 movl $0x0, (%esp) d: e8 fc ff ff ff call e <print_banner+0xe> 12: c9 leave 13: c3 ret
链接阶段是将一个或多个两头文件(.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雏形图,供参考。