精华内容
下载资源
问答
  • 本文以Ian Wienand的博客为蓝本,我在必要的地方予以增补、解释以及再实验,希望读者对PLT和GOT有一个初步的、相对完整的认识。 0×01The Simplest Example 原文的标题是“PLT and GOT – the key to ...

    转自https://www.freebuf.com/articles/system/135685.html

     

    0×00 Intro

    本文以Ian Wienand博客 为蓝本,我在必要的地方予以增补、解释以及再实验,希望读者对PLT和GOT有一个初步的、相对完整的认识。

    0×01 The Simplest Example

    原文的标题是“PLT and GOT – the key to code sharing and dynamic libraries”,可见PLT和GOT对代码复用以及动态库的关键作用。

    共享库(shared library)是现代操作系统的组成部分,但其内部机制却很少有人去了解。当然,有很多解释共享库机制的文章,希望本篇博客能为这方面的知识体系加一把火。

    OK,我们从最起始部分讲起—-在二进制文件(比如object file)中会有一段叫relocations的部分,这部分的内容在链接时候(link time)再进行敲定确切的值,注意链接可以发生在运行前(称为静态链接,toolchain linker),也可发生在运行时(称为动态链接,dynamic linker)。具体relocations部分中的内容就是在讲:“确定X这个符号(symbol)的值,然后把这个值写到二进制文件的Y偏移处”。每一条relocation都有确定的类型(定义与ABI文档中),从而确切地说明每个类型的值到底该如何敲定。

    如下为最简单的例子:

     

    我们可以看到”foo”这个符号的Sym.Value是0,说明在把a.c编译成a.o的时候,“foo”这个符号的值还不知道,所以编译就在Sym.Value处先写0,然后在Type这个位置写上R_X86_64_PC32,从而告诉之后要进行link的链接器:”在最终生成的可执行文件的.text部分的0×6这个偏移位置,patch上foo这个符号的地址值”。如果我们看一下a.o这个对象文件的.text部分0×6这个位置,我们会看到如图绿线部分:

     

    觉得刚刚看到这张图片,可能就会有些刚刚接触这些概念的同学就有些懵了。别急,我一点点地讲这张图。首先一个二进制可执行文件可被划分为多个部分:

     

    这张图片截取自《程序员的自我修养》这本书,真心希望像搞懂“程序是如何跑起来的?”这种问题的同学,去读一下这本书,你会觉得很值的,这是一个很本质的问题,而书中解释得那样的清晰,总之,强烈推荐!

    .text部分会放着这个二进制文件的执行代码,所以当我们用objdump进行反汇编的时候,就会看到a.o这个二进制文件的.text部分的机器码,前两条指令用于布置好栈空间(这部分可参考William Stallings的Computer Security那本书的第十章)不是本文的重点,后两条指令用于清理栈空间,并返回调用函数,也不是本文重点。重点在第三条语句,它在.text的Offset 0×4处。我们可以看到,a.o仅仅是一个经过编译后的object file,所以其中的真实值并没有敲定,从而看到绿线处暂时填4个00。

    好,下面我问一个问题:当CPU执行到0×4出这句指令时,%rip(即PC)的值为多少?两个选项,A. 0×4 ;B. 0xa。答案:B。参考CSAPP第四章,截图如下:

     

    其实,计算机在执行一条指令需要一个指令周期,不同的CPU架构的指令周期可以分为不同阶段,上图中我们可看到Intel x86架构CPU的指令周期分为:取指,译码,执行,访存,写回,PC update,这六个阶段。这里,我说一下我对PC的理解,图中绿线部分,vaIP对应%rip寄存器,%rip趋向于一种实际意义层面上的理解,而PC更趋向于一种意向意义层面上的理解。具体来说,在PC update阶段,CPU才去关注”我要去执行的下一条指令在哪里”(通过关注%rip中的值),而在CPU关注“下一条指令在哪里”之前(即PCupdate周期之前),在Fetch阶段,%rip寄存器按图中绿线的指示,其值便已经改变了,(改变方法是:当前指令的PC加上当前指令的长度,图中subl %edx, %ebx这条指令长度为2,所以vaIP被赋值为PC+2)。简单理解,%rip就是PC,在CPU这执行当前指令时,%rip便已经指向了下一条指令所在的地址处了,只是到了PC update环节,CPU才去看%rip的值(当然在Execute环节,CPU也可以使用%rip,但并不是为了程序的执行流而去关注%rip,此时就纯粹那%rip作为一个存值的寄存器来使用,而且此时的%rip已经指向下一条指令了)。

    对于我上述理解的支撑材料是,王爽老师那本《汇编语言》的第二章的2.10节有关CPU如何执行一条指令的一系列图示。这里由于图示很长,我就不粘贴了,简单讲就是,CPU还没开始执行具体指令的时候,IP的值便已经指向下一条指令了。

    好了,说了这么多,我想,我可以开始解释0×4处这条指令的意思了,就是将0×0 + %rip这个地址处所存的值,赋给%eax。而此时,%rip的值,根据我们上面问的问题的描述,应为0xa(下一条指令的地址),所以这条指令的含义进一步解释为,以%rip为基址,以0×0为偏移的内存地址处取内容,给到%eax。而此时偏移之所以为0×0,是因为还没有经过链接过程,所以真实的偏移地址还没有敲定,所以暂时写0×0。

    明白了该指令的含义,但这句到底是在干啥啊?我们回头看一下a.c源码,foo是一个extern(外部)的int型数据,函数function的返回类型也是一个int型数据,其内容为将foo给return出来。我们知道a.o仅仅是经过编译的,编译器会说:“我不知道foo这个外来int会来自于那个.o文件,那是链接器的活!我就姑且把foo所在的内存地址偏移定为0×0”。而返回值一般都保存在%eax这个寄存器中,所以0×4处这个指令的含义就是function函数的返回值为0 。

    经过link之后,该偏移会被patch成foo的真实地址偏移。

    0×02 Position-Independent Code

    接着上面,如果foo这变量的值出现在其他的对象文件(比如b.o)中,那么便可以通过静态链接,来将a.o和b.o链接到一个executable中,而在executable中,原来a.o中relocations部分foo相关的条目便可以去掉了,因为foo的真实地址,已经被linker,根据b.o中有关foo的信息,给patch好了。但是,“ there is a whole bunch of stuff for a fully linked executable or shared-librarythat just can’t be resolved until runtime. ”,对于一个executable或者共享库,有许多事,只有到了运行时(runtime)才能敲定。比如,我们即将讲到的位置无关代码(position-independent code ,PIC)。

    首先,我们先看一下位置相关代码(清晰起见,用作者的32位机的例子即可):

    $ readelf --headers /bin/ls

    [...]

    ELF Header:

    [...]

      Entry point address:   0x8049bb0

     

    Program Headers:

      Type   Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align

    [...]

      LOAD   0x000000 0x08048000 0x08048000 0x16f88 0x16f88 R E 0x1000

      LOAD   0x016f88 0x0805ff88 0x0805ff88 0x01543 0x01543 RW  0x1000

    可见ls这个可执行文件,它有一个fixed的加载位置,即代码部分(其flag是R和E)必须加载到0×08048000(注意这个是物理地址),而数据部分(其flag是R和W)必须加载到0x0805ff88。这种位置相关的代码,固然有其好处,不用再在runtime的时候去算一些地址信息什么的了,因为地址都是fixed。

    不过,这种fixed地址对shared library(.so文件)并没有好处。so文件的一个核心观点就是拿过来,加载到任意一段物理内存上就用。而如果so文件必须被加载到一个特定的地址上才能运行的话,我们可能就得把计算机上所有so文件都给一个特定的加载地址,以保证在使用这些so文件时不会有重叠(overlap),这其实也是预链接(prelinking)要做的事。但对于一个32位机,你这么做的话,马上内存就分配完了。所以,还是得考虑一种位置无关代码的方案。实际上,当我们去检查一个so文件的时候,会看到它们并不会指定一个特定的加载基地址(可见RE flag标示的代码部分的物理地址为0,即不会指定加载基地址):

    $ readelf --headers /lib/libc.so.6

    Program Headers:

      Type   Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align

    [...]

      LOAD   0x000000 0x00000000 0x00000000 0x236ac 0x236ac R E 0x1000

      LOAD   0x023edc 0x00024edc 0x00024edc 0x0015c 0x001a4 RW  0x1000

    共享库的第二个目标就是代码重用(code sharing)。对于shared library,我们需要让它的code段保持不变,如果变了的话,那么100个进程调用这段代码,因为code段会变的原因,就要占用100段物理内存空间,这肯定不是我们想要的,所以要保持so文件中code段不能动,从而只需将这个code段加载到一个特定的物理地址处,每个调用该so文件的进程的虚存指向这个物理地址就可以了。还有就是,so文件中data段的相对位置也不能变,从而使写死的code段,能够通过相对位置来找到data段(上面的headers中,我们可以看到data段的offset是0x023edc)。这样,通过virtual memory的神奇机制(同一虚拟地址,可以映射到不同的物理地址),“every process sees its own data section but can share the unmodified code”。于是对于每一个进程,要找到其特有的data段时,使用简单的数学即可:我当前的地址(code段某处) + 已知的相对位置 = 我索引的data段 。

    0×03 Global Offset Table

    不过说着轻松,想确定“我当前的地址”可不是件容易的事,比如有如下代码:

    $ cat test.c

    static int foo = 100;

     

    int function(void) {

    return foo;

    }

    $ gcc -fPIC -shared -o libtest.so test.c

    -shared告诉编译器生成so文件,-fPIC告诉编译器生成位置无关的代码,综合在一起就是生成位置无关的so文件,这两个通常配套使用。

    这里foo是static的,所以它会保存在so文件的data段中。对于64位机(amd64),因为可以直接访问%rip,所以当前指令地址很好获得:

    000000000000056c <function>:

     56c:55 push   %rbp

     56d:48 89 e5   mov%rsp,%rbp

     570:8b 05 b2 02 20 00  mov0x2002b2(%rip),%eax# 200828 <foo>

     576:5d pop%rbp

    第三条是重点(其余不属于本文讨论范文),我们在上一节接触过类似的,按照相对位置%rip+0x2002b2,取到该地址处的内容(即data),将其赋给%eax。对于64位机,就这么简单。

    但对于32位机(i386)的话,就要麻烦一些了,因为32位架构下接触不到PC。所以,需要一点小技巧:

    0000040c <function>:

     40c:55 push   %ebp

     40d:89 e5  mov%esp,%ebp

     40f:e8 0e 00 00 00 call   422 <__i686.get_pc_thunk.cx>

     414:81 c1 5c 11 00 00  add$0x115c,%ecx

     41a:8b 81 18 00 00 00  mov0x18(%ecx),%eax

     420:5d pop%ebp

     421:c3 ret

     

    00000422 <__i686.get_pc_thunk.cx>:

     422:8b 0c 24   mov(%esp),%ecx

     425:c3 ret

    0x40f处一个call指令调用__i686.get_pc_thunk.cx函数:先将call指令的下一条指令的地址(这里是0×414)压到栈上,然后蹦到这个函数地址处(0×422)开始执行:将刚刚压栈的下一条指令的地址赋给%ecx,然后ret到0×414继续执行add指令:0x115c + 0×414 = 0×1570。然后再到0x41a把0×1570 + 0×18 = 0×1588地址处的内容,给到%eax寄存器中。这里我们可以去看一下0×1588中写着什么:

    00001588 <global>:

    1588:   64 00 00add%al,%fs:(%eax)

    值为0×0000000064=100,正是源码中static int 类型的变量foo的值100。

    我们注意到,上面源码中foo是static的,即使属于该so文件本身的。那么,如果一个so文件,想去索引其他的so文件中的data怎么办?当然,我们可以patch这个so文件的代码部分,直接把那个data的位置为patch上,但这样就破坏了so文件的 code-sharability。而计算机中有一个原理就是:所有问题都可以通过加一个间接层来解决。这里,这个间接层就叫global offset table or GOT.

    考虑下面情况:

    $ cat test.c

    extern int foo;

     

    int function(void) {

    return foo;

    }

    $ gcc -shared -fPIC -o libtest.so test.c

    这里foo是一个extern外部变量,大概来自什么其他的库文件吧。我们看一下在amd64架构上的情况吧:

    $ objdump --disassemble libtest.so

    [...]

    00000000000005ac <function>:

     5ac:55 push   %rbp

     5ad:48 89 e5   mov%rsp,%rbp

     5b0:48 8b 05 71 02 20 00   mov0x200271(%rip),%rax# 200828 <_DYNAMIC+0x1a0>

     5b7:8b 00  mov(%rax),%eax

     5b9:5d pop%rbp

     5ba:c3 retq

     

    $ readelf --sections libtest.so

    Section Headers:

      [Nr] Name  Type Address   Offset

       Size  EntSize  Flags  Link  Info  Align

    [...]

      [20] .got  PROGBITS 0000000000200818  00000818

       0000000000000020  0000000000000008  WA   0 0 8

     

    $ readelf --relocs libtest.so

    Relocation section '.rela.dyn' at offset 0x418 contains 5 entries:

      Offset  Info   Type   Sym. ValueSym. Name + Addend

    [...]

    000000200828  000400000006 R_X86_64_GLOB_DAT 0000000000000000 foo + 0

    我们可以看到返回值来自于%rip + 0×200271 = 0×200828。我们再看一下这个so文件的headers,可见0×200828属于.got范围内。然后,我们在看一下这个so文件的relocations,我们看到“R_X86_64_GLOB_DAT ”告诉链接器:“链接器啊,你去到其他对象文件中找一下foo的值,然后把它patch到0×200828这个位置”。

    所以,当动态加载器加载该so文件时,会先去看它的relocations,然后找到foo的值,然后把它patch到该so文件的属于.got部分的0×200828地址处。当code段索引到foo这个值的时候,直接到.got的0×200828处去拿就好了,everything just works:code段也不需要修改,从而就没有破坏so文件的code sharability。

    0×04 Procedure Linkage Table

    上节我们可以处理so文件对外部变量的引用了,但如果是外部函数调用呢?此处,我们使用的“间接层”叫做procedure linkage table or PLT。code只会通过PLT stub(PLT桩代码,其实就是一小段代码),实现外部函数调用。如下例子:

    $ cat test.c

    int foo(void);

     

    int function(void) {

    return foo();

    }

    $ gcc -shared -fPIC -o libtest.so test.c

     

    $ objdump --disassemble libtest.so

    [...]

    00000000000005bc <function>:

     5bc:55 push   %rbp

     5bd:48 89 e5   mov%rsp,%rbp

     5c0:e8 0b ff ff ff callq  4d0 <foo@plt>

     5c5:5d pop%rbp

     

    $ objdump --disassemble-all libtest.so

    00000000000004d0 <foo@plt>:

     4d0:   ff 25 82 03 20 00   jmpq   *0x200382(%rip)# 200858 <_GLOBAL_OFFSET_TABLE_+0x18>

     4d6:   68 00 00 00 00  pushq  $0x0

     4db:   e9 e0 ff ff ff  jmpq   4c0 <_init+0x18>

     

    $ readelf --relocs libtest.so

    Relocation section '.rela.plt' at offset 0x478 contains 2 entries:

      Offset  Info   Type   Sym. ValueSym. Name + Addend

    000000200858  000400000007 R_X86_64_JUMP_SLO 0000000000000000 foo + 0

    执行function这个函数时,我们看到有一句callq指令,从而执行流跳到0x4d0处执行,该处为jmpq *0×200382(%rip),即以0×200382 + 0x4d6 = 0×200858地址处取内容作为地址,进行跳转。那么我们看一下第一次这么执行时,0×200858处的内容是什么:

    $ objdump --disassemble-all libtest.so

     

    Disassembly of section .got.plt:

     

    0000000000200840 <.got.plt>:

      200840:   98  cwtl

      200841:   06  (bad)

      200842:   20 00   and%al,(%rax)

    ...

      200858:   d6  (bad)

      200859:   04 00   add$0x0,%al

      20085b:   00 00   add%al,(%rax)

      20085d:   00 00   add%al,(%rax)

      20085f:   00 e6   add%ah,%dh

      200861:   04 00   add$0x0,%al

      200863:   00 00   add%al,(%rax)

      200865:   00 00   add%al,(%rax)

    jmpq是quadra word,即8个字节,所以取0x00000000000004d6(即从地址0×200858到0x20085f地址,取这8个字节,注意小端模式,所以倒着写)。这恰好是jmpq的下一条指令的地址(注意,这是第一次call foo这个外部函数时的情形)。0x4d6的指令是,push $0×0,这里这个push的0是foo这个函数符号在.rela.plt数据结构中的下标(也可称为索引或者index),用于定位这个foo这个符号(具体参见《程序员的自我修养》,估计以后我也会专门写一篇有关对象文件构成的文章)。执行完push后,就jmp到了0x4c0这个位置,我们看一下0x4c0处写了什么:

    00000000000004c0 <foo@plt-0x10>:

     4c0:   ff 35 82 03 20 00   pushq  0x200382(%rip)# 200848 <_GLOBAL_OFFSET_TABLE_+0x8>

     4c6:   ff 25 84 03 20 00   jmpq   *0x200384(%rip)# 200850 <_GLOBAL_OFFSET_TABLE_+0x10>

     4cc:   0f 1f 40 00 nopl   0x0(%rax)

    又push了一个值,这个值是什么呢?在后面注释中我们可以看到_GLOBAL_OFFSET_TABLE_+0×8这种字样,它又是什么呢?

    我先查阅了CSAPP,看到了如下内容:

     

    然后,查阅了《程序员的自我修养》,看到如下内容:

     

    所以,我的理解是,我们可以把_GLOBAL_OFFSET_TABLE_理解成为一个类似数组一样的东西,每个元素保存着一个地址,32位机就是4字节地址,64位机就是8字节地址。

    所以_GLOBAL_OFFSET_TABLE_+0×8就是_GLOBAL_OFFSET_TABLE_的第二个元素,参考上面两幅图片可知,这个代表了libtest.so(即模块的ID),_GLOBAL_OFFSET_TABLE_+0×10则为_GLOBAL_OFFSET_TABLE_的第三个元素,为动态解析函数的入口地址。

    然后在0x4c6,跳到这个动态解析函数,开始对foo这个函数名进行解析,找到其地址,并patch到0×200858这个位置(这个位置在GOT中),从而第二次调用foo的时候,jmp的地址就不是0x00000000000004d6了,而是foo的实际地址了。

    这种,第一次调用foo函数,通过PLT stub(PLT桩代码)进行动态链接(地址解析),找到foo函数真实地址并patch GOT表,第二次直接通过PLT桩代码跳到foo函数真实地址的方式,称为“延迟绑定技术”(lazy binding)

    好了,以上便是PLT技术的一些细节实现。另外多说一句,我们可以在运行某使用so库的可执行文件时,使用LD_PRELOAD 来修改动态链接时,符号解析的顺序。也就是说,LD_PRELOAD会告诉动态链接器,想找什么符号的话,先从这里找,如果LD_PRELOAD中所提供so库里有foo这个符号,那么动态链接器会首先到这里找foo的地址,而不会去其他so库中去找。

    0×05 Summary

    so库的代码段要保持只读,而且数据段也要为各个进程所私有。需要在编译时通过已知的各个符号的偏移量,建立GOT和PLT表,从而间接地达成第一句的目标。

     

    展开全文
  • 理解got和plt

    千次阅读 2018-09-10 15:23:42
    PLT, Procedure Linkage Table, 过程链接表 GOT, Global Offset Table, 全局偏移表 简略来说就是从PLTGOT找api真实地址。 共享库是现代操作系统的一个重要组成部分,但是我们对它背后的实现知之甚少。当然,很多...

    PLT, Procedure Linkage Table, 过程链接表
    GOT, Global Offset Table, 全局偏移表

    简略来说就是从PLT去GOT找api真实地址。

    rel表的作用是:

    本程序装载进内存时,通过自己的rel表项告诉链接器,哪些地方需要重定位。

    got表的作用是:

    用来存放链接器找到的 函数/变量地址。

    plt表的作用是:
    在动态链接过程中, 函数在加载共享库之后,会对got表进行重定向。当程序执行到引用变量或调用函数时候利用plt跳转到got表中项指定的地址即可。

     

     

    共享库是现代操作系统的一个重要组成部分,但是我们对它背后的实现知之甚少。当然,很多文档从各个角度对动态库进行过介绍。希望我的这边文章能给对动态库的理解带来一种新的理解。
    让我们以此开始——在elf格式中,重定位记录是一些允许我们稍后填写的二进制信息——链接阶段由编译工具填充或者在运行时刻由动态连接器填写。一个二进制的重定位记录从本质上说就是“确定符号X的值,然后把这个值放入二进制文件中的偏移量为Y的地方”——每一个重定向记录都有个特定的类型,这个类型在ABI文档中定义,用来准确的描述在实际中是如何确定X的值。

    下面是一个简单的例子:

    $ cat a.c
    extern int foo;
     
    int function(void) {
        return foo;
    }
    $ gcc -c a.c
    $ readelf --relocs ./a.o
     
    Relocation section '.rel.text' at offset 0x2dc contains 1 entries:
     Offset     Info    Type            Sym.Value  Sym. Name
    00000004  00000801 R_386_32          00000000   foo
    

    在编译生成a.o文件的时候,编译器并不知道符号foo的值,所以预留一个重定位记录(类型为R_386_32),表示“在最终的二进制文件中,把这个目标文件中符号foo的地址填入偏移量为4的地方(相对于text 区而言)”。如果你观察下a.o的汇编结果,你就会发现在text区偏移量为4的地方,有4个字节为0,这四个字节最终将会填入真实的地址。

    $ objdump --disassemble ./a.o
     
    ./a.o:     file format elf32-i386
     
     
    Disassembly of section .text:
     
    00000000 <function>:
       0:    55                     push   %ebp
       1:    89 e5                  mov    %esp,%ebp
       3:    a1 00 00 00 00         mov    0x0,%eax
       8:    5d                     pop    %ebp
       9:    c3                     ret
    

    在链接的时候,如果你编译的另外一个目标文件含有foo的地址,并且把这个目标文件与a.o一起编译为一个最终的可执行文件,那么重定位记录就会消失。但是仍然有很多的东西直到运行的时候才能确定,当编译一个可执行文件或者动态库的时候。正如我马上要解释的,PIC,与地址无关的代码是一个很重要的原因(PIC,即Position independent code,直接翻译就是位置无关代码,简单的说就是这个代码可以被load到内存的任意位置而不用做任何地址修正。这里指的是代码段,数据段可能需要地址修正。)。当你观察一个可执行文件,你会注意到它有一个固定的加载地址:

    $ readelf --headers /bin/ls
    [...]
    ELF Header:
    [...]
      Entry point address:               0x8049bb0
     
    Program Headers:
      Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
    [...]
      LOAD           0x000000 0x08048000 0x08048000 0x16f88 0x16f88 R E 0x1000
      LOAD           0x016f88 0x0805ff88 0x0805ff88 0x01543 0x01543 RW  0x1000
    

    这并不是地址无关。代码段(权限为RE,可读可执行)必须被加载到虚拟地址0x08048000,数据段(RW)必须被加载到0x0805ff88。

    这对于可执行文件来说很不错,因为每一次你创建一个新的进程(fork,然后exec),都会有一个全新的地址空间。考虑到时间的消耗提前计算好地址并把它们固定到最终的输出文件中,这种方式是值得考虑的。(当然也可以采取 与地址无关的可执行文件 的方式来实现,但这是另外的一个话题了)

    这对于共享库来说就不是那么好了。关键点是,你可以为了达到你的目标而对共享库随意的组合。如果你的共享库必须要在固定的地址上运行,32位的系统的地址空间很快就不够用了。因此当你查看一个共享库,它们并不指定一个固定的加载地址:

    $ readelf --headers /lib/libc.so.6
    Program Headers:
      Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
    [...]
      LOAD           0x000000 0x00000000 0x00000000 0x236ac 0x236ac R E 0x1000
      LOAD           0x023edc 0x00024edc 0x00024edc 0x0015c 0x001a4 RW  0x1000
    

    共享库还有第二个目的,代码分享。如果有一百个进程使用一个共享库,就没有必要在内存中产生100分代码拷贝。如果代码是完全只读,并且永远不会修改,那么每一个进程就可以分享相同的代码。然而,对于共享库有一个约束:对于每一个进程都必须有一份自己的数据实例。然而,在运行时刻,将库数据放到任何我们想要的地址上也是可行的,这就要我们预留重定义记录为代码段打上“补丁”,告知代码段到哪里找到实际的数据——这种方法实际上是不行的,因为破环了动态库的代码只读属性和共享性。就如同你从头文件信息中看到的一样,解决方案为:可读可写的数据段相对于代码段有一个固定的偏移量。通过这种方式,利用虚拟内存的魔力,每个进程都有属于自己的数据段,而共享不可修改的代码段。所以访问数据段的算法是很简单的:我想访问的数据的地址 = 当前地址+ 固定偏移。

    但是,当前的地址有可能不是那么简单的知道:

    $ cat test.c
    static int foo = 100;
     
    int function(void) {
        return foo;
    }
    $ gcc -fPIC -shared -o libtest.so test.c
    

    foo位于数据段,与函数function中的指令有一个固定的偏移量。我们要做的就是找到它。在amd64上,这很简单:

    000000000000056c <function>:
     56c:        55                     push   %rbp
     56d:        48 89 e5               mov    %rsp,%rbp
     570:        8b 05 b2 02 20 00      mov    0x2002b2(%rip),%eax        # 200828 <foo>
     576:        5d                     pop    %rbp
    

    上面的代码的意思是说“把与当前指令地址偏移0x2002b2处的值放入eax”。另一方面,i386并没有提供访问当前指令偏移的能力。所以有一些限制:

    0000040c <function>:
     40c:    55                     push   %ebp
     40d:    89 e5                  mov    %esp,%ebp
     40f:    e8 0e 00 00 00         call   422 <__i686.get_pc_thunk.cx>
     414:    81 c1 5c 11 00 00      add    $0x115c,%ecx                    //%ecx=0x414
     41a:    8b 81 18 00 00 00      mov    0x18(%ecx),%eax
     420:    5d                     pop    %ebp
     421:    c3                     ret
     
    00000422 <__i686.get_pc_thunk.cx>:
     422:    8b 0c 24       mov    (%esp),%ecx
     425:    c3                     ret
    

    这里的魔数是__i686.get_pc_thunk.cx。i386不允许我们得到当前指令的地址,但是我们可以得到一个已知的固定地址——__i686.get_pc_thunk.cx的值,%ecx中的值是call的返回地址,这里是0x414.我们做一个简单的算术:0x115c+0x414 = 0x1570.最终的数据和0x1588偏移了0x18个字节,查看汇编代码:

    00001588 <global>:
        1588:       64 00 00                add    %al,%fs:(%eax)
    

    正是100所处的地址。

    现在我们越来越接近了,但是还是有很多的问题要处理。如果一个共享库可以被加载到任意的地址,那么,一个可执行文件或者其他的共享库,如何知道怎么访问它的数据或者调用它的函数呢?从理论上,我们是可以的,加载库,然后把数据的地址或者函数的地址填入到库相应的地方。然后这正如之前所讲的,违反了代码共享性。就如同我们所了解的,所有的问题都可以通过增加一个中间层来解决,在这种情形下,称之为全局偏移表(got)。

    考虑下面的库:

    $ cat test.c
    extern int foo;
     
    int function(void) {
        return foo;
    }
    $ gcc -shared -fPIC -o libtest.so test.c
    

    这和之前的文件很像,但是foo是extern的。假设是由其他的库提供。让我们看一下在amd64上它是如何工作的:

    $ objdump --disassemble libtest.so
    [...]
    00000000000005ac <function>:
     5ac:        55                     push   %rbp
     5ad:        48 89 e5               mov    %rsp,%rbp
     5b0:        48 8b 05 71 02 20 00   mov    0x200271(%rip),%rax        # 200828 <_DYNAMIC+0x1a0>
     5b7:        8b 00                  mov    (%rax),%eax
     5b9:        5d                     pop    %rbp
     5ba:        c3                     retq
     
    $ readelf --sections libtest.so
    Section Headers:
      [Nr] Name              Type             Address           Offset
           Size              EntSize          Flags  Link  Info  Align
    [...]
      [20] .got              PROGBITS         0000000000200818  00000818
           0000000000000020  0000000000000008  WA       0     0     8
     
    $ readelf --relocs libtest.so
    Relocation section '.rela.dyn' at offset 0x418 contains 5 entries:
      Offset          Info           Type           Sym. Value    Sym. Name + Addend
    [...]
    000000200828  000400000006 R_X86_64_GLOB_DAT 0000000000000000 foo + 0
    

    反汇编的结果显示返回值位于当前指令偏移0x200271处:0x0200828(0x5b7+0x200271 = 0x200828)。查看section header,这个地址位于.got区。接着我们查看重定位记录,可以发现有一个类型为R_X86_64_GLOB_DAT的重定位的意思是“找到foo的值,然后把它放在地址0x200828处”。

    所以,当这个动态库被加载,动态加载器将会检查重定位记录,找到foo的值,并按照要求为.got中的条目打上“补丁”。当动态库中的代码运行并访问foo的时候,访内指针将会指向正确的地址,一切都会正常工作,而不用去修改指令的值,以避免代码的共享性。

    以上是数据的处理,那么函数调用呢?函数调用的中间层称之为procedure linkage table 或者PLT.代码不会直接调用外部的函数,而是通过一个plt stub。

    $ cat test.c
    int foo(void);
     
    int function(void) {
        return foo();
    }
    $ gcc -shared -fPIC -o libtest.so test.c
     
    $ objdump --disassemble libtest.so
    [...]
    00000000000005bc <function>:
     5bc:        55                     push   %rbp
     5bd:        48 89 e5               mov    %rsp,%rbp
     5c0:        e8 0b ff ff ff         callq  4d0 <foo@plt>
     5c5:        5d                     pop    %rbp
     
    $ objdump --disassemble-all libtest.so
    00000000000004d0 <foo@plt>:
     4d0:   ff 25 82 03 20 00       jmpq   *0x200382(%rip)        # 200858 <_GLOBAL_OFFSET_TABLE_+0x18>
     4d6:   68 00 00 00 00          pushq  $0x0
     4db:   e9 e0 ff ff ff          jmpq   4c0 <_init+0x18>
     
    $ readelf --relocs libtest.so
    Relocation section '.rela.plt' at offset 0x478 contains 2 entries:
      Offset          Info           Type           Sym. Value    Sym. Name + Addend
    000000200858  000400000007 R_X86_64_JUMP_SLO 0000000000000000 foo + 0
    

    现在,我们function跳转到0x4d0.反汇编,我们看到这是一个有趣的调用,我们跳转到当前rip指针偏移0x200382,也就是0x200858处。可以发现,这个地址存放着符号foo的重定位的记录。

    I让我们来看一下0x200858的初始值:

    $ objdump --disassemble-all libtest.so
     
    Disassembly of section .got.plt:
     
    0000000000200840 <.got.plt>:
      200840:       98                      cwtl
      200841:       06                      (bad)
      200842:       20 00                   and    %al,(%rax)
            ...
      200858:       d6                      (bad)
      200859:       04 00                   add    $0x0,%al
      20085b:       00 00                   add    %al,(%rax)
      20085d:       00 00                   add    %al,(%rax)
      20085f:       00 e6                   add    %ah,%dh
      200861:       04 00                   add    $0x0,%al
      200863:       00 00                   add    %al,(%rax)
      200865:       00 00                   add    %al,(%rax)
            ...
    

    0x200858的初始值是0x4d6,居然是下一条指令的地址!这条指令把0要入栈中,然后跳转到0x4c0.通过查看代码我们可以发现,把GOT一个值压入栈中,然后跳到GOT中的第二个值。

    00000000000004c0 <foo@plt-0x10>:
     4c0:   ff 35 82 03 20 00       pushq  0x200382(%rip)        # 200848 <_GLOBAL_OFFSET_TABLE_+0x8>
     4c6:   ff 25 84 03 20 00       jmpq   *0x200384(%rip)        # 200850 <_GLOBAL_OFFSET_TABLE_+0x10>
     4cc:   0f 1f 40 00             nopl   0x0(%rax)
    

    这里究竟是在做什么呢?这就是 lazy binding(延迟绑定)——按照约定,动态连接器加载一个动态库,首先应该在got中的已知地址存放能够解析符号的默认函数。因此,上面的处理流程大体是这样子的:当第一次调用一个函数的时候,因为此时got中还没有它的地址,所以调用失败,从而进入默认的stub处理流程,这个stub用来解决符号解析。当找到foo的地址之后,就会把这个值填入到got,这样下次调用的时候,就直接调用到foo的实际地址。

    原文地址:
    https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html

     

     

    GOT表和PLT表知识详解

    转:https://blog.csdn.net/qq_18661257/article/details/54694748

    GOT表和PLT表在程序中的作用非常巨大,接下来的讲解希望大家可以仔细看看

    我们用一个非常简单的例子来讲解,代码如下: 
    图1

    这里写图片描述


    然后我们编译

    我们直接gdb ./a.out来进行反编译处理,然后通过disas main查看main函数中的反编译代码如下:

    图3

    这里写图片描述


    我们可以观察到gets@plt和puts@plt这两个函数,为什么后面加了个@plt,因为这个为PLT表中的数据的地址。那为什么反编译中的代码地址为PLT表中的地址呢。

    原因

    为了更好的用户体验和内存CPU的利用率,程序编译时会采用两种表进行辅助,一个为PLT表,一个为GOT表,PLT表可以称为内部函数表,GOT表为全局偏移表,这两个表是相对应的,什么叫做相对应呢,PLT表中的数据就是GOT表中的一个地址,如下图:

    图44

    这里写图片描述


    PLT表中的每一项的数据内容都是对应的GOT表中一项的地址,这个是固定不变的,到这里大家也知道了PLT表中的数据根本不是函数的真实地址,而是GOT表项的地址,好坑啊。

    其实在大家进入带有@plt标志的函数时,这个函数其实就是个过渡作用,因为GOT表项中的数据才是函数最终的地址,而PLT表中的数据又是GOT表项的地址,我们就可以通过PLT表跳转到GOT表来得到函数真正的地址。

    那问题来了,这个@plt函数时怎么来的,这个函数是编译系统自己加的,大家可以通过disas gets看看里面的代码,如下图:

    图55

    这里写图片描述


    大家可以发现,这个函数只有三行代码,第一行跳转,第二行压栈,第三行又是跳转,解释: 
    第一行跳转,它的作用是通过PLT表跳转到GOT表,而在第一次运行某一个函数之前,这个函数PLT表对应的GOT表中的数据为@plt函数中第二行指令的地址,针对图中来说步骤如下:

    1. jmp指令跳转到GOT表
    2. GOT表中的数据为0x400486
    3. 跳转到指令地址为0x400486
    4. 执行push 0x3#这个为在GOT中的下标序号
    5. 在执行jmp 0x400440
    6. 而0x400440为PLT[0]的地址
    7. PLT[0]的指令会进入动态链接器的入口
    8. 执行一个函数将真正的函数地址覆盖到GOT表中

    这里我们要提几个问题: 
    1. PLT[0]处到底做了什么,按照我们之前的思路它不是应该跳转到GOT[0]吗? 
    2. 为什么中间要进行push压栈操作? 
    3. 压入的序号为什么为0x3,不是最开始应该为0x0吗?

    解决问题

    问题1

    看下图: 
    图66

    这里写图片描述


    我们尝试着查看0x400440地址的数据内容发现一个问题,从0x400440−0x400450之间的数据完全不知道是什么,而真正的PLT[x]中的数据是从0x400450开始的,从这里才有了@plt为后缀的地址,但是我们disas gets看代码的时候是从0x400440开始的,我们可以通过x /5i 0x400440查看0x400440处的代码,如下: 
    图77

    这里写图片描述


    我们看到了后面的#之后又一个16进制数,一看便可以知道是GOT表的地址,为什么这么肯定呢,因为我们可以通过objdump -R ./a.out查看一个程序的GOT函数的地址,如下图: 
    图88

    这里写图片描述


    这里都是些GOT地址,我们发现都是0x601...这些,所以可以断定图77中的也是GOT地址,那么我们可以猜想出,在正式存储一个函数的GOT地址前,我们的PLT表前面有一项进行一些处理,我们暂且不具体深入剖析这些代码有什么用,但是我们可以肯定puts@plt前面那16个字节也算是PLT表中的内容,这其实就是我们的PLT[0],正如我们之前问题提到的那样,我们的PLT[0]根本没有跳转到GOT[0],它不像我们的PLT[1]这些存储的是GOT表项的地址,它是一些代码指令,换句话说,PLT[0]是一个函数,这个函数的作用是通过]GOT[1]和GOT[2]来正确绑定一个函数的正式地址到GOT表中来。

    咦,这里问题好像又产生了,本来按照最开始的思路PLT[1]也是跳转到GOT[1]的,GOT[2]同理,但是这两个数据好像被PLT[0]利用了,同时GOT[0]好像消失了,这里GOT[0]暂且不说它的作用是什么,针对GOT[1]和GOT[2]被PLT[0]利用,所以我们程序中真实情况其实是从PLT[1]到GOT[3],PPLT[2]到GOT[4],所以我们推翻了我们的图44,建立一张新的处理表

    图99

    这里写图片描述


    而plt[0]代码做的事情则是:由于GOT[2]中存储的是动态链接器的入口地址,所以通过GOT[1]中的数据作为参数,跳转到GOT[2]所对应的函数入口地址,这个动态链接器会将一个函数的真正地址绑定到相应的GOT[x]中。

    这就是PLT表和GOT表,总而言之,我们调用一个函数的时候有两种方法,一个是通过PLT表调用,一个则是通过GOT表调用,因为PLT表最终也是跳转GOT表,GOT表中则是一个函数真正的地址,这里需要注意的是,在一个函数没有运行一次之前,GOT表中的数据为@plt函数中下一条指令的地址,图55有说。

    问题2

    中间进行的压栈是为了确定PLT对应的GOT表项,即是PLT[1]−>GOT[3],0x3就是GOT的下标3,也就是说压栈后我们跳转到PLT[0],接着PLT[0]中的指令会通过这次压栈的序号来确定操作的GOT表项的位置

    问题3

    好像都在第一个问题都已经解决了,这里压入0x3的原因是因为,我们的GOT[0],GOT[1],GOT[2]都有额外用处。要从GOT[3]开始

     

     

    PIC的实现

    转:https://blog.csdn.net/cody_kai/article/details/6589263

    今天研究了下PIC,记录下。

    1)什么是PIC,为什么要PIC

    PIC,即Position independent code,直接翻译就是位置无关代码,简单的说就是这个代码可以被load到内存的任意位置而不用做任何地址修正。这里指的是代码段,数据段可能需要地址修正。
    PIC是share library机制的基础之一,要实现library在各个process之间可以share,代码必须是PIC。为什么?有了load时的relocation还不够么?答案是也可以够,但是仅仅有load时的relocation,基本等于没有用。share library的主要目的是让各个process可以共享common的代码(指代码段),这部分代码只在内存中占用一次内存,所有process共享这部分代码,而不需要每个process都有一份拷贝。因为代码段是要被share的,所以代码段的内容不能被改变。load时的relocation是在load时对代码做地址修正,所以一旦library被load了,这个library在所有共享该library的process的地址空间中的位置也确定了。有人会问这样有什么问题么?OS在第一次load这个library的时候完全可以找个available的地址空间阿,因为第一次load的时候,是可以被load到内存的任意地方的,所以只要有地址空间就可以。遗憾的是,这种情况下地址空间很可能不够,而导致没法load。考虑这样的问题,如果某人写了个捣蛋的library,占用很大的地址空间,一旦这个library被load了,就会导致其他library不能被load,因为已经没有available的地址空间了,这样这个系统就会崩溃。所以要实现share library,仅仅load时的relocation是不够的,我们需要一种机制,可以让library被load进process的任意地址空间,或者说library在不同的process中,可以被load到不同的地址空间,然后在OS层,通过OS的地址空间映射,来实现library在物理内存上的share。所以PIC是必须的。

    2)怎么实现PIC

    PIC需要解决的问题就是找到一种办法,避免load时的地址修正(relocation)。以下面的代码为例,该代码把内存中符号one_dword对应的地址的一个dword的内容放到%eax里。如果一个library包含下面的代码,则这个代码不是PIC的。因为$one_byte会随着该代码被load到不同的地址而有不同的值,这就导致了代码段在load到不同地址时,内容(第一句mov指令)会不同,这就导致了无法share。

    		.text
    		movl	$one_byte, %ebx
    		movl	(%ebx), %eax
    		.data
    		.align	4
    	one_dword:
    		.byte	1
    


    以ELF格式为例来说明PIC如何避免地址修正。在ELF格式中,各个代码段数据段都有固定的位置,代码段中某条代码的位置到数据段的地址都是固定的。所以如果某条指令要引用一个数据的时候,如果能得到当前指令的地址,就可以通过加上到数据段的固定偏移来找到这个数据。ELF格式中引入了Global Offset Table (GOT)来实现这个机制。GOT就是一系列地址的数组,包含了所有全局数据的地址。

    			|		|
    			|---------------|  
    			|  data_A	|
    	Data section	|---------------| 
    			|  data_B	|
    		------->|---------------| 
    			|		|
    			|      ...	|
    			|		|
    			|---------------| 
    			| addr of data_A|
    			|---------------|  
    		GOT base| addr of data_B|
    		------->|---------------| 
    			|		|
    			|		|
    			|		|
    			|      ...	|
    	Text section	|		|
    		------->|---------------| 
    


    根据GOT,经过下面的三步,就可以寻址到特定数据。
        1)得到当前指令的地址
        2)根据固定的偏移找到GOT的地址
        3)根据数据的符号固定偏移找到该数据的地址

    下面的代码实现了这一过程:

    		call 	tmp_label						/* will push EIP to stack */
    	tmp_label:	
    		popl	%ecx							/* %ecx now has address of $tmp_label */
    		addl	$GOT_TABLE_OFFSET_TO_CUR +[. - $tmp_label], %ecx	/* %ecx now has the base address of GOT */
    


    上面的代码中,GOT_TABLE_OFFSET_TO_CUR是所在代码的地址到GOT基地址的固定偏移,这个是compiler & linker决定的。执行上面最后一句后,%ecx里已经是GOT的基地址了,然后就可以根据%ecx寻址数据:

    		movl	data_symbol_offset(%ecx), %ebx	/* %ebx now has address of target data */
    		movl	(%ebx), %eax			/* move data to %eax */
    


    data_symbol_offset也是在编译连接的过程中确定,是固定值。

    一个library中包含了一个全局的GOT,每个library都有自己的GOT。在load时,GOT对每个进程都是私有的,这个和数据段一样。如果某个library引用了另一个library的数据,则该library的GOT里也包含这个数据的地址,只不过这个地址是在dynamic linker在load library的时候负责填入的,编译链接阶段无法确定这个值,ELF格式中定义了特定的类型来表示这种数据。

    3)一个具体例子

    写一个简单的例子来验证PIC的实现。libtest2.so只包含一个数据,被libtest.so引用,main调用libtest.so里的test函数。

    kai@opensolaris-kai:~/src/tmp$ cat test.c
    static int data = 1;
    extern int test2_data;
     
    void test(int p1, int p2, int p3)
    {
    	data += p2;
    	test2_data += p1;
    }
     
    kai@opensolaris-kai:~/src/tmp$ cat test2.c
    int test2_data = 3;
     
    kai@opensolaris-kai:~/src/tmp$ cat main.c 
    void test(int p1, int p2, int p3);
     
    int main(void)
    {
    	int i = 5;
    	
    	test(2, i, 2);
    }
     
    kai@opensolaris-kai:~/src/tmp$ make
    gcc -nostdlib -shared -fPIC -s -o libtest2.so test2.c
    gcc -nostdlib -shared -fPIC -s -o libtest.so test.c -ltest2
    gcc -o main main.c -ltest
    objdump -D libtest2.so > test2.S
    objdump -D libtest.so > test.S
    objdump -D main > main.S
    


    反汇编后的test.S:

    Disassembly of section .text:
     
    0000056c <test>:
     56c:   55                      push   %ebp			/* %ebp指向caller的stack frame base pointer */
     56d:   89 e5                   mov    %esp,%ebp		/* %esp指向test的stack frame base pointer,存进%ebp,用来访问传给test的参数 */
     56f:   53                      push   %ebx			
     570:   e8 00 00 00 00          call   575 <test+0x9>		/* 地址575会被压入stack */
     575:   5b                      pop    %ebx			/* %ebx现在等于地址575 */
     576:   81 c3 2b 00 01 00       add    $0x1002b,%ebx		/* %ebx现在等于GOT的base address,0x1002b是compiler & linker计算的 */
     57c:   8b 45 0c                mov    0xc(%ebp),%eax		/* 把p2的值move到%eax,传给test的三个参数分别在%ebp+0x8, %ebp + 0xc, %ebp + 0x10 */
     57f:   01 83 10 00 00 00       add    %eax,0x10(%ebx)		/* %ebp+0x10直接指向了libtest.so里的data,这里没有经过GOT去寻址,猜测应该是经过优化了 */
     585:   8b 8b 0c 00 00 00       mov    0xc(%ebx),%ecx		/* %ecx, %edx都指向libtest2.so里的test2_data */	
     58b:   8b 93 0c 00 00 00       mov    0xc(%ebx),%edx
     591:   8b 45 08                mov    0x8(%ebp),%eax		/* move test2_data到%eax */
     594:   03 02                   add    (%edx),%eax		/* add p1 to %eax */
     596:   89 01                   mov    %eax,(%ecx)		/* store %eax back to test2_data */
     598:   5b                      pop    %ebx
     599:   83 c4 00                add    $0x0,%esp		/* ? */
     59c:   c9                      leave
     59d:   c3                      ret
     
    Disassembly of section .got:
     
    000105a0 <_GLOBAL_OFFSET_TABLE_>:
       105a0:       94                      xchg   %eax,%esp
            ...
     
    Disassembly of section .data:
     
    000105b0 <_edata-0x4>:
       105b0:       01 00                   add    %eax,(%eax)
            ...
    


    一个问题:对于library自身数据的访问,似乎不需要GOT?因为可以数据段到代码的偏移也是固定的,完全可以直接得到数据段基地址。

    给出执行了push %ebx后的stack的情况(期间有一次地址575的push和pop)。
    SFBP = stack frame base pointer     

    					|			|  
    	stack top when enter main  --->	|-----------------------| 
    					| 			|  
    					|			|  					
    					|			|  
    					| 			|
    					|-----------------------| 
    					|  	p3		|
    					|-----------------------| 
    					|	p2		|
    					|-----------------------|
    					|	p1		|
    					|-----------------------| 
    					| return address in main|
    	stack top when enter test  --->	|-----------------------|  
    					| SFBP of main  	| 
    					|-----------------------|  <--- EBP 
    					| original %ebx		|
    					|-----------------------|  <--- ESP
    					|			|
    					|      			|
    


    4) Advantage & disadvantage of PIC

    PIC的好处显然是在load时可以被load到任意位置而不需要代码段的地址修正,代码可以被不同process share而只留有一份代码在内存中。坏处是增加了额外的对GOT的引用,以及一系列必须的额外的开销(比如对代码段地址的call 和pop等),使得代码运行速度比飞PIC的慢。
    另外,由于GOT里的数据地址也是需要在load时计算的,所以对于一些拥有大量数据的library,load的时间也会变慢。

    一个问题:对于library自身数据的访问,似乎不需要GOT?因为可以数据段到代码的偏移也是固定的,完全可以直接得到数据段基地址。这似乎可以大大减少load时对GOT里地址的修正所带来的额外时间的花销。

    5) Reference

    a. Intel IA-32 Architectures Manual Volume1 Basic Architecture, CHAPTER 6, PROCEDURE CALLS, INTERRUPTS, AND EXCEPTIONS
    b. Linkers & Loaders, Chapter 8, Loading and overlays, Position indenpendent code
    c. http://bottomupcs.sourceforge.net/csbu/x3824.htm            

    展开全文
  • GOT和PLT原理简析

    万次阅读 2014-11-18 22:34:34
    GOT(Global Offset Table)和PLT(Procedure Linkage Table)是Linux系统下面ELF格式的可执行文件中,用于定位全局变量过程的数据信息。以C程序为例,一个程序可能会包含多个文件,可执行文件的生成过程通常由以下几...

    GOT(Global Offset Table)和PLT(Procedure Linkage Table)是Linux系统下面ELF格式的可执行文件中,用于定位全局变量和过程的数据信息。以C程序为例,一个程序可能会包含多个文件,可执行文件的生成过程通常由以下几步组成。

    1. 编译器把每个.c文件编译成汇编(.s)文件。

    2. 汇编器把每个(.s)文件转换为(.o)文件。

    3. 链接器把多个.o文件链接为一个可执行文件(.out)。

    .s文件是汇编文件的后缀,一般对此种类型文件的关注不多,不再讨论,重点在.o文件和.out文件。

    .c文件中通常有对变量和过程的使用,若是变量和过程定义在当前文件中,则可以使用相对偏移寻址来调用。若是定义在其他文件中,则在编译当前文件时无法获取其地址;若是定义在动态库中,则直到程序被加载、运行时,才能够确定。本文通过《深入理解计算机系统》中讲动态链接一章中的例子,通过gdb的调试,研究调用动态库中函数时的重定位过程。

    1. 动态库程序。

    addvec.c

    void addvec(int *x, int *y, int *z, int n){
      int i;
    
      for(i = 0; i < n; i++)
        z[i] = x[i] + y[i];
    }
    
    通过命令gcc -fPIC -shared addvec.c -o libvec.so可以把上面的程序转换为动态库libvec.so。
    2. 调用动态库的主程序。

    main.c

    #include <stdio.h>
    
    int x[2] = {1, 2};
    int y[2] = {3, 4};
    int z[2];
    
    int main(){
      addvec(x, y, z, 2);
      printf("z = [%d %d]\n", z[0], z[1]);
      addvec(x, y, z, 2);
      printf("z = [%d %d]\n", z[0], z[1]);
    
      return 0;
    }
    

    通过命令gcc main.c -o main -L./ -lvec生成可执行文件main。-L./ -lvec表示链接当前目录下的动态链接库libvec.so。

    使用命令objdump -d -s > main.dmp反汇编main。

    反汇编生成的文件中,主要有三个段与对动态库函数addvec的调用有关:.got.plt,.plt和代码段.text。

    代码段容易理解,就是程序语句所对应的指令组成的。.got.plt中保存的是数据,为每个动态调用保存一个条目,条目的内容应该是对动态库函数的调用所跳转到的目标地址。由于Linux采用了延迟绑定技术,可执行文件中got.plt中的地址并不是目标地址,而是动态链接器(ld-linux)中的地址。在程序执行的第一次调用时,ld-linux把.got.plt的地址填写正确,之后的调用,就可以使用.got.plt中的目标地址了。.plt段中的内容则是实现跳转操作的代码片段。

    代码段:


    .got.plt


    .plt


    源代码中,对于函数的addvec的两次调用,命令为

    callq  400580 <addvec@plt>

    调用的目标地址是.plt段中的addvec@plt函数。该函数由三条语句组成,其作用分别为:

    1. 跳转到地址600af8,这个地址位于.got.plt中。从图中可以看到got.plt起始于600ad0,终止于600b08(600b00 + 8)。并且600af8的内容为86054000,按照小端的读法,其内容为00400586,实际就是下一条(第2条)指令。

    2. 第二条指令把当前函数的id(0x2)压入栈中。

    3. 第三条指令,跳转到400550,这之后的工作可以视为系统在运行时填充地址600af8的过程。也就是在延迟绑定机制下,第一次执行时,600af8的内容是400586,第二次及之后的内容就会修改为addvec函数的实际地址,可以通过gdb来验证。

    可以看到,第一次调用addvec的时候,地址600af8中的内容是0x0000000000400586,第二次调用的时候就变成了0x00007ffff7bd95e5,是addvec的实际入口地址,与预期的相同。

    展开全文
  • NULL 博文链接:https://chuanwang66.iteye.com/blog/1839210
  • elf文件的GOT和PLT

    2020-05-06 23:04:52
    转自个人博客0pt1mus 0x00 写在开始 首先,我是...windows下和linux下,有很多共通的地方,比如说这里的.plt和.got.plt同windows下的PE文件输入表中的INT和IAT很像。 参考链接:https://www.jianshu.com/p/5092d6d5caa3

    转自个人博客0pt1mus

    0x00 写在开始

    首先,我是从PE文件开始学习的,之后接触到了Linux下的ELF文件,在本质上来说,无论是Windows下的PE文件,还是Linux下的elf文件,他们本质上都是一个可执行文件,所运行的平台不同,它们的文件格式也有不同,但究其本质,还是会有互通的地方。

    0x01 基础知识

    首先,elf文件也是由众多的节构成的,可以通过objdump -h查看。

    下面解释.got.plt.got.plt三个节。

    .got

    GOT(Global Offset Table)全局偏移表。这是链接器为外部符号填充的实际偏移票。

    .plt

    PLT(Procedure Linkage Table)程序链接表。作用是一个跳板,保存了某个符号在重定位表中的偏移量(用来第一次查找某个符号)和对应的.got.plt的对应的地址。它有两个功能,要么在.got.plt节中拿到地址,并跳转。要么当.got.plt没有所需地址的时候,触发链接器去找到所需的地址。

    .got.plt

    这个是GOT专门为PLT准备的节。保存了重定位地址。.got.plt中的值是GOT的一部分。它包含上述PLT表所需地址(已经找到的和需要去触发的)。

    实例

    比如printf是一个重定位符号,需要链接该符号时过程是这样:

    main函数call .plt段中的一个地址,这里的第一句话就是跳转到.got.plt中的保存的printf的地址,如果是第一次,那么保存的地址就是.plt中的下一句话,这个下一句话就是压入这个符号在.rel.plt中的重定位表的偏移量,然后ld程序就会根据重定位表中的信息加上这个偏移量找到这个地址,保存到重定位表所指向的地址中,这个地址其实就是.got.plt段的一个地址。

    第二次调用时就可以直接获取到.got.plt中保存的地址了。

    0x02 实践

    接下来我们来实践一下,加深对这几个节的认识。

    首先要有一个分析的程序,我们用一个helloworld。

    //gcc -m32 -no-pie -g -o helloworld_li helloworld.c
    //-g 产生有调试符号的程序
    #include<stdio.h>
    #include<stdlib.h>
    
    int main(int argc, char const *argv[])
    {
        puts("Hello World!\n");
        return 0;
    }
    
    

    用gdb打开该文件,开始分析:

    • 首先反汇编main

    • 找到call puts的地址,用b *0x804844b下断点,r执行到断点处并通过si单步步入。

      我们可以看到当前指令是jmp跳转指令,跳转到0x804a00c。

      我们之前通过objdump查看该文件的各个节,发现0x804a00c是在.got.plt中。

    • 我们使用x/wx 0x804a00c查看这个位置的值。

      image-20200506172031681

      发现该地址存着的信息是当前执行指令的下一个位置。所以执行jmp [0x804a00c]后会到0x80482e6的位置。

      这里就可以理解,在第一次执行时,plt在.got.plt中没找到puts函数的地址,然后触发链接器去寻找puts函数的地址。

    • 通过finish执行完当前函数,然后再查看0x804a00c位置的内容。

      image-20200506173453829

      可以发现该位置的值已经变了,该地值便是puts函数的地址。

    0x03 总结

    主要是在学习rop的时候,中间提到了return to libc,通过调用系统函数,而不是shellcode来实现打开shell。有一种方法是return to PLT,因为之前学习的是windows下的,对这个PLT很陌生,因此查资料学习了一下。windows下和linux下,有很多共通的地方,比如说这里的.plt.got.plt同windows下的PE文件输入表中的INTIAT很像。

    参考链接:https://www.jianshu.com/p/5092d6d5caa3

    展开全文
  • 彻底搞清楚 GOT PLT

    2020-05-22 00:31:27
    虽然我在实习「前端」,但是离毕业还早,做安全的心也始终没有灭,今天看到了一篇文章彻底帮我把 PLT GOT 表之间的区别搞通了。注意:下面的内容,大部分来自这篇文章。 0X00 准备工作 一定要去使用 pwndbg,...
  • GOT和PLT表知识详解

    万次阅读 多人点赞 2017-01-23 17:11:25
    图1然后我们编译我们直接gdb./a.outgdb ./a.out来进行反编译处理,然后通过disasmaindisas main查看mainmain函数中的反编译代码如下:图3我们可以观察到gets@pltgets@plt和puts@pltputs@plt这两个函数,
  • 本文从一个具体例子,一步步引出PLT和GOT的定义。
  • 动态链接库、GOTPLT

    2021-06-03 08:20:35
    后来,人们发现某一些功能是大部分程序里面都要用到的、共通的,且具体程序没有该关系,例如向控制台打印一个字符等。于是,为了减少写代码的成本,人们把这些实现共通功能的代码汇聚起来,每一次需要使用的时候就...
  • 动态链接的got和plt

    2016-01-22 16:29:34
    更多请搜索got和plt 其实都大同小异 基本思路: 代码中在调用共享库中的函数时(这里是动态延迟绑定)会call一个地址(比如0x08048454),而这个地址中存储的内容是指令 jmp *0x0804a010 0x0804a010是被调用函数在got中的...
  • 最近在打0ctf时, 我的队友问我关于RELRO、GOT和PLT以及所有ELF section的问题, 我意识到我只知道大概的概念, 而没有打破沙锅搞到底, 所以希望搞这篇文章来研究研究, 也同时能够有益路过人. 所有的例子都是在x86的...
  • 本文通过示例剖析了 .plt、.plt.got、.got .got.plt sections 之间的区别,从而有助于理解位置无关代码技术。
  • GOT和PLT

    2020-04-22 12:47:30
    PLT则是有代码片段组成的,每个代码片段都跳转到GOT表中的一个具体的函数调用 重定位 链接时重定位 链接杰顿是将一个或多个中间文件(.o文件)通过链接器将它们链接成一个可执行文件主要做的事情有 对各个中间文件的...
  • 准备 实验环境 Ubuntu-Desktop 18.04 $ uname -a Linux ubuntu 5.4.0-48-generic #52~18.04.1-Ubuntu SMP Thu Sep 10 12:50:22 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux ... puts("learning plt and got!\n");
  • 深入理解GOT和PLT

    2020-04-14 10:30:00
    0x02 初探GOT和PLT表 我们先简单看一个例子 我们跟进一下scanf@plt 会发现,有三行代码 jmp 一个地址 push 一个值到栈里面 jmp 一个地址 看函数的名字就可以知道这是scanf函数的plt表,先不着急去了解plt是做什么...
  • 浅析ELF中的GOTPLT

    万次阅读 2017-03-28 23:53:51
    Linux的ELF(Excutable Linkable Format)。   编译器编译源代码后生成的文件叫做目标文件,从目标文件的结构上讲, 它是已经编译后的可执行文件格式,只是还没有链接的过程,其中可能有些符号或有些地址还...
  • GOT和PLT表的基本作用他们之间的关系, 所以今天就来详细分析下其具体的工作过程. 本文所用的依然是Linux x86 64位环境, 不过分析的ELF文件是32位的(-m32). 大局观 首先, 我们要知道, GOT和PLT只是一种重定向的...
  • hijack GOT, 打个比喻,可以理解为狸猫换太子,修改某一个被调用函数的地址,让其指向另一个函数,例如修改printf()函数的地址让其指向system(),这样做的结果就是原本对于printf()的调用就变成了调用system()函数。 ...
  • ELF的GOT和PLT以及PIC

    2013-04-25 16:24:46
    ELF的GOT和PLT以及PIC 发表回复 全局偏移表(GOT)过程链接表(PLT) ELF 格式的共享库使用 PIC 技术使代码数据的引用与地址无关,程序可以被加载到地址空间的任意位置。PIC 在代码中的跳转分支...
  • 今天来学习GOT和PLT表 操作系统通常使用动态链接的方法来提高程序运行的效率。在动态链接的情况下,程序加载的时候并不会把链接库中所有函数都一起加载进来,而是程序执行的时候按需加载,如果有函数并没有被调用...
  • 通过 GDB 调试理解 GOT/PLT

    千次阅读 2019-03-01 16:40:48
    关于 Linux 中 ELF 文件格式可参考详细文档《ELF_Format》,本文仅记录笔者理解GOT/PLT的过程。 GOT(Global Offset Table):全局偏移表用于记录在 ELF 文件中所用到的共享库中符号的绝对地址。在程序刚开始运行时...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 8,735
精华内容 3,494
关键字:

got和plt