书还是接上回,本篇主要对第七章的相关内容进行总结。第七章主要对动态链接的相关内容进行分析。

7.1 为什么要动态链接

既然要对动态链接进行分析,首先应对动态链接出现的原因进行一个简单的分析。动态链接从名称上看很自然就能联想到静态链接,在本书的第2部分对静态链接的相关内容进行了详细的分析,静态链接方法简单,原理也容易理解。但过于简单的东西肯定存在其漏洞。第一点漏洞就是“内存与磁盘空间的浪费”,先看磁盘空间的浪费,由于库函数在可执行文件装载前已经完成链接,因此每个可执行文件中存在有大量的库函数代码。此即是对磁盘空间的浪费,而上述可执行文件被装载进入内存后,相同的程序镜像同样在内存空间中存在有多份,此即是对内存空间的浪费。第二点漏洞是程序开发和发布过程中存在的问题,如果可执行文件中的某个目标文件进行了更新,那么整个可执行文件就需要重新进行链接,即一旦程序中有任何模块的更新,整个程序就要重新链接、发布给用户。问题明确了,也就意味着需求清晰了,因此为解决上述两点问题,借用书中的说法“最简单的办法就是把程序的模块相互分割开来,形成独立的文件,等到程序要运行时才进行链接”。还是套用书中的原话“也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接的基本思想”。通过动态链接,还可以加强程序的可扩展性和兼容性。还要借用书中的一句话对动态链接的过程进行总结:“当程序被装载的时候,系统的动态链接器会将程序所需要的所有动态链接库(最基本的就是libc.so)装载到进程的地址空间,并且将程序中所有未决议的符号绑定到相应的动态链接库中,并进行重定位工作”。

7.2 简单的动态链接例子

本章主要通过一个例子让大家首先随意感受一下什么是动态链接库。与静态链接过程相同,当链接器将目标文件链接成可执行文件时,链接器必须确定所引用函数的地址,但确定函数的地址前,首先应确定函数的性质,性质不同重定位方式也不同。但链接器如何知道函数引用是一个静态符号还是一个动态符号?动态链接器中保存了完整的符号信息(因为运行时进行动态链接还须使用符号信息),把动态链接库也作为链接的输入文件之一,链接器在解析符号时就可以知道:某个函数是一个定义在动态链接库中的符号。

将程序链接为可执行文件后,就可以从运行时、静态两个方面对可执行文件的内容进行分析。首先从运行时角度对地址空间分布进行分析,还是采用上一篇中的方法。与静态链接可执行文件运行的空间地址分布相比,动态链接形成的可执行文件多出了C语言运行库与动态链接器。动态链接器与普通共享对象一样被映射到了进程的地址空间,在系统开始运行可执行文件前,首先会把控制权交给动态链接器,由它完成所有的动态链接工作以后再把控制权交给可执行文件,然后开始执行。再来从执行视图角度分析可执行文件的内容,通过对其内容进行分析可以发现,共享对象的最终装载地址在编译时是不确定的,而是在装载时,装载器根据当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间给相应的共享对象。

7.3 地址无关代码

在开始第三小节的内容总结前,给大家分享一个资料:http://download.csdn.net/detail/u012927281/9449413

以上资料是一个关于“装载时重定位”比较详细的介绍,可与书中的内容结合着看。

好了开始本小节的内容。

1) 装载时重定位

从第二小节的分析中可以知道:“共享对象的最终装载地址是动态的,是不确定的”。在静态链接时,由于可执行文件的装载地址是确定的(linux下为0x0804000),因此对于绝对地址的引用在链接时即可确定,但由于共享对象的装载地址在装载前都是无法确定的,因此造成了某些绝对地址无法重定位。上面这么多话总结起来就是一句话:“装载地址的不确定造成了绝对地址的无法重定位”。针对以上问题,还是从最简单的想法出发,既然无法确定,那是否能直接假设呢(与静态链接类似)?答案很明确,不能。之所以说不能,是因为假设的某块装载地址可能造成某块之间的冲突。

因此,对于以上问题的解决,linux ELF共享库采取的措施是“装载时重定位”与“地址无关代码”。

先来看“装载时重定位”,其实装载时重定位也是一种最为直接的想法。既然装载地址无法假设,那就将重定位过程推迟到装载时,一旦模块装载地址确定,及目标地址确定,那么系统就对程序中所有的绝对地址引用进行重定位。对于装载时重定位的方法,大家可以参考我分享的资料。但装载时重定位同样存在问题,模块被装载进入程序地址空间的位置不同(进程之间共享库的使用情况不同),造成指令修改结果不同,进而造成指令无法在多个不同的进程间共享,如此就失去了动态链接节省内存的一大优势。当然,动态链接库中可修改数据部分对于不同的进程来说有多个副本,所以他们可以采用装载时重定位的方法来解决。

对于以上知识,我还要补充一点,对于64位linux,不使用“-fPIC”选项是无法编译动态链接库的,原因嘛我没看懂。

给出这篇博客,有这么几句解释的话,哪位同学看懂了给我讲讲这段。

http://blog.csdn.net/pear86743/article/details/8686140

2) 地址无关代码

对于以上问题,依然采用最直接的想法,把指令中需要修改的部分分离出来,同数据部分放在一起,如此指令部分就可以保持不变,而数据部分可以在每个进程中拥有一份副本。以上方法就是另一种方法“地址无关代码”。

既然要提取地址无关代码,首先就应清楚哪些是地址无关代码,根据书中给出的分类,共四种:模块内部函数调用、跳转等,模块内部数据访问,模块外部的函数调用、跳转,模块外部的数据访问。其中第一种本来就是地址无关的,因此其根本就不需要解决。第二种情况的解决方式其思想方法都是一样的,就是要把绝对地址转化为相对地址,也因此问题就转化为了如何把绝对地址转化为相对地址。还是比较直接的想法,任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的,那么只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据了。以上就是一些原理分析,现把源码和我的分析过程给大家分享一下。

首先《程序员自我修养》中对于第二种情况给出的解决方法是调用__i686.get_pc_thunk.cx函数获取PC的值,不过我的程序在反汇编之后,根本没有这个函数调用。

程序源码如下:

<pre name="code" class="cpp">#include <stdio.h>static int glob;void foobar()
{int a = 2;    glob = 1;printf("Printing from Lib.so %d\n",glob);
} 

之所以要加上一句“int a = 2”,是为便于设立断点。

反汇编结果如下:

<pre name="code" class="plain">00000000000006e0 <foobar>:6e0:  55                      push   %rbp6e1: 48 89 e5                mov    %rsp,%rbp6e4:    48 83 ec 10             sub    $0x10,%rsp6e8:   c7 45 fc 02 00 00 00    movl   $0x2,-0x4(%rbp)6ef:  c7 05 43 09 20 00 01    movl   $0x1,0x200943(%rip)        # 20103c <glob>6f6: 00 00 00 6f9:   8b 05 3d 09 20 00       mov    0x20093d(%rip),%eax        # 20103c <glob>6ff: 89 c6                   mov    %eax,%esi701:    48 8d 3d 15 00 00 00    lea    0x15(%rip),%rdi        # 71d <_fini+0x9>708:  b8 00 00 00 00          mov    $0x0,%eax70d:    e8 9e fe ff ff          callq  5b0 <printf@plt>712:  c9                      leaveq 713: c3                      retq

可以看到

6e4: c7 05 4e 09 20 00 01    movl   $0x1,0x20094e(%rip)        # 20103c <glob>

这一句就是把1赋值给glob,从形式上看依然是一条相对地址跳转指令,根据指令glob的地址应该是“rip+0x20094e”。现在只需要验证一下glob的地址的即可,因此可采用gdb对其内容进行解析。

启动gdb,不过要注意的是断点一定要放在“int a = 2”。运行到断点处,通过“info r rip”查看其值,结果如下:

rip            0x7ffff7bd76e8    0x7ffff7bd76e8 <foobar+8>

可以发现rip的值为“0x7ffff7bd76e8”,加上“0x200943”,结果为“0x7ffff7dd802b”,下面就是见证奇迹的时刻

print &glob
$2 = (int *) 0x7ffff7dd803c <glob>

不开心奇迹没发生,两个数值相差0x11,我估计可能是对齐造成的,欢迎了解的同学给咱解释一下,这到底是怎么回事?

不过有个方法倒是可以验证一下,根据上面的反汇编:

 6ef:    c7 05 43 09 20 00 01     movl   $0x1,0x200943(%rip)        # 20103c <glob>6f6:    00 00 006f9:    8b 05 3d 09 20 00        mov    0x20093d(%rip),%eax        # 20103c <glob>

这两个地址加一下,结果分别为:“0x7ffff7dd802b”、“0x7ffff7dd802c”,又差了“0x1”,彻底晕了。

还有点疑问,觉得是不是rip寄存器的值搞错了,所以使用汇编级调试

使用如下命令设置gdb 汇编模式,并反汇编foobar函数

set disassembly-flavor intel
disassemble foobar

反汇编结果如下:

Dump of assembler code for function foobar:0x00007ffff7bd76e0 <+0>:   push   rbp0x00007ffff7bd76e1 <+1>:   mov    rbp,rsp0x00007ffff7bd76e4 <+4>:   sub    rsp,0x10
=> 0x00007ffff7bd76e8 <+8>:  mov    DWORD PTR [rbp-0x4],0x20x00007ffff7bd76ef <+15>:  mov    DWORD PTR [rip+0x200943],0x1        # 0x7ffff7dd803c <glob>0x00007ffff7bd76f9 <+25>:   mov    eax,DWORD PTR [rip+0x20093d]        # 0x7ffff7dd803c <glob>0x00007ffff7bd76ff <+31>:   mov    esi,eax0x00007ffff7bd7701 <+33>:  lea    rdi,[rip+0x15]        # 0x7ffff7bd771d0x00007ffff7bd7708 <+40>:  mov    eax,0x00x00007ffff7bd770d <+45>:  call   0x7ffff7bd75b0 <printf@plt>0x00007ffff7bd7712 <+50>:   leave  0x00007ffff7bd7713 <+51>: ret
End of assembler dump.

同时设置自动显示rip寄存器的值

display /i $rip

使用“stepi”逐条调试

结果发现结果确实不对,rip的值应该为“0x7ffff7bd76ef”,[rip+0x200943]对应的地址应该是“0x7ffff7dd8032”,结果仍然不对,相差“0xa”。使用“x 0x7ffff7dd8032”查看该地址处的内容,结果为“0x7ffff7dd8032:    0x7ffff7dd”,此处我又懵逼了。接下来如法炮制,[rip+0x20093d] 对应的地址为“0x7ffff7dd8036”,相差“0x6”。看来再弄下去也不会有什么结果,此处先留下一个问题吧,欢迎看到此处的大神给小弟讲讲。

好了第二种情况先讲解到这里。

第三种情况与书中讲的差不多,对于其他模块中数据引用,其地址均存储在.got中,对这部分数据进行引用时,就是直接引用.got中对应项的值,而这些值就是这些数据的地址。

来看第四种情况。对于模块间的调用与跳转,同样可以类似于第三种的方法,不过其值是存储在.plt中。

好了,写了这么多以上四种地址无关代码的生成方法就给大家分析到了这里,我其实对于很多问题都没有理解清楚。不过可以确定的一点是,x86_64体系结构下,其地址无关代码生成方式与i386体系结构不同。

把我到现在为止的问题总结一下吧:

1.首先是那两处对glob的引用地址与glob的地址不同。

2.“0x200943”与“0x20093d”是如何计算出来的,其实如果第一个问题能够得到解决,我觉得第二个问题也能够迎刃而解。

7.4 延迟绑定(PLT)

动态链接相较静态链接更加灵活,但其在运行速度上较慢。因此为优化动态链接库,ELF采用了一种叫做延迟绑定的做法,其基本思想是当函数第一次被用到时才进行绑定,若未用到则不进行绑定。

好了原理基本清楚了,来看看PLT的过程,还是利用gdb对程序运行过程进行追踪。

这篇文章可以作为书中内容的补充,也建议大家也看一看

http://www.tuicool.com/articles/vIBbE3f

启动gdb

gdb ./program1

设置断点

break Lib.c : 7

开始运行,在断点处反汇编

disassemble foobar

汇编结果如下:

Dump of assembler code for function foobar:0x00007ffff7bd7760 <+0>:   push   %rbp0x00007ffff7bd7761 <+1>:  mov    %rsp,%rbp0x00007ffff7bd7764 <+4>: sub    $0x10,%rsp
=> 0x00007ffff7bd7768 <+8>:  movl   $0x2,-0x4(%rbp)0x00007ffff7bd776f <+15>:  mov    0x200872(%rip),%rax        # 0x7ffff7dd7fe80x00007ffff7bd7776 <+22>:  movl   $0x1,(%rax)0x00007ffff7bd777c <+28>:  mov    0x200865(%rip),%rax        # 0x7ffff7dd7fe80x00007ffff7bd7783 <+35>:  mov    (%rax),%eax0x00007ffff7bd7785 <+37>:  mov    %eax,%esi0x00007ffff7bd7787 <+39>:    lea    0x27(%rip),%rdi        # 0x7ffff7bd77b50x00007ffff7bd778e <+46>:  mov    $0x0,%eax0x00007ffff7bd7793 <+51>:    callq  0x7ffff7bd7620 <printf@plt>0x00007ffff7bd7798 <+56>:   mov    $0xffffffff,%edi0x00007ffff7bd779d <+61>: mov    $0x0,%eax0x00007ffff7bd77a2 <+66>:    callq  0x7ffff7bd7640 <sleep@plt>0x00007ffff7bd77a7 <+71>:    leaveq 0x00007ffff7bd77a8 <+72>: retq
End of assembler dump.

可以发现在“0x00007ffff7bd7793”处调用<printf@plt>,继续设置断点跟踪执行

Breakpoint 3 at 0x7ffff7bd7620

直接反汇编

(gdb) disassemble
Dump of assembler code for function printf@plt:
=> 0x00007ffff7bd7620 <+0>:  jmpq   *0x2009f2(%rip)        # 0x7ffff7dd8018 <printf@got.plt>0x00007ffff7bd7626 <+6>:   pushq  $0x00x00007ffff7bd762b <+11>: jmpq   0x7ffff7bd7610
End of assembler dump.

以上代码位于“.plt”中,“.plt”实际上也是一种代码。

查看0x7ffff7dd8018地址处的内容

(gdb) x 0x7ffff7dd8018
0x7ffff7dd8018 <printf@got.plt>: 0xf7bd7626

也就是下一条指令的地址,PS:貌似这个地址是在.got.plt中,不过我无法用实验的方法证明,欢迎知道的同学一起交流。再PS以下:上面那句话有问题,“0x7ffff7dd8018”这个地址貌似是在 “.got” 中,不过我没有办法证实这一点,仅凭图中的内容进行推测,盗来了一副图,向图的原著人表示感谢:http://blog.chinaunix.net/uid-24774106-id-3349549.html

这个地方要总结以下:以上两处“PS”都是有问题的,现在把动态绑定流程给大家总结以下:

让我们回到最初的起点,呆呆的站在调用前,如下:

   0x00007ffff7bd7793 <+51>:  callq  0x7ffff7bd7620 <printf@plt>

此时我们设立断点,进入“0x7ffff7bd7620”地址处,这个地址其实是位于.plt中,如何验证?直接对链接库进行反汇编,结果如下:

Disassembly of section .plt:00000000000005a0 <printf@plt-0x10>:5a0:   ff 35 62 0a 20 00       pushq  0x200a62(%rip)        # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>5a6:    ff 25 64 0a 20 00       jmpq   *0x200a64(%rip)        # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>5ac:  0f 1f 40 00             nopl   0x0(%rax)00000000000005b0 <printf@plt>:<pre name="code" class="plain"> 5b0:   ff 25 62 0a 20 00       jmpq   *0x200a62(%rip)        # 201018 <_GLOBAL_OFFSET_TABLE_+0x18>5b6:  68 00 00 00 00          pushq  $0x05bb: e9 e0 ff ff ff          jmpq   5a0 <_init+0x28>00000000000005c0 <__gmon_start__@plt>:5c0: ff 25 5a 0a 20 00       jmpq   *0x200a5a(%rip)        # 201020 <_GLOBAL_OFFSET_TABLE_+0x20>5c6:  68 01 00 00 00          pushq  $0x15cb: e9 d0 ff ff ff          jmpq   5a0 <_init+0x28>00000000000005d0 <__cxa_finalize@plt>:5d0: ff 25 52 0a 20 00       jmpq   *0x200a52(%rip)        # 201028 <_GLOBAL_OFFSET_TABLE_+0x28>5d6:  68 02 00 00 00          pushq  $0x25db: e9 c0 ff ff ff          jmpq   5a0 <_init+0x28>

好了到此,延迟绑定的第一步就来了:

1. 首先由“.text” 跳入 “.plt”

再来又是一个跳转,其实通过后面的注释已经很明确了,跳转到“201018”地址处,使用readelf -S Lib.so命令发现“.got.plt”的起始地址恰好是“201000”,而这个地址正好是“.got.plt”的第四项,0x18是24字节,在64位系统中,使用8字节表示一项,如此正好有三项,这三项依次是“.dynamic的地址”、“模块的ID”、“_dl_runtime_resolve() 的地址”

 5b0:    ff 25 62 0a 20 00       jmpq   *0x200a62(%rip)        # 201018 <_GLOBAL_OFFSET_TABLE_+0x18>

根据以上描述,延迟绑定的第二步就来了:

2. 由".plt" 跳入 “.got.plt” 因此我下面给大家分享的那副图存在一定程度上的不准确。

但在实现printf函数重定位之前,“201018”中存放的是下一条指令的地址。如此就跳转到下一条指令,pushq $0x0 压栈的这个数字是printf这个符号引用在重定位表“.rel.plt”中的下标。

接下来就要跳转到“0x7ffff7bd7610”,还是设置断点追踪。

运行至断点处,已无法使用disassemble反汇编,需要使用如下命令查看内容。

(gdb) x /5 0x00007ffff7bd7610
=> 0x7ffff7bd7610:  pushq  0x2009f2(%rip)        # 0x7ffff7dd80080x7ffff7bd7616:    jmpq   *0x2009f4(%rip)        # 0x7ffff7dd80100x7ffff7bd761c:   nopl   0x0(%rax)0x7ffff7bd7620 <printf@plt>: jmpq   *0x2009f2(%rip)        # 0x7ffff7dd8018 <printf@got.plt>0x7ffff7bd7626 <printf@plt+6>:    pushq  $0x0

第一条push用来放置本模块ID,第二条放置的就是_dl_runtime_resolve的地址,可通过stepi指令逐条运行。

(gdb) stepi
_dl_runtime_resolve () at ../sysdeps/x86_64/dl-trampoline.S:58
58  ../sysdeps/x86_64/dl-trampoline.S: 没有那个文件或目录.

而后通过_dl_runtime_resolve将printf的实际地址填入“.got.plt” 段中(此处就是“0x7ffff7dd8018”这个地址),下一次直接调用printf函数。printf函数返回的时候会根据堆栈里面保存的EIP直接返回调用者,而不会再继续执行printf@plt中的第二条指令开始的那段代码,那段代码只会在符号未被解析时执行一次。ELF为实现延迟绑定这一机制将GOT拆分成了“.got”和“ .got.plt” 。其中“.got” 用来保存全局变量引用的地址,“.got.plt” 用来保存函数引用的地址,也即所有对于外部函数的引用全部分离出来放到了“.got.plt”中。

再给大家盗一个图,图片出处还是上一个图的出处。下图就是第二次调用printf时,代码的执行路径示意。

好了延迟绑定的有关内容就给大家分析到这里,如果内容中有什么不对的地方,欢迎大家指正。

写接下来的内容之前,先给大家分享一下对于之前偏移地址总是算不对的问题,解决方法见下贴,其实就是我问的

http://bbs.csdn.net/topics/391911726

总结起来就是一句话,cpu在完成解码后,rip已经移动到下一条指令所在地址,所以在算偏移量的时候,要使用当前的rip值+当前rip所在指令长度+offset值。

好了,问题来了,那个offset是怎么算出来的?

通过反汇编结果我们可以知道glob的地址是0x20103c(不知有什么办法可以查看它的地址???),而数据访问指令的地址是0x6ef,指令长度是0xa(10),

0x20103c-0x6ef-0xa = 0x200943。

7.5 动态链接相关结构

与动态链接相关的结构有

  1. ".interp"动态链接器所在的路径
  2. “.dynamic”保存与动态链接所需的相关信息,是一个指针与数值的集合,指针指向其他动态链接所需的段。
  3. “.dynsym”动态符号表,保存与动态链接相关的符号,其实就是本模块所定义的函数与本模块引用的函数。与之相关的段还有“.dynstr” 动态符号字符串表,用于保存符号名。“.gnu.hash” 符号哈希表,为了加快符号的查找过程。
  4. 动态链接重定位表 “.rel.dyn”、“.rela.plt”,以上两个表用于记录哪些指令或数据需要重定位 。“.rela.dyn”实际上是对数据引用的修正,它所修正的位置位于".got"以及数据段;而“.rel.plt”是对函数引用的修正,它所修正的位置位于“.got.plt”中。将之前的内容串起来,“.plt”中是代码,运行到调用某一个函数的时候,首先跳入到".got.plt"中,而“.got.plt”中仅记录哪些符号需要重定位,并没有记录这些符号如何重定位,因此需要“.rela.plt”对如何重定位的信息加以记录。

我们在分析动态链接器工作流程的时候已经了解到在动态链接情况下,操作系统还不能在装载完可执行文件之后就把控制权交给可执行文件,因为我们知道可执行文件依赖于很多共享对象。这时候,可执行文件里对于很多外部符号的引用还处于无效地址的状态,即还没有跟相应的共享对象中的实际位置链接起来。所以在映射完可执行文件之后,操作系统会先启动一个动态链接器。那么问题来了,操作系统在加载程序时应使用哪个动态链接器就成为了问题。还是从最简单的想法出发,可以在elf文件中设立一个专门的段来记录所需的动态链接器。elf文件通过".interp" 段来记录动态链接器所在的位置。实验如下:

objdump -s program1
Contents of section .interp:400238 2f6c6962 36342f6c 642d6c69 6e75782d  /lib64/ld-linux-400248 7838362d 36342e73 6f2e3200           x86-64.so.2.

通过file命令查看动态链接器的属性

file /lib64/ld-linux-x86-64.so.2
/lib64/ld-linux-x86-64.so.2: symbolic link to `/lib/x86_64-linux-gnu/ld-2.21.so'

发现ld-linux-x86-64.so.2是对x86_64-linux-gnu/ld-2.21.so的符号链接,继续查看

 file /lib/x86_64-linux-gnu/ld-2.21.so
/lib/x86_64-linux-gnu/ld-2.21.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=903bb7a6deefd966dceec4566c70444c727ed294, stripped

当系统中的Glibc库更新或者安装其他版本的时候,软链接就会指向到新的动态链接器。

好了有了动态链接器,就有了程序启动的入口了。接下来就要完成动态链接,elf文件中设立了“.dyamic”段,用来保存动态链接器所需的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位的位置、共享对象初始化代码地址等。现就以上内容做详细分析。

先来看看“.dyamic”段的内容,“-d”选项用于查看“.dyamic”段的内容

readelf -d Lib.so Dynamic section at offset 0xe18 contains 24 entries:标记        类型                         名称/值0x0000000000000001 (NEEDED)             共享库:[libc.so.6]0x000000000000000c (INIT)               0x5780x000000000000000d (FINI)               0x7140x0000000000000019 (INIT_ARRAY)         0x200e000x000000000000001b (INIT_ARRAYSZ)       8 (bytes)0x000000000000001a (FINI_ARRAY)         0x200e080x000000000000001c (FINI_ARRAYSZ)       8 (bytes)0x000000006ffffef5 (GNU_HASH)           0x1f00x0000000000000005 (STRTAB)             0x3800x0000000000000006 (SYMTAB)             0x2300x000000000000000a (STRSZ)              177 (bytes)0x000000000000000b (SYMENT)             24 (bytes)0x0000000000000003 (PLTGOT)             0x2010000x0000000000000002 (PLTRELSZ)           72 (bytes)0x0000000000000014 (PLTREL)             RELA0x0000000000000017 (JMPREL)             0x5300x0000000000000007 (RELA)               0x4700x0000000000000008 (RELASZ)             192 (bytes)0x0000000000000009 (RELAENT)            24 (bytes)0x000000006ffffffe (VERNEED)            0x4500x000000006fffffff (VERNEEDNUM)         10x000000006ffffff0 (VERSYM)             0x4320x000000006ffffff9 (RELACOUNT)          30x0000000000000000 (NULL)               0x0

其实这个段就是由一系列“值”与“指针”组成,通过指针指向不同的“段”,动态链接器实现了对程序的装载与链接。

为了完成动态链接,首要需要的就是所依赖的符号和相关文件的信息。elf文件中使用“.dynsym”表保存模块之间的导入导出关系。仅靠“.dynsym”还不能完成任务,还需要专门保存符号名的字符串表,动态字符串表“.dynstr”。由于动态链接下,我们需要在程序运行时查找符号,为了加快符号的查找过程,往往还有辅助的符号哈希表“.hash”。通过命令让我们来看一下

readelf -s Lib.so Symbol table '.dynsym' contains 14 entries:Num:    Value          Size Type    Bind   Vis      Ndx Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 0000000000000578     0 SECTION LOCAL  DEFAULT    9 2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses6: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable7: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)8: 0000000000201038     0 NOTYPE  GLOBAL DEFAULT   22 _edata9: 0000000000201040     0 NOTYPE  GLOBAL DEFAULT   23 _end10: 0000000000201038     0 NOTYPE  GLOBAL DEFAULT   23 __bss_start11: 0000000000000578     0 FUNC    GLOBAL DEFAULT    9 _init12: 0000000000000714     0 FUNC    GLOBAL DEFAULT   12 _fini13: 00000000000006e0    52 FUNC    GLOBAL DEFAULT   11 foobarreadelf -sD Lib.so Symbol table of `.gnu.hash' for image:Num Buc:    Value          Size   Type   Bind Vis      Ndx Name8   0: 0000000000201038     0 NOTYPE  GLOBAL DEFAULT  22 _edata9   0: 0000000000201040     0 NOTYPE  GLOBAL DEFAULT  23 _end10   1: 0000000000201038     0 NOTYPE  GLOBAL DEFAULT  23 __bss_start11   1: 0000000000000578     0 FUNC    GLOBAL DEFAULT   9 _init12   2: 0000000000000714     0 FUNC    GLOBAL DEFAULT  12 _fini13   2: 00000000000006e0    52 FUNC    GLOBAL DEFAULT  11 foobar

共享对象需要重定位的主要原因是导入符号的存在。动态链接下,无论是可执行文件或共享对象,一旦它依赖于其他共享对象,也就是说有导入的符号时,那么它的代码或数据中就会有对于导入符号的引用。在编译时这些导入符号的地址未知,那么它的代码或数据中就会有对于导入符号的引用。在编译时这些导入符号的地址未知,在静态链接中,这些未知的地址引用在最终链接时被修正。在动态链接过程中,导入符号的地址在运行时确定,所以需要在运行时将这些导入符号的引用修正,因此需要重定位。

通过以下命令查看动态链接库的重定位表

readelf -r Lib.so 重定位节 '.rela.dyn' 位于偏移量 0x470 含有 8 个条目:Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000200e00  000000000008 R_X86_64_RELATIVE                    6b0
000000200e08  000000000008 R_X86_64_RELATIVE                    670
000000201030  000000000008 R_X86_64_RELATIVE                    201030
000000200fd8  000200000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000200fe0  000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000200fe8  000500000006 R_X86_64_GLOB_DAT 0000000000000000 _Jv_RegisterClasses + 0
000000200ff0  000600000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000200ff8  000700000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize + 0重定位节 '.rela.plt' 位于偏移量 0x530 含有 3 个条目:Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000201018  000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf + 0
000000201020  000400000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0
000000201028  000700000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize + 0

当动态链接器需要进行重定位时,它先查找“printf”的地址,“printf”位于libc-2.6.1.so。动态链接器在全局符号表里面找到“printf”后,就将该地址填入到“.got.plt”中相应的位置中去。在动态链接时,动态链接器根据重定位符号的类型采取对应的策略以进行重定位。以“printf”为例,R_X86_64_JUMP_SLO 类型的重定位符号只需要直接填入符号的地址即可。

7.6 动态链接的步骤和实现

这一章的内容我并没有十分搞懂,很多内容就是看个大概,也因此不给大家做太详细的分析。

动态链接基本上分为3步:先是启动动态链接器本身,然后装载所有所需的共享对象,最后是重定位和初始化。

1. 动态链接器的启动

在此就不给大家分析源码了,有机会的话再补上~

但还是给大家分析一点原理吧

对于动态链接器主要存在两个限制条件:

  1. 首先是,动态链接器本身不可以依赖于其他任何共享对象;
  2. 其次是动态链接器本身所需要的全局和静态变量的重定位工作由它本身完成。

为解决第一个限制条件,动态链接器选择的做法就是简单粗暴:在编写动态链接器时保证不使用任何系统库、运行库;对于第二个限制条件,动态链接器采取的策略仍然是最直接的方法——不用,不过这个不用是有一定说法的,这种具有一定限制条件的启动代码往往被称为自举(bootstrap)。

以下内容直接引自《程序的自我修养》:动态链接器入口地址即是自举代码的入口,当操作系统将进程控制权交给动态链接器时,动态链接器的自举代码即开始执行。自举代码首先会找到它自己的GOT,而GOT的第一个入口保存的即是“.dynamic” 段的偏移地址,由此找到了动态链接器本身的 “.dynamic”  段。通过“.dynamic”中的信息,自举代码便可以获得动态链接器本身的重定位表和符号表等,从而得到动态链接器本身的重定位入口,先将它们重定位。从这一步开始,动态链接器代码中才可以使用自己的全局变量与静态变量。

除了不能使用全局变量与静态变量外,甚至也不能调用函数,因为使用PIC模式编译的共享对象,对于模块内部的函数调用也是采用跟模块外部函数调用一样的方式,即也是PLT/GOT 方式,因此在GOT/PLT 没有重定位之前,自举代码不可以使用任何全局变量,也无法调用函数。

2. 装载共享对象

自举完成后,由于需要对未决符号进行重定位,因此动态链接器需要将可执行文件和链接器本身的符号表都合并到一个符号表中,这个表被称为全局符号表(Global Symbol Table)。在此基础上,链接器开始寻找可执行文件所依赖的共享对象,在“.dynamic”段中,有一种类型的入口是DT_NEEDED,它所指出的是该可执行文件(或共享对象)所依赖的共享对象。由此,链接器可以列出可执行文件所需要的所有共享对象,并将这些共享对象的名字放入到一个装载集合中,然后链接器开始从集合里取一个所需要的共享对象的名字,找到相应的文件后打开该文件,读取相应的ELF文件头和“ .dynamic ”段,然后将它相应的代码段和数据段映射到进程空间中。链接器一般采用广度优先遍历完成这一过程。

对于全局符号介入问题给大家分享一篇文章吧,全局符号介入一定要和符号重定义问题相区别,对于这一问题会专门开一篇blog进行讲解。

http://blog.chinaunix.net/uid-26548237-id-3837099.html

这里还要跟大家分享的一点就是,动态链接器是个非常特殊的共享对象,它不仅是个共享对象,还是个可执行程序。

对于全局符号介入与地址无关代码这一小节再给大家做一点解释。

由于全局符号介入问题的存在,对于模块内函数不能够采用模块内部直接调用的方法,因为一旦模块内部函数由于全局符号介入被其他模块中的同名函数覆盖,那么该函数仍然采用相对地址调用的话,那该函数的地址就需要重定位,这又与共享对象的地址无关型矛盾。所以对于模块内部函数调用,编译器只能采用第三种,即当作模块外部符号处理,也就是采用PLT方式处理,实验验证如下:

#include <stdio.h>static int glob;void moudleinbar(){glob = 2;
}void foobar()
{int a = 2;   glob = 1;moudleinbar();printf("Printing from Lib.so %d\n",glob);
}

反汇编结果如下:

75e: e8 8d fe ff ff          callq  5f0 <moudleinbar@plt>

可以看到,该模块内部采用了PLT处理方式。

如果一定要把函数调用方式改为模块内部调用,可以把函数改为静态函数,代码如下:

static void moudleinbar(){glob = 2;
}

反汇编结果如下:

70e: e8 cd ff ff ff          callq  6e0 <moudleinbar>
00000000000006e0 <moudleinbar>:6e0:   55                      push   %rbp6e1: 48 89 e5                mov    %rsp,%rbp6e4:    c7 05 4e 09 20 00 02    movl   $0x2,0x20094e(%rip)        # 20103c <glob>6eb: 00 00 00 6ee:   5d                      pop    %rbp6ef: c3                      retq

可以发现此时函数调用方式已变为模块内部调用。

3. 重定位和初始化

当共享对象装载完成后,链接器开始执行重定位工作。

重定位完成后,如果某个共享对象有“.init”段,那么动态链接器会执行“.init”段中的代码,用以实现共享对象特有的初始化过程。相应地,共享对象中还可能有“.finit” 段,当进程退出时会执行“.finit”段中的代码。

如果进程的可执行文件也有“.init”段,那么动态链接器不会执行它,因为可执行文件中的“.init” 、“.finit”段由程序初始化部分代码负责执行。

《程序员自我修养》第七章读书笔记相关推荐

  1. 《深入理解计算机系统》第七章读书笔记

    <深入理解计算机系统>第七章读书笔记 第七章:连接 连接 1.连接:将各种代码和数据部分收集起来并组合成为一个单一文件的过程.这个文件可被加载或拷贝到存储器并执行. 2.连接可以执行于编译 ...

  2. 《重构》第七章--读书笔记

    第七章 在对象之间搬移特性 --读书笔记 在对象的设计过程中,要决定把对象放在哪里,可能不会一开始就做对,但是可以运用重构,改变自己原先的设计,这就用到了本章所提到额重构手法. 7.1 Move Me ...

  3. 《文明之光》第七章读书笔记

    第七章--一个家族的奇迹--文艺复兴 综述:美第奇家族曾是这个世界上最富有,最具影响力的家族,他们控制着欧洲的金融,与皇室联姻,左右着教皇的任命.虽然这个家族随着它的最后一位成员的去世而终结,可我们现 ...

  4. 深入理解计算机系统-第七章(链接)笔记

    深入理解计算机系统-第七章(链接)笔记 背景 链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程 这个文件可被加载(拷贝)到存储器中并执行: 链接可以执行于编译时,也就是源代码翻译成机器码 ...

  5. 《构建之法》第4.17章读书笔记

    <构建之法>第4.17章读书笔记 第四章 原文语句: 异常不能跨过DLL或进程的边界来传递信息,所以异常不是万能的. 提出问题: 1.什么是DLL?DLL是来解决什么问题的? 网上说法: ...

  6. Python编程:从入门到实践第六章读书笔记6.3遍历字典

    Python编程:从入门到实践第六章读书笔记6.3遍历字典 #coding:gbk#6.3.1遍历所有的键-值对 user_0 = {'username': 'efermi','first': 'en ...

  7. 《人人都是产品经理》第四章读书笔记及读后感作文2400字

    <人人都是产品经理>第四章读书笔记及读后感作文2400字: 最近一直在忙别的学习,以至于好久没有更新公众号了,也好久没有写读书笔记了.<人人都是产品经理>这本书其实早在一个月前 ...

  8. 放松就上当了?——《思考,快与慢》第5章读书笔记

    <思考,快与慢>第5章读书笔记 是放松引起的错觉? 首先给大家一个判断,下图左右两家羊绒大衣店,忽略价格,你更喜欢哪一家?(2秒回答) 大部分人应该会选择左边的店铺,相较来说也更容易对左边 ...

  9. Android深度探索--HAL与驱动开发----第五章读书笔记

    第五章主要学习了搭建S3C6410开发板的测试环境.首先要了解到S3C6410是一款低功耗.高性价比的RISC处理器它是基于ARMI1内核,广泛应用于移动电话和通用处理等领域. 开发板从技术上说与我们 ...

最新文章

  1. CodeForces - 444C DZY Loves Colors(线段树+剪枝)
  2. 【Kafka】Kafka Leader:none ISR 为空 消费超时
  3. 世界你好! 个人网站搭建过程
  4. 电视android降低版本,电视猫旧版本下载-电视猫视频去升级版3.1.3 安卓版下载_飞翔下载...
  5. ACM/ICPC 2018亚洲区预选赛北京赛站网络赛 A、Saving Tang Monk II 【状态搜索】
  6. 生产线上怎么做“防错”?不妨看看这个“防错”技术案例!
  7. PHP 操作MongoDB
  8. 阿里达摩院/字节后端研发一面凉面经
  9. C语言 | 位域的使用详解
  10. 微信公众号 语音转文字api_原来微信不仅能实现语音转文字,还能实现文字转语音!你还不知吗...
  11. python把字符串转化为字典_python 将字符串转换为字典
  12. 国际上哪个学校计算机专业好,美国计算机专业大学排名前十有哪些?
  13. Python操作Neo4j图数据库的两种方式
  14. 计算机不同进制数之间的转换,计算机进制数之间的转换002
  15. Camera2报错: BufferQueue has been abandoned
  16. 绪论(p1-p2) author:run
  17. 正则表达式,终极使用!3个工具,搞定一切
  18. ROOT后RE管理器上无法更改权限,因为文件系统只读
  19. 175.纯 CSS 实现视频转场特效
  20. 无重复字符的最长子串(加注释)

热门文章

  1. 适合学生的蓝牙耳机有哪些?学生党必备平价蓝牙耳机
  2. NSString的stringWithFormat用法
  3. 荧光染料FITC标记泛素Ubiquitin Rhodamine(Ub);FITC-Ub;Ub-FITC
  4. 【Bluetooth蓝牙开发】一、开篇词 | 打造全网最详细的Bluetooth开发教程
  5. 无从下手的数字音频处理器?我来教你怎样玩
  6. hibernate基础sessionFactory
  7. [Swift]LeetCode741. 摘樱桃 | Cherry Pickup
  8. 红色抗战电影《铁血八大组》主创演职人员和观众见面会举行
  9. ISE约束文件UCF的基本语法
  10. alin的学习之路:在Qt中使用Oracle数据库