精华内容
下载资源
问答
  • 动态链接原理分析

    千次阅读 2017-05-08 23:20:23
    linux 下动态链接实现原理 符号重定位 讲动态链接之前,得先说说符号重定位。 c/c++ 程序的编译是以文件为单位进行的,因此每个 c/cpp 文件也叫作一个编译单元(translation unit), 源文件先是被编译成一个个目标...

    linux 下动态链接实现原理

    符号重定位

    讲动态链接之前,得先说说符号重定位。(X86)

    c/c++ 程序的编译是以文件为单位进行的,因此每个 c/cpp 文件也叫作一个编译单元(translation unit), 源文件先是被编译成一个个目标文件, 再由链接器把这些目标文件组合成一个可执行文件或库,链接的过程,其核心工作是解决模块间各种符号(变量,函数)相互引用的问题,对符号的引用本质是对其在内存中具体地址的引用,因此确定符号地址是编译,链接,加载过程中一项不可缺少的工作,这就是所谓的符号重定位。本质上来说,符号重定位要解决的是当前编译单元如何访问「外部」符号这个问题。

    因为编译是以源文件为单位进行的,编译器此时并没有一个全局的视野,因此对一个编译单元内的符号它是无力确定其最终地址的,而对于可执行文件来说,在现代操作系统上,程序加载运行的地址是固定或可以预期的,因此在链接时,链接器可以直接计算分配该文件内各种段的绝对或相对地址。所以对于可执行文件来说,符号重定位是在链接时完成的(如果可执行文件引用了动态库里的函数,则情况稍有不同)。但对于动态链接库来说,因为动态库的加载是在运行时,且加载的地址不固定,因此没法事先确定该模块的起始地址,所以对动态库的符号重定位,只能推迟。

    符号重定位既指在当前目标文件内进行重定位,也包括在不同目标文件,甚至不同模块间进行重定位,这里面有什么不同吗?如果是同一个目标文件内,或者在同一个模块内,链接后,各个符号的相对地址就已经确定了,看起来似乎不用非得要知道最后的绝对地址才能引用这些符号,这说起来好像也有道理,但事实不是这样,x86 上 mov 之类访问程序中数据段的指令,它要求操作数是绝对地址,而对于函数调用,虽然是以相对地址进行调用,但计算相对地址也只限于在当前目标文件内进行,跨目标文件跨模块间的调用,编译期也是做不到的,只能等链接时或加载时才能进行相对地址的计算,因此重定位这个过程是不能缺少的,事实上目前来说,对于动态链接即使是当前目标文件内,如果是全局非静态函数,那么它也是需要进行重定位的,当然这里面有别的原因,比如说使得能实现 LD_PRELOAD 的功能等。

    链接时符号重定位

    链接时符号重定位指的是在链接阶段对符号进行重定位,一般来说,构建一个可执行文件可以简单分为两个步骤:编译及链接,如下例子,我们尝试使用静态链接的方式构建一个可执行文件:

    
    // file: a.c
    int g_share = 1;
    
    int g_func(int a)
    {
       g_share += a;
       return a * 3;
    }
    
    // file: main.c
    extern int g_share;
    extern int g_func(int a);
    
    int main()
    {
      int a = 42;
      a = g_func(a);
      return 0;
    }

    正如前面所说,此时符号的重定位在链接时进行,那么在编译时,编译器是怎么生成代码来引用那些还没有重定位的符号呢?让我们先编译一下,再来看看目标文件的内容:

    // x86_64, linux 2.6.9
    -bash-3.00$ gcc -c a.c main.c -g
    -bash-3.00$ objdump -S a.o

    然后得到如下输出(对于 main.o 中对 g_func 的引用,实现是一样的,故略):

    a.o:     file format elf64-x86-64
    
    Disassembly of section .text:
    
    0000000000000000 <g_func>:
    int g_share = 1;
    
    int g_func(int a)
    {
       0:   55                      push   %rbp
       1:   48 89 e5                mov    %rsp,%rbp
       4:   89 7d fc                mov    %edi,0xfffffffffffffffc(%rbp)
        g_share += a;
       7:   8b 45 fc                mov    0xfffffffffffffffc(%rbp),%eax
       a:   01 05 00 00 00 00       add    %eax,0(%rip)        # 10 <g_func+0x10>
        return a * 2;
      10:   8b 45 fc                mov    0xfffffffffffffffc(%rbp),%eax
      13:   01 c0                   add    %eax,%eax
    }
      15:   c9                      leaveq
      16:   c3                      retq

    从中可以看到,目标文件里的 .txt 段地址从 0 开始,其中地址为7的指令用于把参数 a 放到寄存器 %eax 中,而地址 a 处的指令则把 %eax 中的内容与 g_share 相加,注意这里g_share 的地址为:0(%rip). 显然这个地址是错的,编译器当前并不知道 g_share 这个变量最后会被分配到哪个地址上,因此在这儿只是随便用一个假的来代替,等着到接下来链接时,再把该处地址进行修正。那么,链接器怎么知道目标文件中哪些地方需要修正呢?很简单,编译器编译文件时时,会建立一系列表项,用来记录哪些地方需要在重定位时进行修正,这些表项叫作“重定位表”(relocatioin table):

    -bash-3.00$ objdump -r a.o
    a.o:     file format elf64-x86-64
    
    RELOCATION RECORDS FOR [.text]:
    OFFSET           TYPE              VALUE
    000000000000000c R_X86_64_PC32     g_share+0xfffffffffffffffc

    如上最后一行,这条记录记录了在当前编译单元中,哪儿对 g_share 进行了引用,其中 offset 用于指明需要修改的位置在该段中的偏移,TYPE 则指明要怎样去修改,因为 cpu 的寻址方式不是唯一的,寻址方式不同,地址的形式也有所不同,这个 type 用于指明怎么去修改, value 则是配合 type 来最后计算该符号地址的。

    有了如上信息,链接器在把目标文件合并成一个可执行文件并分配好各段的加载地址后,就可以重新计算那些需要重定位的符号的具体地址了, 如下我们可以看到在可执行文件中,对 g_share(0x40496处), g_func(0x4047a处)的访问已经被修改成了具体的地址:

    -bash-3.00$ gcc -o am a.o main.o
    -bash-3.00$ objdump -S am
    // skip some of the ouput
     
    extern int g_func(int a);
    
    int main()
    {
      400468:       55                      push   %rbp
      400469:       48 89 e5                mov    %rsp,%rbp
      40046c:       48 83 ec 10             sub    $0x10,%rsp
        int a = 42;
      400470:       c7 45 fc 2a 00 00 00    movl   $0x2a,0xfffffffffffffffc(%rbp)
        a = g_func(a);
      400477:       8b 7d fc                mov    0xfffffffffffffffc(%rbp),%edi
      40047a:       e8 0d 00 00 00          callq  40048c <g_func>
      40047f:       89 45 fc                mov    %eax,0xfffffffffffffffc(%rbp)
        return 0;
      400482:       b8 00 00 00 00          mov    $0x0,%eax
    }
      400487:       c9                      leaveq
      400488:       c3                      retq
      400489:       90                      nop
      40048a:       90                      nop
      40048b:       90                      nop
    
    000000000040048c <g_func>:
    int g_share = 1;
    
    int g_func(int a)
    {
      40048c:       55                      push   %rbp
      40048d:       48 89 e5                mov    %rsp,%rbp
      400490:       89 7d fc                mov    %edi,0xfffffffffffffffc(%rbp)
        g_share += a;
      400493:       8b 45 fc                mov    0xfffffffffffffffc(%rbp),%eax
      400496:       01 05 dc 03 10 00       add    %eax,1049564(%rip)        # 500878 <g_share>
        return a * 2;
      40049c:       8b 45 fc                mov    0xfffffffffffffffc(%rbp),%eax
      40049f:       01 c0                   add    %eax,%eax
    }
      4004a1:       c9                      leaveq
      4004a2:       c3                      retq
    
    // skip some of the ouput

    当然,重定位时修改指令的具体方式还牵涉到比较多的细节很啰嗦,这里就不细说了。

    加载时符号重定位

    前面描述了静态链接时,怎么解决符号重定位的问题,那么当我们使用动态链接来构建程序时,这些符号重定位问题是怎么解决的呢?目前来说,Linux 下 ELF 主要支持两种方式:加载时符号重定位及地址无关代码。地址无关代码接下来会讲,对于加载时重定位,其原理很简单,它与链接时重定位是一致的,只是把重定位的时机放到了动态库被加载到内存之后,由动态链接器来进行。

    int g_share = 1;
    
    int g_func(int a)
    {
        g_share += a;
        return a * 2;
    }
    
    int g_func2()
    {
        int a = 2;
        int b = g_func(3);
    
        return a + b;
    }
    // compile on 32bit linux OS
    -bash-3.00$ gcc -c a.c main.c
    -bash-3.00$ gcc -shared -o liba.so a.o
    -bash-3.00$ gcc -o am main.o -L. -la
    -bash-3.00$ objdump -S liba.so
    // skip some of the output
    000004f4 <g_func>:
    int g_share = 1;
    
    int g_func(int a)
    {
     4f4:   55                      push   %ebp
     4f5:   89 e5                   mov    %esp,%ebp
        g_share += a;
     4f7:   8b 45 08                mov    0x8(%ebp),%eax
     4fa:   01 05 00 00 00 00       add    %eax,0x0
        return a * 2;
     500:   8b 45 08                mov    0x8(%ebp),%eax
     503:   d1 e0                   shl    %eax
    }
     505:   c9                      leave  
     506:   c3                      ret    
    
    00000507 <g_func2>:
    
    int g_func2()
    {
     507:   55                      push   %ebp
     508:   89 e5                   mov    %esp,%ebp
     50a:   83 ec 08                sub    $0x8,%esp
        int a = 2;
     50d:   c7 45 fc 02 00 00 00    movl   $0x2,0xfffffffc(%ebp)
        int b = g_func(3);
     514:   6a 03                   push   $0x3
     516:   e8 fc ff ff ff          call   517 <g_func2+0x10>
     51b:   83 c4 04                add    $0x4,%esp
     51e:   89 45 f8                mov    %eax,0xfffffff8(%ebp)
    
        return a + b;
     521:   8b 45 f8                mov    0xfffffff8(%ebp),%eax
     524:   03 45 fc                add    0xfffffffc(%ebp),%eax
    }
     527:   c9                      leave  
    
    // skip some of the output

    注意其中地址 4fa 及 516 处的指令:此两处分别对 g_share 及 g_func 进行了访问,显然此时它们的地址仍然是假地址,这些地址在动态库加载完成后会被动态链接器进行重定位,最终修改为正确的地址,这看起来与静态链接时进行重定位是一样的过程,但实现上有几个关键的不同之处:

    1. 因为不允许对可执行文件的代码段进行加载时符号重定位,因此如果可执行文件引用了动态库中的数据符号,则在该可执行文件内对符号的重定位必须在链接阶段完成,为做到这一点,链接器在构建可执行文件的时候,会在当前可执行文件的数据段里分配出相应的空间来作为该符号真正的内存地址,等到运行时加载动态库后,再在动态库中对该符号的引用进行重定位:把对该符号的引用指向可执行文件数据段里相应的区域。

    2. ELF 文件对调用动态库中的函数采用了所谓的"延迟绑定"(lazy binding)策略, 只有当该函数在其第一次被调用发生时才最终被确认其真正的地址,因此我们不需要在调用动态库函数的地方直接填上假的地址,而是使用了一些跳转地址作为替换,这样一来连修改动态库和可执行程序中的相应代码都不需要进行了,当然延迟绑定的目的不是为了这个,具体先不细说。

    至此,我们可以发现加载时重定位实际上是一个重新修改动态库中数据符号地址的过程(函数符号的地址因为延迟绑定的存在不需要在代码段中重定位),但我们知道,不同的进程即使是对同一个动态库也很可能是加载到不同地址上,因此当以加载时重定位的方式来使用动态库时,该动态库就没法做到被各个进程所共享,而只能在每个进程中 copy 一份:因为符号重定位后,该动态库与在别的进程中就不同了,可见此时动态库节省内存的优势就不复存在了。

    地址无关代码(PIC, position independent code)

    从前面的介绍我们知道装载时重定位有重大的缺点:

    1. 它不能使动态库的指令代码被共享。
    2. 程序启动加载动态库后,对动态库中的符号引用进行重定位会比较花时间,特别是动态库多且复杂的情况下。

    为了克服这些缺陷,ELF 引用了一种叫作地址无关代码的实现方案,该解决方案通过对变量及函数的访问加一层跳转来实现,非常的灵活。

    1.模块内部符号的访问

    模块内部符号在这里指的是:static 类型的变量与函数,这种类型的符号比较简单,对于 static 函数来说,因为在动态库编译完后,它在模块内的相对地址就已经确定了,而 x86 上函数调用只用到相对地址,因此此时根本连重定位都不需要进行,编译时就能确定地址,稍微麻烦一点的是访问数据,因为访问数据需要绝对地址,但动态库未被加载时,绝对地址是没法得知的,怎么办呢?

    ELF 在这里使用了一个小技巧,根据当前 IP 值来动态计算数据的绝对地址,它的原理很简单,当动态库编译好之后,库中的数据段,代码段的相对位置就已经固定了,此时对任意一条指令来说,该指令的地址与数据段的距离都是固定的,那么,只要程序在运行时获取到当前指令的地址,就可以直接加上该固定的位移,从而得到所想要访问的数据的绝对地址了,下面我们用实例验证一下:

    int g_share = 1;
    static int g_share2 = 2;
    
    int g_func(int a)
    {
        g_share += a;
        return a * 2;
    }
    
    int g_func2()
    {
        int a = 2;
        int b = g_func(3);
    
        return a + b;
    }
    
    static int g_fun3()
    {
        g_share2 += 3;
        return g_share2 - 1;
    }
    
    static int g_func4()
    {
        int a = g_fun3();
    
        a + 2;
        return a;
    }

    以上代码在x86 linux 下编译,再反汇编看看得到如下结果:

    -bash-3.00$ gcc -o liba.so -fPIC -shared a.c
    -bash-3.00$ objdump -S liba.so 
    // skip some of the output
    00000564 <g_fun3>:
     564:   55                      push   %ebp
     565:   89 e5                   mov    %esp,%ebp
     567:   e8 00 00 00 00          call   56c <g_fun3+0x8>
     56c:   59                      pop    %ecx
     56d:   81 c1 60 11 00 00       add    $0x1160,%ecx
     573:   83 81 20 00 00 00 03    addl   $0x3,0x20(%ecx)
     57a:   8b 81 20 00 00 00       mov    0x20(%ecx),%eax
     580:   48                      dec    %eax
     581:   c9                      leave  
     582:   c3                      ret    
    // skip some of the output

    现在我们来分析验证一下:首先是地址 567 的指令有些怪,这儿不深究,简单来说,x86 下没有指令可以取当前 ip 的值,因此这儿使了个技巧通过函数调用来获取 ip 值(x86_64 下就不用这么麻烦),这个技巧的原理在于进行函数调用时要将返回地址压到栈上,此时通过读这个栈上的值就可以获得下一条指令的地址了,在这儿我们只要知道指令 56c 执行后,%ecx 中包含了当前指令的地址,也就是 0x56c,再看 56d 及 573 两条指令,得知 %ecx + 0x1160 + 0x20 = 0x16ec 就是 573 指令所需要访问的地址,这个地址指向哪里了呢?

    -bash-3.00$ objdump -s liba.so
    Contents of section .data:
     16e0 e0160000 f4150000 01000000 02000000  ................

    结果是数据段里的第二个 int,也就是 g_share2!

    2.模块间符号的访问

    模块间的符号访问比模块内的符号访问要麻烦很多,因为动态库运行时被加载到哪里是未知的,为了能使得代码段里对数据及函数的引用与具体地址无关,只能再作一层跳转,ELF 的做法是在动态库的数据段中加一个表项,叫作 GOT(global offset table), GOT 表格中放的是数据全局符号的地址,该表项在动态库被加载后由动态加载器进行初始化,动态库内所有对数据全局符号的访问都到该表中来取出相应的地址,即可做到与具体地址了,而该表作为动态库的一部分,访问起来与访问模块内的数据是一样的。

    仍然使用前面的例子,我们来看看 g_func 是怎么访问 g_share 变量的。

    00000504 <g_func>:
     504:   55                      push   %ebp
     505:   89 e5                   mov    %esp,%ebp
     507:   53                      push   %ebx
     508:   e8 00 00 00 00          call   50d <g_func+0x9>
     50d:   5b                      pop    %ebx
     50e:   81 c3 bf 11 00 00       add    $0x11bf,%ebx
     514:   8b 8b f0 ff ff ff       mov    0xfffffff0(%ebx),%ecx
     51a:   8b 93 f0 ff ff ff       mov    0xfffffff0(%ebx),%edx
     520:   8b 45 08                mov    0x8(%ebp),%eax
     523:   03 02                   add    (%edx),%eax
     525:   89 01                   mov    %eax,(%ecx)
     527:   8b 45 08                mov    0x8(%ebp),%eax
     52a:   d1 e0                   shl    %eax
     52c:   5b                      pop    %ebx
     52d:   c9                      leave  
     52e:   c3                      ret    

    上面的输出中,508 与 50d 处的指令用于获取 ip 值, 执行完 50d 后, %ebx 中放的是 0x50d, 地址 50e 用于计算 g_share 在 GOT 中的地址 0x50d + 0x11bf + 0xfffffff0 = 0x16bc, 我们检查一下该地址是不是 GOT:

    -bash-3.00$ objdump -h liba.so
    liba.so:     file format elf32-i386
    
    Sections:
    Idx Name          Size      VMA       LMA       File off  Algn
    //skip some of the output
    
     16 .got          00000010  000016bc  000016bc  000006bc  2**2
                      CONTENTS, ALLOC, LOAD, DATA

    显然,0x16bc 就是 GOT 表的第一项。

    事实上,ELF 文件中还包含了一个重定位段,里面记录了哪些符号需要进行重定位,我们可以通过它验证一下上面的计算是否与之匹配:

    -bash-3.00$ objdump -R liba.so
    liba.so:     file format elf32-i386
    
    DYNAMIC RELOCATION RECORDS
    OFFSET   TYPE              VALUE
    000016e0 R_386_RELATIVE    *ABS*
    000016e4 R_386_RELATIVE    *ABS*
    000016bc R_386_GLOB_DAT    g_share
    000016c0 R_386_GLOB_DAT    __cxa_finalize
    000016c4 R_386_GLOB_DAT    _Jv_RegisterClasses
    000016c8 R_386_GLOB_DAT    __gmon_start__
    000016d8 R_386_JUMP_SLOT   g_func
    000016dc R_386_JUMP_SLOT   __cxa_finalize

    如上输出, g_share 的地址在 0x16bc,与前面的计算完全吻合!
    致此,模块间的数据访问就介绍完了,模块间的函数调用在实现原理上是一样的,也需要经过一个类似 GOT 的表格进行跳转,但在具体实现上,ELF 为了实现所谓延迟绑定而作了更精细的处理,接下来会介绍。值得一提的是,PIC 也可在编译可执行文件时指定,此时可执行文件中的代码对外部符号的引用方式会改变,不再是直接(绝对地址或相对地址)引用该符号,而是也通过 GOT 来间接的引用。

    延迟加载

    我们知道,动态库是在进程启动的时候加载进来的,加载后,动态链接器需要对其作一系列的初始化,如符号重定位(动态库内以及可执行文件内),这些工作是比较费时的,特别是对函数的重定位,那么我们能不能把对函数的重定位延迟进行呢?这个改进是很有意义的,毕竟很多时候,一个动态库里可能包含很多的全局函数,但是我们往往可能只用到了其中一小部分而已,而且在这用到的一小部分里,很可能其中有些还压根不会执行到,因此完全没必要把那些没用到的函数也过早进行重定位,具体来说,就是应该等到第一次发生对该函数的调用时才进行符号绑定 -- 此谓之延迟绑定。

    延迟绑定的实现步骤如下:

    1. 建立一个 GOT.PLT 表,该表用来放全局函数的实际地址,但最开始时,该里面放的不是真实的地址而是一个跳转,接下来会讲。
    2. 对每一个全局函数,链接器生成一个与之相对应的影子函数,如 fun@plt
    3. 所有对 fun 的调用,都换成对 fun@plt 的调用,每个fun@plt 长成如下样子:

      fun@plt:
      jmp *(fun@got.plt)
      push index
      jmp _init

      其中第一条指令直接从 got.plt 中去拿真实的函数地址,如果已经之前已经发生过调用,got.plt 就已经保存了真实的地址,如果是第一次调用,则 got.plt 中放的是 fun@plt 中的第二条指令,这就使得当执行第一次调用时,fun@plt中的第一条指令其实什么事也没做,直接继续往下执行,第二条指令的作用是把当前要调用的函数在 got.plt 中的编号作为参数传给 _init(),而 _init() 这个函数则用于把 fun 进行重定位,然后把结果写入到 got.plt 相应的地方,最后直接跳过去该函数。

    仍然是使用前面的例子,我们看看 g_func2 是怎样调用 g_func 的:

    0000052f <g_func2>:
     52f:   55                      push   %ebp
     530:   89 e5                   mov    %esp,%ebp
     532:   53                      push   %ebx
     533:   83 ec 14                sub    $0x14,%esp
     536:   e8 00 00 00 00          call   53b <g_func2+0xc>
     53b:   5b                      pop    %ebx
     53c:   81 c3 91 11 00 00       add    $0x1191,%ebx
     542:   c7 45 f8 02 00 00 00    movl   $0x2,0xfffffff8(%ebp) // a = 2
     549:   83 ec 0c                sub    $0xc,%esp
     54c:   6a 03                   push   $0x3 // push argument 3 for g_func.
     54e:   e8 d5 fe ff ff          call   428 <g_func@plt>
     553:   83 c4 10                add    $0x10,%esp
     556:   89 45 f4                mov    %eax,0xfffffff4(%ebp)
     559:   8b 45 f4                mov    0xfffffff4(%ebp),%eax
     55c:   03 45 f8                add    0xfffffff8(%ebp),%eax
     55f:   8b 5d fc                mov    0xfffffffc(%ebp),%ebx
     562:   c9                      leave  
     563:   c3                      ret 

    如上汇编,指令 536, 53b, 53c, 用于计算 got.plt 的具体位置,计算方式与前面对数据的访问原理是一样的,经计算此时, %ebx = 0x53b + 0x1191 = 0x16cc, 注意指令 54e, 该指令调用了函数 g_func@plt:

    00000428 <g_func@plt>:
     428:   ff a3 0c 00 00 00       jmp    *0xc(%ebx)
     42e:   68 00 00 00 00          push   $0x0
     433:   e9 e0 ff ff ff          jmp    418 <_init+0x18>

    注意到此时, %ebx 中放的是 got.plt 的地址,g_func@plt 的第一条指令用于获取 got.plt 中 func 的具体地址, func 放在 0xc + %ebx = 0xc + 0x16cc = 0x16d8, 这个地址里放的是什么呢?我们查一下重定位表:

    -bash-3.00$ objdump -R liba.so
    
    liba.so:     file format elf32-i386
    
    DYNAMIC RELOCATION RECORDS
    OFFSET   TYPE              VALUE
    000016e0 R_386_RELATIVE    *ABS*
    000016e4 R_386_RELATIVE    *ABS*
    000016bc R_386_GLOB_DAT    g_share
    000016c0 R_386_GLOB_DAT    __cxa_finalize
    000016c4 R_386_GLOB_DAT    _Jv_RegisterClasses
    000016c8 R_386_GLOB_DAT    __gmon_start__
    000016d8 R_386_JUMP_SLOT   g_func
    000016dc R_386_JUMP_SLOT   __cxa_finalize

    可见,该地址里放的就是 g_func 的具体地址,那此时 0x16d8 放的是真正的地址了吗?我们再看看 got.plt:

    Contents of section .got.plt:
     16cc fc150000 00000000 00000000 2e040000  ................
     16dc 3e040000 

    16d8 处的内容是: 2e040000, 小端序,换回整形就是 0x000042e, 该地址就是 fun@plt 的第二条指令!是不是觉得有点儿绕?你可以定下心来再看一遍,其实不绕,而是很巧妙。

    后话

    对动态链接库来说,加载时重定位与链接时重定位各有优缺点,前者使得动态库的代码段不能被多个进程间所共享,加载动态库时也比较费时,但是加载完成后,因为对符号的引用不需要进行跳转,程序运行的效率相对是较高的。而对地址无关的代码,它的缺点是动态库的体积相对较大,毕竟增加了很多表项及相关的函数,另外就运行时对全局符号的引用需要通过表格进行跳转,程序执行的效率不可避免有所损失,优点嘛,就是动态库加载比较快,而且代码可以在多个进程间共享,对整个系统而言,可以大大节约对内存的使用,这个好处的吸引力是非常大的,所以你可以看到,目前来说在常用的动态库使用上,PIC 相较而言是更加被推崇的,道理在此。

    展开全文
  • Linux动态连接原理 GOT PLT表详解

    千次阅读 2013-07-05 13:54:48
    Linux动态连接原理 GOT PLT表详解 注意: 以下所用的连接器是指,ld, 而加载器是指ld-linux.so; 1, GOT表; GOT(Global Offset Table)表中每一项都是本运行模块要引用的一个全局变量或函数...

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

    Linux动态连接原理 GOT PLT表详解



    注意:

    以下所用的连接器是指,ld,

    而加载器是指ld-linux.so;

    <!--[if !supportLists]-->1,  <!--[endif]-->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,这个表的内容是什么呢?请看下面:

    <!--[if !supportLists]-->2,  <!--[endif]-->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:           <-- jmp*fun@GOT  --> change GOT entry from
                               |             $loader to $fun,
                               v             then jump to there
                           GOT table
                        --------------
                        fun@GOT <!--[if !vml]--><!--[endif]-->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.

    <!--[if !supportLists]-->3,  <!--[endif]-->代码段重定位前提。

    代码段本身是存在于只读区域的,所以理论上它是不可能在运行的时候重新修改的,但是这就涉及一个问题,如何保证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指令的立即寻址方式操作数。以后再也不需要重定位了。

    这样做的好处是目的是什么呢?

    就是在函数内部引用外部符号的时候能够正确的转到适当的地方去。

    <!--[if !supportLists]-->4,  <!--[endif]-->变量、函数引用

    当引用的是静态变量、静态函数或字符串常量时,使用R_386_GOTOFF重定位方式。它与GOTPC重定位方式很相似,同样首先由编译器在目标文件中设上重定位标记,然后连接器计算GOT表与被引用元素首地址的差值,作为leal指令的变址寻址方式操作数。代码片断如下:

    leal .LC1@GOTOFF(%ebx), %eax
    .o:  R_386_GOTOFF
    .so: NULL
    <!--[if !supportLineBreakNewLine]-->
    <!--[endif]-->

    当引用的是全局变量、全局函数时,编译器会在目标文件中设上一个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里面的地址,于是最终还是没能逃过加载器的协助。

    <!--[if !supportLists]-->5,  <!--[endif]-->直接调用函数
    如前所述,浮动代码中的函数调用语句会编译成相对跳转指令。首先编译器会在目标文件中设上一个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重定位 项,这里指函数地址引用的情况;

    <!--[if !supportLists]-->6,  <!--[endif]-->数据段的重定位

    在数据段中的重定位是指对指针类型的静态变量、全局变量进行初始化。它与代码段中的重定位比较起来至少有以下明显不 同:一、在用户程序获得控制权(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

     

    <!--[if !supportLists]-->7,  <!--[endif]-->总结:

    下表给出了前面讨论得到的全部结果:
                                    .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


    展开全文
  • Linux动态连接原理 注意: 以下所用的连接器是指,ld, 而加载器是指ld-linux.so; 1, GOT表; GOT(Global Offset Table)表中每一项都是本运行模块要引用的一个全局变量或函数的地址。可以用GOT表来间接...

    转载地址:http://blog.csdn.net/lmh12506/article/details/6801630 

    Linux动态连接原理

    注意:

    以下所用的连接器是指,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:           <-- jmp*fun@GOT  --> change GOT entry from
    |             $loader to $fun,
    v             then jump to there
    GOT table
    --------------
    fun@GOTloader

    第 一次调用以后,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

    展开全文
  • VC++动态链接原理、创建和使用 更加详细的熟悉连接库的由来和使用
  • 引用动态链接库的原理 位置无关代码 可以架在你而无需重定位的代码成为位置无关代码(Position-Independent Code,PIC)。用户对GCC使用-fpic选项指示GNU编译系统生成PIC代码。动态链接库的编译必须总是使用该选项。...

    引用动态链接库的原理

    位置无关代码

    可以架在你而无需重定位的代码成为位置无关代码(Position-Independent Code,PIC)。用户对GCC使用-fpic选项指示GNU编译系统生成PIC代码。动态链接库的编译必须总是使用该选项。

    在x86-64系统中,对同一个目标模块中符号的引用是不需要特殊处理使之成为PIC。可以用PC相对寻址来编译这些引用,构造目标文件时由静态链接器重定位。然而,对动态链接库定义的外部过程和对全局变量的引用需要一些技巧。

    PIC数据引用

    无论我们在内存中何处加载一个目标模块,数据段与代码段的距离总是保持不变。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置是无关的。

    所以想要生成对全局变量PIC引用,可以在数据段开始的地方构造一张全局偏移量(Global Offset Table,GOT)。在GOT中,每个被这个目标模块引用的全局数据都有一个由编译器生成的重定位记录。在加载时,动态链接库会重定位GOT中的每个条目,使它包含目标的正确的绝对地址。运行的时候,根据数据段和代码段之间距离保持不变的特点,使用PC相对引用得到GOT表中的绝对地址即可。

    在这里插入图片描述

    PIC函数调用

    假设程序调用有一个动态链接库定义的函数。编译器无法预测这个函数的运行时地址,因为定义它的共享模块再运行时可以加载到任意位置。GNU编译系统使用一种称为延迟绑定的技术将过程地址的绑定推迟到第一次调用该过程时。

    使用延迟绑定的动机是对于一个像libc.so这样的动态链接库中包含很多函数,而一个应用程序往往只会使用到其中的小部分。把函数地址的解析推迟到它实际被调用的地方,能避免动态链接库在加载时对没有使用的函数进行不必要的重定位。第一次调用过程运行时开销很大,但是其后的每次调用只会花费一条指令和一个间接的内存引用。

    延迟绑定技术使用到两个数据结构:

    1. 过程链接表(PLT)。PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接库中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
    2. 全局偏移量表(GOT)。GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息,GTO[2]是动态链接器在ld-linux.so模块中的入口地址。其余的每个条目对应一个被调用的函数,其地址需要在运行时被解析,每个条目都有一个相匹配的PLT条目。

    在这里插入图片描述

    上图展示了,延迟绑定的具体步骤:

    1. 代码段中调用addvec函数,指令跳转到PLT[2],这是addvec函数的PLT条目。
    2. 接着,指令跳转到GOT[4]指向的地址,GOT[4]中应该包含addvec函数的地址。但是第一次进入时,GOT表中没有addvec的地址信息,只是简单地控制传送回PLT[2]中的下一条指令。
    3. 在把addvec的ID压入栈中只有,PLT[2]跳转到PLT[0]。
    4. PLT[0]通过GOT[1]把动态链接库的一个参数压入栈中,然后通过GOT[2]间接跳转进动态链接器中。动态链接库使用两个参数来确定addvec的运行时位置,并将addvec的地址写入GOT[4]中,再把控制传递给addvec。

    参考书籍:《深入理解计算机系统(第三版)》

    展开全文
  • DLL原理动态链接程序库) 概述 编译分为3步,首先对源文件进行预处理,这个过程主要是处理一些#号定义的命令或语句(如宏、#include、预编译指令#ifdef等),生成*.i文件;然后进行编译,这个过程主要是进行词法...
  • 动态链接VS静态链接: 动态链接从命名中可以看出动态是关键, 那这个动态体现在哪呢? 静态链接中的静态体现在哪呢?, 我们知道代码需要经过几个步骤才会编译成机器认识的符号, 代码一般经过预编译, 编译, 汇编, 链接...
  • mybatis接口动态代理原理
  •  初学动态链接库时,总被许多概念搞得一塌糊涂,比如“隐式链接”,“动态加载”,lib静态库和lib导入库。虽然会用,但概念还是很模糊的。后来了解了编译和链接的一些原理,总算有一个比较清晰的理解,整理出来和...
  • 多进程共享动态链接库的原理

    千次阅读 2015-08-25 08:33:48
    多进程共享动态链接库的原理 ...多进程共享动态链接库的原理   当多个进程共享dll时,其实内存中只保留一份dll代码,而每个进程调用dll的导出函数或类等等时是单独在进程
  • 【Android】动态链接库so的加载原理

    千次阅读 2018-07-04 14:51:46
    所以本文将从AOSP源码简单跟踪Android中的动态链接库so的加载原理,试图找出一丝线索。 加载入口 首先我们知道在Android(Java)中加载一个动态链接库非常简单。就是我们日常调用的 System.load(...
  • DLL动态链接库的工作原理

    千次阅读 2016-10-03 20:19:28
    "动态链接"这几字指明了DLLs是如何工作的。对于常规的函数库,链接器从中拷贝它需要的所有库函数,并把确切的函数地址传送给调用这些函数的程序。而对于DLLs,函数储存在一个独立的动态链接库文件中。在创建Windows...
  • 2 动态链接库编程之基础概念 DLL(Dynamic Linkable Library)的概念 你可以简单的把DLL看成一种仓库,它提供你一些可以直接拿来用的变量、函数或类。在仓库的发展史上经历了“无库 -- 静态链接库 -- 动态链接库”...
  • 动态表单及动态建表实现原理

    千次阅读 2019-02-18 17:09:14
    动态表单及动态建表实现原理
  • 动态链接库-原理

    千次阅读 2004-09-15 08:54:00
    理解库库是一个文件, 它包含函数或者其它可以在应用程序中使用的资源. 这些资源一般是通用性的, 允许许多工作不同的应用程序能轻易的共享同呈代码或者资源. 静态链接库是应用程序可执行文件的一部分, ... 动态链接
  • 多个进程间共享动态链接库的原理

    千次阅读 2015-11-27 15:04:56
    多个进程间共享动态链接库的原理  同样这个问题是我在面试总监的时候,总监问我的一个问题,这些天一直 忙活,都没有好好看书了,今天总结一下这个问题?为什么一个进程用完一个动态链接库以后另...
  • 动态链接就是把调用的函数所在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,程序运行的时候再从DLL中寻找相应函数代码,因此需要相应DLL文件的支持。 静态链接库与动态链接库都是共享代码的方式,...
  • 数码管动态显示原理

    千次阅读 2018-12-27 22:03:00
    1、多个数码管的段码连接在一起,位码分别控制。 2、由于段码连接在一起,如果数码管全亮,则显示的数据相同,所以为了显示不同的数字,任何时刻,只能有一个数码管显示,其余不显示。 3、我们用软件使这几个...
  • 纯css绘制三角形原理动态演示链接地址
  • 有别于静态库,动态库的链接是在程序执行的时候被链接的。所以,即使程序编译完,库仍须保留在系统上,以供程序运行时调用。(TODO:链接动态库时链接阶段到底做了什么) 2 静态库和动态库的比较 链接静态库其...
  • 动态链接库(SO)加载原理

    千次阅读 2016-11-21 14:52:11
    1、动态链接库的加载流程 首先从宏观流程上来看,对于 load 过程我们分为 find&load,首先是要找到 so 所在的位置,然后才是 load 加载进内存,同时对于 dalvik 和 art 虚拟机来说,他们加载 so 的流程和方式也不尽...
  • java 动态代理实现原理

    千次阅读 2015-07-10 21:08:26
    上篇讲了:java动态代理浅析 这篇讲讲其内部实现原理。 1、相关的类和接口 1.1 java.lang.reflect.Proxy 这是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。 Proxy 的...
  • 链接原理

    2015-05-09 11:04:47
    链接分为静态目标文件链接,静态库链接动态共享库链接动态共享库加载四种。 1.1 静态目标文件链接 每个代码文本文件都生成一个.O文件,链接链接所有的目标文件到一个可执行文件。 链接器完成的两个任务...
  • CGLib动态代理原理及实现

    千次阅读 2016-09-25 13:48:51
    CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。 ...
  • 实际开发中都会使用连接池,因为可以减少获取连接消耗的时间,连接池就是用来存储连接的一个容器,通常用一个集合对象表示,该集合必须是线程安全的,不能两个线程拿到同一个连接,该集合还必须实现队列的特性,先进...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 358,383
精华内容 143,353
关键字:

动态链接原理