linux 链接 got plt,Linux动态连接原理 GOT PLT表详解_weixin_39991055的博客-程序员宅基地

技术标签: linux 链接 got plt  

转载逆风飞扬的文章,有时间有需要认真研究。

Linux动态连接原理 GOT PLT表详解 注意: 以下所用的连接器是指,ld, 而加载器是指ld-linux.so; 1,  GOT表; GOT(Global Offset Table)表中每一项都是本运行模块要引用的一个全局变量或函数的地址。可以用GOT表来间接引用全局变量、函数,也可以把GOT表的首地址作为一个基 准,用相对于该基准的偏移量来引用静态变量、静态函数。由于加载器不会把运行模块加载到固定地址,在不同进程的地址空间中,各运行模块的绝对地址、相对位 置都不同。这种不同反映到GOT表上,就是每个进程的每个运行模块都有独立的GOT表,所以进程间不能共享GOT表。 在x86体系结构 上,本运行模块的GOT表首地址始终保存在%ebx寄存器中。编译器在每个函数入口处都生成一小段代码,用来初始化%ebx寄存器。这一步是必要的,否 则,如果对该函数的调用来自另一运行模块,%ebx中就是调用者模块的GOT表地址;不重新初始化%ebx就用来引用全局变量和函数,当然出错。   这两段话的意思是说,GOT是一个映射表,这里的内容是此段代码里面引用到的外部符号的地址映射,比如你用用到了一个printf函数,在这里就会有一项假设是1000,则就像这样的: .Got 符号                             地址 Printf                      1000 ……… 这样的话程序在运行到printf的时候就寻找到这个地址1000从而走到其实际的代码中的地方去。 但是这里存在一个问题,因为printf是在共享库里面的,而共享库在加载的时候是没有固定地址的,所以你不知道它的地址是1000还是2000?怎么办呢? 于是引入了下面的表plt,这个表的内容是什么呢?请看下面: 2,  PLT表; PLT(Procedure Linkage Table)表每一项都是一小段代码,对应于本运行模块要引用的一个全局函数。以对函数fun的调用为例,PLT中代码片断如下: .PLTfun:  jmp *[email protected](%ebx)           pushl $offset           jmp [email protected] 其中引用的GOT表项被加载器初始化为下一条指令(pushl)的地址,那么该jmp指令相当于nop空指令。 用户程序中对fun的直接调用经编译连接后生成一条call [email][email protected] 指令,这是一条相对跳转指令(满足浮动代码的要求!),跳到.PLTfun 。如果这是本运行模块中第一次调用该函数,此处的jmp等于一个空指令,继续往下执行,接着就跳到PLT[email]0。该PLT项保留给编译器生成的 额外代码,会把程序流程引入到加载器中去。加载器计算fun的实际入口地址,填入[email protected]表项。图示如下:                      user program                     --------------                      call [email protected]                            |                            v       DLL             PLT table                loader --------------   --------------   ----------------------- fun:           change GOT entry from                            |             $loader to $fun,                            v             then jump to there                        GOT table                     --------------                     [email protected] loader 第 一次调用以后,GOT表项已指向函数的正确入口。以后再有对该函数的调用,跳到PLT表后,不再进入加载器,直接跳进函数正确入口了。从性能上分析,只有第一次调用才要加载器作一些额外处理,这是完全可以容忍的。还可以看出,加载时不用对相对跳转的代码进行修补,所以整个代码段都能在进程间共享。   上面的话是什么意思呢? 拿我们上面举的例子,printf在got表里面对应的地址是1000,而这个1000到底以为着什么呢? PLTfun:  jmp *[email protected](%ebx)  1000: pushl $offset           jmp [email protected] 你可以看到所谓1000就是它下面的这个地址,也就是说在外部函数还没有实现连接的时候,got表里面的内容其实是指向下一条指令的,于是开始执行了plt表里面的内容,于是这个段里面的内容肯定包括计算当前这个函数的实际地址的内容,于是求得实际地址添入got表,假设地址为0x800989898 于是got表里面的内容就应该这样的: Printf                        0x800989898 ……………….. 这样当下一次调用这个printf的时候就不需要再去plt表里面走一遭了。 这里需要提一下的是,查找printf的地址实际上就是递归查找当前执行的程序所依赖的库,在她们export的符号表里面寻找,如果找到就返回,否则,报错,就是我们经常看到的undefined referenc to XXXXX. 3,  代码段重定位前提。 代码段本身是存在于只读区域的,所以理论上它是不可能在运行的时候重新修改的,但是这就涉及一个问题,如何保证Got表的正确使用,因为每一个进程都有自己的got表,而共享库完全同时被许多个进程使用的,于是在每个函数的入口都有这样的语句: call L1 L1:  popl %ebx      addl $GOT+[.-.L1], %ebx .o:  R_386_GOTPC .so: NULL 上述过程是编译、连接相合作的结果。编译器生成目标文件时,因为此时还不存在GOT表(每个运行模块有一个GOT表,一个PLT表,由连接器生成),所以暂时不能计算GOT表与当前IP间的差值,仅在第三句处设上一个R_386_GOTPC重定位标记而已。然后进行连接。连接器注意到GOTPC重定位项,于是计算GOT与此处IP的差值,作为addl指令的立即寻址方式操作数。以后再也不需要重定位了。 这样做的好处是目的是什么呢? 就是在函数内部引用外部符号的时候能够正确的转到适当的地方去。 4,  变量、函数引用 当引用的是静态变量、静态函数或字符串常量时,使用R_386_GOTOFF重定位方式。它与GOTPC重定位方式很相似,同样首先由编译器在目标文件中设上重定位标记,然后连接器计算GOT表与被引用元素首地址的差值,作为leal指令的变址寻址方式操作数。代码片断如下: leal [email protected](%ebx), %eax .o:  R_386_GOTOFF .so: NULL 当引用的是全局变量、全局函数时,编译器会在目标文件中设上一个R_386_GOT32重定位标记。连接器会在GOT表中保留一项,注上 R_386_GLOB_DAT重定位标记,用于加载器填写被引用元素的实际地址。连接器还要计算该保留项在GOT表中的偏移,作为movl指令的变址寻址 方式操作数。代码片断如下: movl [email protected](%ebx), %eax .o:  R_386_GOT32 .so: R_386_GLOB_DAT 需要指出,引用全局函数时,由GOT表读出不是全局函数的实际入口地址,而是该函数在PLT表中的入口.PLTfun。这样,无论直接调用,还是先取得函数地址再间接调用,程序流程都会转入PLT表,进而把控制权转移给加载器。加载器就是利用这个机会进行动态连接的。      注意:这里讨论的是变量函数的引用,不是函数的直接调用,而是函数,变量的地址的取得,如果是函数的话,取得的实际上是plt里面的地址,于是最终还是没能逃过加载器的协助。 5,  直接调用函数 如前所述,浮动代码中的函数调用语句会编译成相对跳转指令。首先编译器会在目标文件中设上一个R_386_PLT32重定位标记,然后视静态函数、全局函数不同而连接过程也有所不同。 如果是静态函数,调用一定来自同一运行模块,调用点相对于函数入口点的偏移量在连接时就可计算出来,作为call指令的相对当前IP偏移跳转操作数,由此直接进入函数入口,不用加载器操心。相关代码片断如下: call [email protected] .o:  R_386_PLT32 .so: NULL 如果是全局函数,连接器将生成到.PLTfun的相对跳转指令,之后就如前面所述,对全局函数的第一次调用会把程序流程转到加载器中去,然后计算函数的入口地址,填充[email protected]表项。这称为R_386_JMP_SLOT重定位方式。相关代码片断如下: call [email protected] .o:  R_386_PLT32 .so: R_386_JMP_SLOT 如此一来,一个全局函数可能有多至两个重定位项。一个是必需JMP_SLOT重定位项,加载器把它指向真正的函数入口;另一个是GLOB_DAT重定位 项,加载器把它指向PLT表中的代码片断。取函数地址时,取得的总是GLOB_DAT重定位项的值,也就是指向.PLTfun,而不是真正的函数入口。 进一步考虑这样一个问题:两个动态连接库,取同一个全局函数的地址,两个结果进行比较。由前面的讨论可知,两个结果都没有指向函数的真正入口,而是分别指向两个不同的PLT表。简单进行比较,会得出"不相等"的结论,显然不正确,所以要特殊处理。 注意: 一个是必需JMP_SLOT重定位项,这里指的就是直接调用函数的情况; 另一个是GLOB_DAT重定位 项,这里指函数地址引用的情况; 6,  数据段的重定位 在数据段中的重定位是指对指针类型的静态变量、全局变量进行初始化。它与代码段中的重定位比较起来至少有以下明显不 同:一、在用户程序获得控制权(main函数开始执行)之前就要全部完成;二、不经过GOT表间接寻址,这是因为此时%ebx中还没有正确的GOT表首地 址;三、直接修改数据段,而代码段重定位时不能修改代码段。 如果引用的是静态变量、函数、串常量,编译器会在目标文件中设上 R_386_32重定位标记,并计算被引用变量、函数相对于所在段首地址的偏移量。连接器把它改成R_386_RELATIVE重定位标记,计算它相对于动态连接库首地址(通常为零)的偏移量。加载器会把运行模块真正的首地址(不为零)与该偏移量相加,结果用来初始化指针变量。代码片断如下: .section .rodata .LC0: .string "Ok/n" .data p:     .long .LC0 .o:  R_386_32 w/ section .so: R_386_RELATIVE 如果引用的是全局变量、函数,编译器同样设上R_386_32重定位标记,并且记录引用的符号名字。连接器不必动作。最后加载器查找被引用符号,结果用来初始化指针变量。对于全局函数,查找的结果仍然是函数在PLT表中的代码片断,而不是实际入口。这与前面引用全局函数的讨论相同。代码片断如下: .data p:       .long printf .o:  R_386_32 w/ symbol .so: R_386_32 w/ symbol   7,  总结: 下表给出了前面讨论得到的全部结果:                                 .o                          .so --------------------------------------------------------------------------       |装载GOT表首地址        R_386_GOTPC     NULL 代码段|----------------------------------------------------- 重定位|引用变量函数地址 静态  R_386_GOTOFF    NULL       |                 全局  R_386_GOT32     R_386_GLOB_DAT       |-----------------------------------------------------       |直接调用函数     静态  R_386_PLT32     NULL       |                    全局  R_386_PLT32     R_386_JMP_SLOT ------|----------------------------------------------------- 数据段|引用变量函数地址 静态  R_386_32 w/sec  R_386_RELATIVE 重定位|                 全局  R_386_32 w/sym  R_386_32 w/sym

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_39991055/article/details/116875374

智能推荐

《Unity3D网络游戏实战》第2章_yxqq378287007的博客-程序员宅基地

《Unity3D网络游戏实战》第2章异步代码异步客户端异步代码Async.csusing System;using System.Threading;namespace Async { class MainClass { public static void Main (string[] args) { Timer timer = new Timer(Timeout, null, 2000, 0); Thread.Sleep(2000*2); Console.WriteL

安装系统_iefanrui的博客-程序员宅基地

第一步、准备工作    安装系统前要做一些准备工作,首先是制作你要安装的系统的系统镜像。制作系统镜像要先下载一个UltraISO软件。然后右键点击选择“以管理员的身份运行”(注意:必须这样做)。打开后点击左上角的“文件”-—“打开”,找到存放系统镜像的目录,选中系统镜像,然后点击“打开”。然后点击 “启动”—“开始硬盘录入”,然后选择一个U盘作为系统启动盘(注意:这个U盘必须足够大,一般16G的就...

pv3d 的 Camera3D练习_suzhou_boy1的博客-程序员宅基地

 http://www.sandy1219.com/3d/test2/testflabcamera.html  package{ import flab3d.FlabCamera3D; import flash.display.Sprite; import flash.display.StageAlign; import flash.display.Stage

CAsyncSocket::Send()、OnSend()_hahahapeige的博客-程序员宅基地

virtual int Send( const void* lpBuf, int nBufLen, int nFlags = 0);Dialog中Socket设置AsyncSelect(FD_WRITE),触发虚函数OnSend(),之后调用Send()发送消息。参数lpBuf包含要传输的数据的缓冲区。nBufLen中的数据的长度lpBuf以字节为单位。nFlags...

怎么学python-新手如何自学python课程?_weixin_37988176的博客-程序员宅基地

为了Python的自学党们,传智播客真的是拼了,今天给大家讲讲新手如何自学python课程?传智播客不仅有“人工智能+Python学习路线图”,还根据以往的经验,总结出学习Python之前的各种问题,想详细了解,从此贴开始吧!!!想学Python就已经迈出了通向成功的第一步,想要通过自学成才的话,是需要很大的毅力和吃苦精神,切忌三天打鱼两天晒网。建议通过我们传智黑马论坛的python相关帖子来一步...

随便推点

pthread笔记_银龙w的博客-程序员宅基地

特点:有joinable和detached两种类型的线程。joinable(默认)可以被其他线程回收和杀死,但其存储器、资源不会在显式和隐式退出后被回收。detached不可以被回收和杀死,但其资源会在结束后自动回收。 线程属性可以存在pthread_attr_t类型的变量中,它包含着一个线程可以设置的所有属性,并需要初始化才能使用。同理,互斥锁和条件变量属性使用类pthread_mutexa...

解决zabbix中文不能导入数据库和图形中文乱码的问题_运维饺子的博客-程序员宅基地

一.解决中文不能导入:先说以下没安装之前,正常的解决方法:设置数据库 :vim /etc/my.cnf在[mysqld]下添加:character_set_server=utf8并且在创建zabbix库的时候,设置utf8:create database zabbix character set utf8;以上。然后再说以下已经安装完了出现如下错误的解决方法:导出数据库:...

Django自定义模板标签与过滤器_mapyking的博客-程序员宅基地

常用内置标签:标签描述{% for %} {% endfor %}遍历输出上下文内容{% if %} {% endif %}对上下文条件进行判断{% csrf_token %}生成csrf_token标签,用于防护跨站请求伪造攻击{% url %}生成相应路由地址{% load %}加载导入django标签库{% static %}加载读取静态资源文件{% extends xxx %}使当前模板继承xxx模板

SpringBoot遗忘知识点整理_大忽悠爱忽悠的博客-程序员宅基地

SpringBoot遗忘知识点整理@[email protected]@PropertySourceSpring高级之注解@PropertySource详解(超详细)@PropertySource配置的用法@Value【Spring注解驱动开发】如何使用@Value注解为bean的属性赋值,我们一起吊打面试官!OncePerRequestFilterOncePerRequestFilter 过滤器Spring MVC应用 – 过滤器Spr