美文网首页
ELF探究 之 重定位(二)

ELF探究 之 重定位(二)

作者: Sharkchilli | 来源:发表于2020-06-23 10:18 被阅读0次

    概述

    重定位操作是连接符号引用(symbolic references)和符号定义(symbolic definitions)的过程。例如,程序中调用一个(外部)函数,代码中我们只需要指定函数名(符号引用)即可,但是当程序实际运行的时候,相关的CALL指令必须能够正确无误地跳转到函数实际地址处(符号定义)去执行函数代码。可是在链接阶段之前,符号的虚拟地址(亦可称运行时地址)并没有分配,只有在链接阶段的符号解析过程中链接器才会为符号分配虚拟地址。在符号地址确认后,链接器这才会修改机器指令(即重定位操作是在符号解析之后),可是链接器并不会聪明到可以自动找到可重定位文件中引用外部符号的地方(即需要修改的地方),所以可重定位文件必须提供相应的信息来帮助链接器,换句话说,可重定位文件中必须包含相关的信息来告诉链接器如何去修改节的内容,只有这样,最后生成的可执行文件或者共享库才会包含正确的信息来构建最终的进程映像。可重定位项就是帮助链接器进行重定位操作的信息。

    重定位就是把符号引用与符号定义链接起来的过程,这也是 android linker 的主要工作之一。
    当程序中调用一个函数时,相关的 call 指令必须在执行期将控制流转到正确的目标地址。所以,so 文件中必须包含一些重定位相关的信息,linker 据此完成重定位的工作。

    链接时重定位

    在.o文件链接时将发生重定位
    这些重定位信息保存在一系列的重定位项中(.rel.dyn等表),重定位项的结构如下:

    typedef struct
    {
        Elf32_Addr  r_offset;       /* Address */
        Elf32_Word  r_info;         /* Relocation type and symbol index */
    } Elf32_Rel;
    
    typedef struct
    {
        Elf32_Addr  r_offset;       /* Address */
        Elf32_Word  r_info;         /* Relocation type and symbol index */
        Elf32_Sword r_addend;       /* Addend */
    } Elf32_Rela;
    

    r_offset
    本数据成员给出重定位所作用的位置。对于重定位文件来说,此值是受重定位作用的存储单元在节中的字节偏移量;对于可执行文件或共享ELF文件来说,此值是受重定位作用的存储单元的虚拟地址。

    r_info
    本数据成员既给出了重定位所作用的符号表索引,也给出了重定位的类型。以下是应用于 r_info 的宏定义。

    #define ELF32_R_SYM(val)        ((val) >> 8)  //得到符号表的索引
    #define ELF32_R_TYPE(val)       ((val) & 0xff)  //得到type
    #define ELF32_R_INFO(sym, type)     (((sym) << 8) + ((type) & 0xff))
    

    r_addend
    本成员指定了一个加数,这个加数用于计算需要重定位的域的值。

    Elf32_Rela 与 Elf32_Rel 在结构上只有一处不同,就是前者有 r_addend。Elf32_Rela 中是用r_addend 显式地指出加数;而对 Elf32_Rel来说,加数是隐含在被修改的位置里的。Elf32_Rel中加数的形式这里并不定义,它可以依处理器架构(ELF32_R_TYPE(info))的不同而自行决定。
    计算方式
    计算方式是根据ELF32_R_TYPE宏定义得到类型决定的,我给出一些386架构的计算方式:
    被重定位域(relocatable field)是一个 32 位的域,占 4 字节并且地址向 4 字节对齐,其字节序与所在体系结构下其他双字长数据的字节序相同。重定位项用于描述如何修改如下的指令和数据域:

    image.png
    为了下面的描述方便,这里定义以下几种运算符号:
    • A 表示用于计算重定位域值的加数。
    • B 表示在程序运行期,共享ELF被装入内存时的基地址。一般来说,共享ELF文件在构建时基地址为 0,但在运行时则不是。
    • G 表示可重定位项在全局偏移量表中的位置,这里存储了此重定位项在运行期间的地址。更多信息参见下文“全局偏移量表”。
      -GOT 表示全局偏移量表的地址。
    • L 表示一个符号的函数连接表项的所在之处,可能是节内偏移量,或者是内存地址。函数连接表项把函数调用定位到合适的位置。在构建期间,连接编辑器创建初始的函数连接表;在运行期间,动态连接器会修改表项。更多信息参见“函数连接表”部分。
    • P 表示被重定位的存储单元在节内的偏移量或者内存地址,由 r_offset 计算得到。
    • S 表示重定位项中某个索引值所代表的符号的值。
      重定位类型指定了哪些位需要被修改以及如何算计它们的值,下面使用x86系统处理器的重定位类型的计算方法说明。
    名字 数据类型 计算
    R_386_GOT32 3 word32 G+A
    R_386_PLT32 4 word32 L+A-P
    R_386_COPY 5 none none
    R_386_GLOB_DAT 6 word32 S
    R_386_JMP_SLOT 7 word32 S
    R_386_RELATIVE 8 word32 B+A
    R_386_GOTOFF 9 word32 S+A-GOT
    R_386_GOTPC 10 word32 GOT+A-P

    编译链接

    编译器编译源代码后生成的文件叫做目标文件,从目标文件的结构上讲,它是已经编译后的可执行文件格式,只是还没有链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。

    简单的说,目标文件就是源代码编译后但未进行链接的那些中间文件(Winodws的.obj和Linux下的.o) ,它跟可执行文件的内容结构很相似,所以一般跟可执行文件格式一起采用一种格式存储。从某种意义上,可以把目标文件和可执行文件看成是一种类型的文件。在Linux下,称之为ELF文件。

    libc简介
    在Linux中,常用的C语言库运行库glibc动态链接形式保存在"/lib"目录下,文件名叫做“libc.so”,整个系统只保留一份C语言库的动态链接文件“libc.so”,而所有的C语言编写的、动态链接的程序都可以在运行时使用它。

    当程序被装载时,系统的动态链接器 会将程序所需的动态链接库装载到进程的地址空间,并且将程序中所有未决议的符号绑定到相应的动态链接库中,并进行重定位工作。

    装载时重定位

    基本思路是:在链接时,对所有绝对地址的引用不作重定位,而把这一步推迟到装载时再完成。一旦模块装载地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址引用进行重定位。

    假设函数foobar相对于代码段的起始地址是0x100,当模块被装载到0x10000000时,我们假设代码段位于模块的最开始,即代码段的装载地址也是0x10000000,那么我们就可以确定foobar的地位为0x1000100。这时候,系统遍历模块中的重定位表,把所有对foobar的地址引用都重定位至0x10000100。

    地址无关代码(PIC)

    装载时重定位解决了动态模块中有绝对地址引用的问题,但是又带了指令部分无法在多个进程间共享的问题。

    具体想法就是把程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变。把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本。这种方案就是目前的地址无关代码(PIC)技术

    具体方法:先分析模块中各种类型的地址引用方式,把共享对象模块中地址引用按照是否跨模块分为两类:模块内部引用和模块外部引用。
    ;按照不同的引用方式又可以分为指令引用和数据访问。


    image.png

    全局偏移表(GOT)

    对于类型三,我们需要用到代码地址无关(PIC)技术,基本的思想就是把跟地址相关部分放到数据段里面。

    ELF的做法是在数据段里建立一个指向这些变量的指针数据,称为全局偏移表(GOT),当代码需要引用该全局变量时,可以通过GOT中相对应的项间接引用。


    image.png

    如图,当指令需要访问变量b时,程序先会找到GOT,然后根据GOT中的变量所对应的项找到变量的目标地址。
    由于GOT本身是放在数据段的,所以它可以在模块装载时被修改,并且每个进程都可以有独立的副本,相互不受影响。

    对于模块间调用和跳转,GOT中保存的是目标函数的地址,可以借助GOT中的项进行间接跳转。
    方法:先得到当前指令地址PC,然后加上一个偏移地址得到函数地址在GOT中的偏移,然后一个间接调用


    image.png

    延迟绑定(PLT)

    基本思想
    动态链接以牺牲一部份性能为代价。PLT是另一种优化动态链接性能的方法。

    在动态链接下,程序模块之间包含了大量的函数引用,所以在程序开始执行前,会耗费不少时间解决函数引用的符号查找以及重定位。
    但是,在一个程序运行过程中,可能很多函数在程序执行完时都不会被用到,比如一些错误处理函数。
    所以ELF采用了一种叫做延迟绑定的做法。
    基本思想:就是当函数第一次被用到时才进行绑定。如果没有用则不进行绑定,所以在开始时模块间的函数调用都没有进行绑定,而是需要用到时才绑定。

    具体做法
    动态链接器需要某个函数来完成地址绑定工作,这个函数至少要知道这个地址绑定发生在哪个模块 哪个函数,如lookup(module,function)。

    在glibc中,lookup的函数真名叫做_dl_runtime_reolve()

    • 当我们调用某个外部模块时,调用函数并不直接通过GOT跳转,而是通过一个叫做PLT项的结构来进行跳转,每个外部函数在PLT中都有一个相应的项,比如bar()函数在PLT中的项地址叫做bar@plt,具体实现
    bar@plt:
    
       jmp *(bar@GOT)
    
       push n
    
       push moduleID
    
      jump _dl_runtime_resolve
    

    第一条指令是一条通过GOT间接跳转指令,bar@GOT表示GOT中保存bar()这个函数的相应项。

    但是为了实现延迟绑定,连接器在初始化阶段没有将bar()地址填入GOT,而是将“push n”的地址填入到bar@GOT中,所以第一条指令的效果是跳转到第二条指令,相当于没有进行任何操作。第二条指令将n压栈,接着将模块ID压栈,跳转到_dl_runtime_resolve。实际上就是lookup(module,function)的调用。

    _dl_runtime_resolve()在工作完成后将bar()真实地址填入bar@GOT中。

    一旦bar()解析完毕,再次调用bar@plt时,直接就能跳转到bar()的真实地址。

    实际实现
    PLT的真正实现要更复杂些,ELF将GOT拆分成两个表“.got”和".got.plt",前者用来保存全局变量引用的地址,后者用来保存函数引用的地址。

    也就是说,所有对于外部函数的引用被分离出来放到了“.got.plt”中

    最后在给出一些要用到一些表
    .rel.text
    重定位的地方在.text段内,以offset指定具体要定位位置。在连接时候由连接器完成。注意比较.text段前后变化。指的是比较.o文件和最终的执行文件(或者动态库文件)。就是重定位前后比较,以上是说明了具体比较对象而已。

    .rel.dyn
    重定位的地方在.got段内。主要是针对外部数据变量符号。例如全局数据。重定位在程序运行时定位,一般是在.init段内。定位过程:获得符号对应value后,根据rel.dyn表中对应的offset,修改.got表对应位置的value。另外,.rel.dyn 含义是指和dyn有关,一般是指在程序运行时候,动态加载。区别于rel.plt,rel.plt是指和plt相关,具体是指在某个函数被调用时候加载。

    .rel.plt
    重定位的地方在.got.plt段内(注意也是.got内,具体区分而已)。 主要是针对外部函数符号。一般是函数首次被调用时候重定位。可看汇编,理解其首次访问是如何重定位的,实际很简单,就是初次重定位函数地址,然后把最终函数地址放到.got.plt内,以后读取该.got.plt就直接得到最终函数地址(参考过程说明)。 所有外部函数调用都是经过一个对应桩函数,这些桩函数都在.plt段内。

    过程说明:调用对应桩函数--->桩函数取出.got表(具体是.got.plt)表内地址--->然后跳转到这个地址.如果是第一次,这个跳转地址默认是桩函数本身跳转处地址的下一个指令地址(目的是通过桩函数统一集中取地址和加载地址),后续接着把对应函数的真实地址加载进来放到.got.plt表对应处,同时跳转执行该地址指令.以后桩函数从.got.plt取得地址都是真实函数地址了。

    .plt段,存放重定位桩函数的。

    强调说明
    .rel.text属于普通重定位辅助段 ,他由编译器编译产生,存在于obj文件内。连接器连接时,他 用于最终可执行文件或者动态库的重定位。通过它修改原obj文件的.text段后,和并 到 最终可执行文件或者动态文件的.text段。
    注:readelf -r a.o 查看 .rel.text。其类型一般为R_386_32和R_386_PC32

    .rel.dyn和.rel.plt是动态定位辅助段。由连接器产生,存在于可执行文件或者动态库文件内。借助这两个辅助段可以动态修改对应.got和.got.plt段,从而实现运行时重定位。
    .rel.dyn 对应地点在.got表内;.rel.plt 在.got.plt,注意不是在.text,这点和普通不同,也是重要点。

    .rel.text由编译器产生,然后在连接时候,由链接器负责根据.rel.text对.text段进行修改,从而达到重定位目的;

    .rel.dyn和.rel.plt由连接器产生,然后在运行时候,动态加载符号地址。
    对于数据,根据.rel.dyn找到.got中的offset位置;
    对于函数则通过.plt桩函数和.rel.plt段来获取函数真实地址,然后存在于.got.plt。
    要理解动态连接中访问外部符号是通过.got和.got.plt

    本文引用以下链接:
    ELF重定位简介
    ELF文件系列第五篇ELF文件静态结构中的重定位项
    浅析ELF中的GOT与PLT
    linux下elf重定位理解

    相关文章

      网友评论

          本文标题:ELF探究 之 重定位(二)

          本文链接:https://www.haomeiwen.com/subject/obkffktx.html