技术标签: 聊聊glibc plt got 动态链接 重定位
在介绍PLT和GOT出场之前,先以一个简单的例子引入两个主角,各位请看以下代码:
#include <stdio.h>
void print_banner()
{
printf("Welcome to World of PLT and GOT\n");
}
int main(void)
{
print_banner();
return 0;
}
编译:
gcc -Wall -g -o test.o -c test.c -m32
链接:
gcc -o test test.o -m32
注意:现代Linux系统都是x86_64系统了,后面需要对中间文件test.o以及可执行文件test反编译,分析汇编指令,因此在这里使用-m32选项生成i386架构指令而非x86_64架构指令。
经编译和链接阶段之后,test可执行文件中print_banner函数的汇编指令会是怎样的呢?我猜应该与下面的汇编类似:
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
print_banner函数内调用了printf函数,而printf函数位于glibc动态库内,所以在编译和链接阶段,链接器无法知知道进程运行起来之后printf函数的加载地址。故上述的**<printf函数地址>**
一项是无法填充的,只有进程运运行后,printf函数的地址才能确定。
那么问题来了:进程运行起来之后,glibc动态库也装载了,printf函数地址亦已确定,上述call指令如何修改(重定位)呢?
一个简单的方法就是将指令中的**<printf函数地址>**
修改printf函数的真正地址即可。
但这个方案面临两个问题:
- 现代操作系统不允许修改代码段,只能修改数据段
- 如果print_banner函数是在一个动态库(.so对象)内,修改了代码段,那么它就无法做到系统内所有进程共享同一个动态库。
因此,printf函数地址只能回写到数据段内,而不能回写到代码段上。
注意:刚才谈到的回写,是指运行时修改,更专业的称谓应该是运行时重定位,与之相对应的还有链接时重定位。
说到这里,需要把编译链接过程再展开一下。我们知道,每个编译单元(通常是一个.c文件,比如前面例子中的test.c)都会经历编译和链接两个阶段。
编译阶段是将.c源代码翻译成汇编指令的中间文件,比如上述的test.c文件,经过编译之后,生成test.o中间文件。print_banner函数的汇编指令如下(使用强调内容objdump -d test.o命令即可输出):
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
是否注意到call指令的操作数是fc ff ff ff,翻译成16进制数是0xfffffffc(x86架构是小端的字节序),看成有符号是-4。这里应该存放printf函数的地址,但由于编译阶段无法知道printf函数的地址,所以预先放一个-4在这里,然后用重定位项来描述:这个地址在链接时要修正,它的修正值是根据printf地址(更确切的叫法应该是符号,链接器眼中只有符号,没有所谓的函数和变量)来修正,它的修正方式按相对引用方式。
这个过程称为链接时重定位,与刚才提到的运行时重定位工作原理完全一样,只是修正时机不同。
链接阶段是将一个或者多个中间文件(.o文件)通过链接器将它们链接成一个可执行文件,链接阶段主要完成以下事情:
- 各个中间文之间的同名section合并
- 对代码段,数据段以及各符号进行地址分配
- 链接时重定位修正
除了重定位过程,其它动作是无法修改中间文件中函数体内指令的,而重定位过程也只能是修改指令中的操作数,换句话说,链接过程无法修改编译过程生成的汇编指令。
那么问题来了:编译阶段怎么知道printf函数是在glibc运行库的,而不是定义在其它.o中
答案往往令人失望:编译器是无法知道的
那么编译器只能老老实实地生成调用printf的汇编指令,printf是在glibc动态库定位,或者是在其它.o定义这两种情况下,它都能工作。如果是在其它.o中定义了printf函数,那在链接阶段,printf地址已经确定,可以直接重定位。如果printf定义在动态库内(链接阶段是可以知道printf在哪定义的,只是如果定义在动态库内不知道它的地址而已),链接阶段无法做重定位。
根据前面讨论,运行时重定位是无法修改代码段的,只能将printf重定位到数据段。那在编译阶段就已生成好的call指令,怎么感知这个已重定位好的数据段内容呢?
答案是:链接器生成一段额外的小代码片段,通过这段代码支获取printf函数地址,并完成对它的调用。
链接器生成额外的伪代码如下:
.text
...
// 调用printf的call指令
call printf_stub
...
printf_stub:
mov rax, [printf函数的储存地址] // 获取printf重定位之后的地址
jmp rax // 跳过去执行printf函数
.data
...
printf函数的储存地址:
这里储存printf函数重定位后的地址
链接阶段发现printf定义在动态库时,链接器生成一段小代码print_stub,然后printf_stub地址取代原来的printf。因此转化为链接阶段对printf_stub做链接重定位,而运行时才对printf做运行时重定位。
前面由一个简单的例子说明动态链接需要考虑的各种因素,但实际总结起来说两点:
- 需要存放外部函数的数据段
- 获取数据段存放函数地址的一小段额外代码
如果可执行文件中调用多个动态库函数,那每个函数都需要这两样东西,这样每样东西就形成一个表,每个函数使用中的一项。
总不能每次都叫这个表那个表,于是得正名。存放函数地址的数据表,称为重局偏移表(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雏形图,供各位参考。
当然这个原理图并不是Linux下的PLT/GOT真实过程,Linux下的PLT/GOT还有更多细节要考虑了。这个图只是将这些躁声全部消除,让大家明确看到PLT/GOT是如何穿针引线的。
文章浏览阅读1k次。通过使用ajax方法跨域请求是浏览器所不允许的,浏览器出于安全考虑是禁止的。警告信息如下:不过jQuery对跨域问题也有解决方案,使用jsonp的方式解决,方法如下:$.ajax({ async:false, url: 'http://www.mysite.com/demo.do', // 跨域URL ty..._nginx不停的xhr
文章浏览阅读2k次。关于在 Oracle 中配置 extproc 以访问 ST_Geometry,也就是我们所说的 使用空间SQL 的方法,官方文档链接如下。http://desktop.arcgis.com/zh-cn/arcmap/latest/manage-data/gdbs-in-oracle/configure-oracle-extproc.htm其实简单总结一下,主要就分为以下几个步骤。..._extproc
文章浏览阅读1.5w次。linux下没有上面的两个函数,需要使用函数 mbstowcs和wcstombsmbstowcs将多字节编码转换为宽字节编码wcstombs将宽字节编码转换为多字节编码这两个函数,转换过程中受到系统编码类型的影响,需要通过设置来设定转换前和转换后的编码类型。通过函数setlocale进行系统编码的设置。linux下输入命名locale -a查看系统支持的编码_linux c++ gbk->utf8
文章浏览阅读750次。今天准备从生产库向测试库进行数据导入,结果在imp导入的时候遇到“ IMP-00009:导出文件异常结束” 错误,google一下,发现可能有如下原因导致imp的数据太大,没有写buffer和commit两个数据库字符集不同从低版本exp的dmp文件,向高版本imp导出的dmp文件出错传输dmp文件时,文件损坏解决办法:imp时指定..._imp-00009导出文件异常结束
文章浏览阅读143次。当下是一个大数据的时代,各个行业都离不开数据的支持。因此,网络爬虫就应运而生。网络爬虫当下最为火热的是Python,Python开发爬虫相对简单,而且功能库相当完善,力压众多开发语言。本次教程我们爬取前程无忧的招聘信息来分析Python程序员需要掌握那些编程技术。首先在谷歌浏览器打开前程无忧的首页,按F12打开浏览器的开发者工具。浏览器开发者工具是用于捕捉网站的请求信息,通过分析请求信息可以了解请..._初级python程序员能力要求
文章浏览阅读7.6k次,点赞2次,收藏6次。@Service标注的bean,类名:ABDemoService查看源码后发现,原来是经过一个特殊处理:当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致public class AnnotationBeanNameGenerator implements BeanNameGenerator { private static final String C..._@service beanname
文章浏览阅读6.9w次,点赞73次,收藏463次。1.前序创建#include<stdio.h>#include<string.h>#include<stdlib.h>#include<malloc.h>#include<iostream>#include<stack>#include<queue>using namespace std;typed_二叉树的建立
文章浏览阅读7.1k次。在Asp.net上使用Excel导出功能,如果文件名出现中文,便会以乱码视之。 解决方法: fileName = HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8);_asp.net utf8 导出中文字符乱码
文章浏览阅读2.1k次,点赞4次,收藏23次。第一次实验 词法分析实验报告设计思想词法分析的主要任务是根据文法的词汇表以及对应约定的编码进行一定的识别,找出文件中所有的合法的单词,并给出一定的信息作为最后的结果,用于后续语法分析程序的使用;本实验针对 PL/0 语言 的文法、词汇表编写一个词法分析程序,对于每个单词根据词汇表输出: (单词种类, 单词的值) 二元对。词汇表:种别编码单词符号助记符0beginb..._对pl/0作以下修改扩充。增加单词
文章浏览阅读773次。我在使用adb.exe时遇到了麻烦.我想使用与bash相同的adb.exe shell提示符,所以我决定更改默认的bash二进制文件(当然二进制文件是交叉编译的,一切都很完美)更改bash二进制文件遵循以下顺序> adb remount> adb push bash / system / bin /> adb shell> cd / system / bin> chm..._adb shell mv 权限
文章浏览阅读6.8k次,点赞12次,收藏125次。1. 单目相机标定引言相机标定已经研究多年,标定的算法可以分为基于摄影测量的标定和自标定。其中,应用最为广泛的还是张正友标定法。这是一种简单灵活、高鲁棒性、低成本的相机标定算法。仅需要一台相机和一块平面标定板构建相机标定系统,在标定过程中,相机拍摄多个角度下(至少两个角度,推荐10~20个角度)的标定板图像(相机和标定板都可以移动),即可对相机的内外参数进行标定。下面介绍张氏标定法(以下也这么称呼)的原理。原理相机模型和单应矩阵相机标定,就是对相机的内外参数进行计算的过程,从而得到物体到图像的投影_相机-投影仪标定
文章浏览阅读2.2k次。文章目录Wayland 架构Wayland 渲染Wayland的 硬件支持简 述: 翻译一篇关于和 wayland 有关的技术文章, 其英文标题为Wayland Architecture .Wayland 架构若是想要更好的理解 Wayland 架构及其与 X (X11 or X Window System) 结构;一种很好的方法是将事件从输入设备就开始跟踪, 查看期间所有的屏幕上出现的变化。这就是我们现在对 X 的理解。 内核是从一个输入设备中获取一个事件,并通过 evdev 输入_wayland