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

技术标签: 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 *fun@GOT(%ebx)           pushl $offset           jmp .PLT0@PC 其中引用的GOT表项被加载器初始化为下一条指令(pushl)的地址,那么该jmp指令相当于nop空指令。 用户程序中对fun的直接调用经编译连接后生成一条call [email]fun@PLT 指令,这是一条相对跳转指令(满足浮动代码的要求!),跳到.PLTfun 。如果这是本运行模块中第一次调用该函数,此处的jmp等于一个空指令,继续往下执行,接着就跳到PLT[email]0。该PLT项保留给编译器生成的 额外代码,会把程序流程引入到加载器中去。加载器计算fun的实际入口地址,填入fun@GOT表项。图示如下:                      user program                     --------------                      call fun@PLT                            |                            v       DLL             PLT table                loader --------------   --------------   ----------------------- fun:           change GOT entry from                            |             $loader to $fun,                            v             then jump to there                        GOT table                     --------------                     fun@GOT loader 第 一次调用以后,GOT表项已指向函数的正确入口。以后再有对该函数的调用,跳到PLT表后,不再进入加载器,直接跳进函数正确入口了。从性能上分析,只有第一次调用才要加载器作一些额外处理,这是完全可以容忍的。还可以看出,加载时不用对相对跳转的代码进行修补,所以整个代码段都能在进程间共享。   上面的话是什么意思呢? 拿我们上面举的例子,printf在got表里面对应的地址是1000,而这个1000到底以为着什么呢? PLTfun:  jmp *fun@GOT(%ebx)  1000: pushl $offset           jmp .PLT0@PC 你可以看到所谓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 .LC1@GOTOFF(%ebx), %eax .o:  R_386_GOTOFF .so: NULL 当引用的是全局变量、全局函数时,编译器会在目标文件中设上一个R_386_GOT32重定位标记。连接器会在GOT表中保留一项,注上 R_386_GLOB_DAT重定位标记,用于加载器填写被引用元素的实际地址。连接器还要计算该保留项在GOT表中的偏移,作为movl指令的变址寻址 方式操作数。代码片断如下: movl x@GOT(%ebx), %eax .o:  R_386_GOT32 .so: R_386_GLOB_DAT 需要指出,引用全局函数时,由GOT表读出不是全局函数的实际入口地址,而是该函数在PLT表中的入口.PLTfun。这样,无论直接调用,还是先取得函数地址再间接调用,程序流程都会转入PLT表,进而把控制权转移给加载器。加载器就是利用这个机会进行动态连接的。      注意:这里讨论的是变量函数的引用,不是函数的直接调用,而是函数,变量的地址的取得,如果是函数的话,取得的实际上是plt里面的地址,于是最终还是没能逃过加载器的协助。 5,  直接调用函数 如前所述,浮动代码中的函数调用语句会编译成相对跳转指令。首先编译器会在目标文件中设上一个R_386_PLT32重定位标记,然后视静态函数、全局函数不同而连接过程也有所不同。 如果是静态函数,调用一定来自同一运行模块,调用点相对于函数入口点的偏移量在连接时就可计算出来,作为call指令的相对当前IP偏移跳转操作数,由此直接进入函数入口,不用加载器操心。相关代码片断如下: call f@PLT .o:  R_386_PLT32 .so: NULL 如果是全局函数,连接器将生成到.PLTfun的相对跳转指令,之后就如前面所述,对全局函数的第一次调用会把程序流程转到加载器中去,然后计算函数的入口地址,填充fun@GOT表项。这称为R_386_JMP_SLOT重定位方式。相关代码片断如下: call f@PLT .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

智能推荐

【Maven教程】(十):使用 Hudson 进行持续集成—— 从Hudson的安装到任务创建 ~_hudson搭建-程序员宅基地

文章浏览阅读1.4k次。优秀的持续集成工具有很多,如老牌的开源工具CruiseControl 、商业的 Bamboo 和 TeamCity 等。这里只介绍 Hudson, 因为它是目前较流行的开源持续集成工具。该项目过去一直托管在 java.net 社区,不过现在已经迁移到。Hudson 主要是由Kohsuke Kawaguchi 开发和维护的,Kohsuke Kawaguchi 自2001年就已经加入 Sun 公司(当然,现在已经是 Oracle 了)。_hudson搭建

计算机网络物理层第一章物理层详解_物理层的过程特性举例-程序员宅基地

文章浏览阅读5.9k次,点赞25次,收藏153次。计算机网络在我们实际项目中可能应用不多,但是学懂它决定了一个人能成长的上限,进来,一起学_物理层的过程特性举例

Eclipse快速上手Hibernate--1. 入门实例_eclipse hibernate 入門-程序员宅基地

文章浏览阅读5.7w次。 这篇文章主要谈谈Hibernate的入门开发,例子很简单,就是向数据表中添加用户名和密码。我分别使用了三种方法,一种是直接写代码,写Hbm映射文件等;一种是通过Hbm映射文件来生成代码;一种是通过代码来生成Hbm映射文件。使用了一些自动化工具,XMLBuddy是用来编辑XML文件的,JBoss Eclipse IDE是用来编写Doclet标记的。这篇文章还谈到了一些Eclipse的使用技巧_eclipse hibernate 入門

【明道云】如何实现对富文本组件内容的视图查询_明道云 富文本 格式-程序员宅基地

文章浏览阅读172次。富文本无法作为被搜索的字段,无非就是因为富文本包含了太多格式信息、图片等。只要再造一个同步的纯文本组件,把纯文本设置为搜索目标字段即可间接实现搜索富文本内容的需求。明道云视图中可以非常方便地自定义查询字段,但是富文本组件却无法直接作为查询目标字段使用。本篇给出一种WalkAround方法。_明道云 富文本 格式

【mnn】——模型离线量化流程代码浅析_mnn 量化模型-程序员宅基地

文章浏览阅读1.3k次,点赞2次,收藏3次。mnn, 离线量化1. 前言mnn的离线量化,需要首先将其他模型转换成mnn的模型表达,再进行量化。这里我们采用MAX_ABS进行weight权重量化,KL散度进行激活值的量化,int8对称量化。2. Code2.1 mnn模型读入与解析std::unique_ptr<MNN::NetT> netT; { std::ifstream input(modelFile); std::ostringstream outputOs; ._mnn 量化模型

SCG K8S(Spring Cloud Gateway Kubernetes)一直报503错误-程序员宅基地

文章浏览阅读626次。做了网关应用,可以正常工作。但我们项目是基于Springboot3.x 的,而原来的教程是基于Springboot 2.x的,升级后发现一直没法发现服务,报503 Service Not Available的异常。查阅了Spring官方文档,估计是原来的依赖不对,改了一下依赖,终于正常了。项目需要在Kubernetes上使用SCG,参照网上的教程。

随便推点

Java中的类加载和双亲委派原则_java所有的类的加载都必须遵循双亲委派原则-程序员宅基地

文章浏览阅读361次。Java类加载过程1,加载–》2,验证–》3,准备–,4,解析–》5,初始化加载加载是指将类的class文件读到内存中,并为其创建一个java.lang.Class对象(每个类都有其独一无二的.Class对象),类加载由JVM中的类加载器完成,且其加载一般符合"双亲委派原则",(下文会简单的介绍类加载器和双亲委派原则,不要担心),除此之外,还可以自定义类加载器对类进行初始化;通过不同的类加载器,可以从不同的源加载类的二进制数据文件:1.从本地文件系统加载class文件。2.从JAR包加载cla._java所有的类的加载都必须遵循双亲委派原则

OJ(Online Judge)-程序员宅基地

文章浏览阅读259次。OJ:它是Online Judge系统的简称,用来在线检测程序源代码的正确性。著名的OJ有RQNOJ、URAL等。国内著名的题库有北京大学题库、浙江大学题库等。国外的题库包括乌拉尔大学、瓦拉杜利德大学题库等。ACM:ACM国际大学生程序设计竞赛(英文全称:ACM International Collegiate ProgrammingContest(ACM-ICPC或ICPC)是由美国计算..._online judge csdn

robot framework接口自动化测试post请求_robotframework post请求加token-程序员宅基地

文章浏览阅读1.1w次。之前介绍了get请求不需要传递token的 也介绍了post请求,下面简介一下post请求需要token的方式。首先获取到之前创建的token接下来创建字典格式将请求头赋给变量header作为头文件2.创建session服务器连接,把请求数据传输方式和token传入3.post请求把URI和数据传入4.判断响应状态码是否为2005.将响应格式转换为json格式6.判_robotframework post请求加token

Android Intent传递object_intent object实例-程序员宅基地

文章浏览阅读1k次。Android中Intent传递类对象提供了两种方式一种是 通过实现Serializable接口传递对象,一种是通过实现Parcelable接口传递对象。要求被传递的对象必须实现上述2种接口中的一种才能通过Intent直接传递。Intent中传递这2种对象的方法:Bundle.putSerializable(Key,Object); //实现Serializable接口的_intent object实例

python——定时任务task_env.task(0, 1)-程序员宅基地

文章浏览阅读1.4w次,点赞2次,收藏3次。pyhton的定时任务写法:#!/usr/bin/env python#-- encoding:utf-8 --import timedef task(): print "task ..."def timer(n): while True: print time.strftime('%Y-%m-%d %X',time.localtime_env.task(0, 1)

IDUtils工具类-程序员宅基地

文章浏览阅读2.6k次。package com.taotao.common.utils;import java.util.Random;/** * 各种id生成策略 * Title: IDUtils * @date 2017年7月16日下午2:32:10 * @version 1.0 */public class IDUtils { /** * 图片名生成 */ public stati_idutils