计算机原理系列之七 ——– 链接过程分析

 在可执行文件详解详细的介绍了可执行文件的各个section的内容和功能,但是唯独.text section没有被提到。其实.text section的内容,我们在上一篇文章Linux X86 程序启动 – main函数是如何被执行的?中已经分析的很详细了。这篇文章将从链接的角度来探讨.text的指令的生成过程,尤其是引用到外部动态链接库代码的指令生成过程。

一、 静态链接和动态链接

 链接过程实际上是把多个可重定位文件合并成一个可执行文件的过程。这个过程中最重要的两个步骤是符号解析重定位。所谓的符号解析,就是将符号的定义和引用关联起来,而重定位就是给所有符号和指令添加运行时的地址的过程。这个两个过程具体来讲就是:

  1. 符号解析,就是根据编译器生成的可重定位文件中的符号表,来使符号定义和符号引用一一对应并关联。
  2. 重定位,就是将每个符号和内存中的一个位置关联起来,然后修改代码中所有对这些符号的引用,使它们指向这个内存位置。

 很多时候我们可能会复用多次某个函数或者某个模块,如果每次都需要把它对应的代码找出来,然后重新编译的话,这样效率是很低的。静态链接库的出现正好解决了这个问题。其基本原理是,将常用的函数或者模块单独编译成其对应的可重定位文件,然后再将其封装成一个单独的.a文件(静态链接库)。在链接的时候,根据解析的规则把使用到的模块打包到最终生成的可执行文件内。这个过程称为静态链接。但是,这样同时带来了两个问题:

  1. 如果更新了某个静态库,那么必须重新链接该可执行文件。
  2. 如果每个可执行程序都把库函数的代码复制到内存中其对应进程的文本段,那么这将是极大的浪费。

 于是,动态链接库应运而生。动态库在编译时使用了位置无关码,在运行和加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。动态链接的过程有别于静态链接,在创建可执行文件时,动态链接库的代码并未被复制到可执行文件中,而只是复制了一些符号表和重定位信息,而在运行时,要先执行动态链接,加载动态链接库的文本段和代码段到内存,修改符号表和重定位信息,然后再继续执行真正的可执行文件的代码段。

二、可执行文件代码段

 本篇文章仍然使用上篇文章中的add.cmain.c作为研究对象,分别研究静态链接和动态链接生成可执行文件的代码段的区别。由于在代码中使用到了libc库的库函数printf,这里的静态链接和动态链接指的就是静态链接或者动态链接libc库。

2.1 编译方法

2.1.1 静态链接

gcc -static -o add.static add.c main.c

 这里-static参数的意思是不使用动态链接,也就是说使用静态链接的方法生成可执行文件。

2.1.2 动态链接

gcc  -o add.static add.c main.c

 这条命令没有加任何特殊参数,也就是说默认情况下,gcc是使用动态链接的。

2.2 代码段分析

 在分析代码段之前,先查看一下这俩文件的大小:

$ ll -al add add.static
-rwxr-xr-x 1 users users 8696 Nov 27 16:26 add*
-rwxr-xr-x 1 users users 912784 Nov 27 16:26 add.static*

 可以看出来经过静态链接的文件比动态链接的文件大了100倍。使用readelf -S add add.static 分析这两个文件的section,发现对于add.static来讲,最占空间的两个section.text.symtab。结合objdump -d add add.static命令,我们发现add.static的代码段增加了大量的代码,与此同时符号表里也增加了大量的符号,显然,这些代码和符号来源于glibc库,正如我们之前所说,静态链接把glibc库相关的代码全部复制过来了(由于篇幅原因,这里就不在列举执行上述命令显示的结果)。

 下面我们查看由动态链接生成的可执行文件的代码段。执行命令objdump -d add,得到:

add:     file format elf64-x86-64


Disassembly of section .init:

00000000004003c8 <_init>:
  4003c8:   48 83 ec 08             sub    $0x8,%rsp
... 省略 ...

Disassembly of section .plt:

00000000004003f0 <printf@plt-0x10>:
  4003f0:   ff 35 12 0c 20 00       pushq  0x200c12(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
... 省略 ...

Disassembly of section .plt.got:

0000000000400420 <.plt.got>:
  400420:   ff 25 d2 0b 20 00       jmpq   *0x200bd2(%rip)        # 600ff8 <_DYNAMIC+0x1d0>
  400426:   66 90                   xchg   %ax,%ax

Disassembly of section .text:

0000000000400430 <_start>:
  400430:   31 ed                   xor    %ebp,%ebp
  400432:   49 89 d1                mov    %rdx,%r9
... 省略 ...

0000000000400460 <deregister_tm_clones>:
  400460:   b8 3f 10 60 00          mov    $0x60103f,%eax
  400465:   55                      push   %rbp
... 省略 ...

00000000004004a0 <register_tm_clones>:
  4004a0:   be 38 10 60 00          mov    $0x601038,%esi
  4004a5:   55                      push   %rbp
... 省略 ...

00000000004004e0 <__do_global_dtors_aux>:
  4004e0:   80 3d 51 0b 20 00 00    cmpb   $0x0,0x200b51(%rip)        # 601038 <__TMC_END__>
  4004e7:   75 11                   jne    4004fa <__do_global_dtors_aux+0x1a>
... 省略 ...

0000000000400500 <frame_dummy>:
  400500:   bf 20 0e 60 00          mov    $0x600e20,%edi
  400505:   48 83 3f 00             cmpq   $0x0,(%rdi)
... 省略 ...

0000000000400526 <add>:
  400526:       55                      push   %rbp
  400527:       48 89 e5                mov    %rsp,%rbp
  40052a:       89 7d fc                mov    %edi,-0x4(%rbp)
  40052d:       89 75 f8                mov    %esi,-0x8(%rbp)
  400530:       8b 05 06 0b 20 00       mov    0x200b06(%rip),%eax        # 60103c <times>
  400536:       83 c0 01                add    $0x1,%eax
  400539:       89 05 fd 0a 20 00       mov    %eax,0x200afd(%rip)        # 60103c <times>
  40053f:       8b 55 fc                mov    -0x4(%rbp),%edx
  400542:       8b 45 f8                mov    -0x8(%rbp),%eax
  400545:       01 d0                   add    %edx,%eax
  400547:       5d                      pop    %rbp
  400548:       c3                      retq

0000000000400549 <main>:
  400549:       55                      push   %rbp
  40054a:       48 89 e5                mov    %rsp,%rbp
  40054d:       48 83 ec 10             sub    $0x10,%rsp
  400551:       c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%rbp)
  400558:       c7 45 fc 03 00 00 00    movl   $0x3,-0x4(%rbp)
  40055f:       8b 55 fc                mov    -0x4(%rbp),%edx
  400562:       8b 45 f8                mov    -0x8(%rbp),%eax
  400565:       89 d6                   mov    %edx,%esi
  400567:       89 c7                   mov    %eax,%edi
  400569:       e8 b8 ff ff ff          callq  400526 <add>
  40056e:       89 c2                   mov    %eax,%edx
  400570:       8b 05 c6 0a 20 00       mov    0x200ac6(%rip),%eax        # 60103c <times>
  400576:       89 c6                   mov    %eax,%esi
  400578:       bf 44 06 40 00          mov    $0x400644,%edi
  40057d:       b8 00 00 00 00          mov    $0x0,%eax
  400582:       e8 79 fe ff ff          callq  400400 <printf@plt>
  400587:       8b 55 fc                mov    -0x4(%rbp),%edx
  40058a:       8b 45 f8                mov    -0x8(%rbp),%eax
  40058d:       89 d6                   mov    %edx,%esi
  40058f:       89 c7                   mov    %eax,%edi
  400591:       e8 90 ff ff ff          callq  400526 <add>
  400596:       89 c2                   mov    %eax,%edx
  400598:       8b 05 9e 0a 20 00       mov    0x200a9e(%rip),%eax        # 60103c <times>
  40059e:       89 c6                   mov    %eax,%esi
  4005a0:       bf 44 06 40 00          mov    $0x400644,%edi
  4005a5:       b8 00 00 00 00          mov    $0x0,%eax
  4005aa:       e8 51 fe ff ff          callq  400400 <printf@plt>
  4005af:       b8 00 00 00 00          mov    $0x0,%eax
  4005b4:       c9                      leaveq
  4005b5:       c3                      retq
  4005b6:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  4005bd:       00 00 00

00000000004005c0 <__libc_csu_init>:
  4005c0:   41 57                   push   %r15
  4005c2:   41 56                   push   %r14
... 省略 ...
0000000000400630 <__libc_csu_fini>:
  400630:   f3 c3                   repz retq 

Disassembly of section .fini:

0000000000400634 <_fini>:
  400634:   48 83 ec 08             sub    $0x8,%rsp
  400638:   48 83 c4 08             add    $0x8,%rsp
  40063c:   c3                      retq   

2.2.1 可重定位文件代码段

 根据前面的陈述,我们知道所有的可重定位文件的代码段最终会合并到可执行文件的代码段中。对于当前的例子,也就是说add.omain.o的.text section将会被合并到最终生成的可执行文件add的.text中。上面列举出add.textsection反编译出来的代码。我们使用objdump -d add.o main.o来反编译出可重定位文件中的代码段。如下:

$ objdump -d add.o main.o

add.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <add>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d fc                mov    %edi,-0x4(%rbp)
   7:   89 75 f8                mov    %esi,-0x8(%rbp)
   a:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 10 <add+0x10>
  10:   83 c0 01                add    $0x1,%eax
  13:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # 19 <add+0x19>
  19:   8b 55 fc                mov    -0x4(%rbp),%edx
  1c:   8b 45 f8                mov    -0x8(%rbp),%eax
  1f:   01 d0                   add    %edx,%eax
  21:   5d                      pop    %rbp
  22:   c3                      retq

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 83 ec 10             sub    $0x10,%rsp
   8:   c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%rbp)
   f:   c7 45 fc 03 00 00 00    movl   $0x3,-0x4(%rbp)
  16:   8b 55 fc                mov    -0x4(%rbp),%edx
  19:   8b 45 f8                mov    -0x8(%rbp),%eax
  1c:   89 d6                   mov    %edx,%esi
  1e:   89 c7                   mov    %eax,%edi
  20:   e8 00 00 00 00          callq  25 <main+0x25>
  25:   89 c2                   mov    %eax,%edx
  27:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 2d <main+0x2d>
  2d:   89 c6                   mov    %eax,%esi
  2f:   bf 00 00 00 00          mov    $0x0,%edi
  34:   b8 00 00 00 00          mov    $0x0,%eax
  39:   e8 00 00 00 00          callq  3e <main+0x3e>
  3e:   8b 55 fc                mov    -0x4(%rbp),%edx
  41:   8b 45 f8                mov    -0x8(%rbp),%eax
  44:   89 d6                   mov    %edx,%esi
  46:   89 c7                   mov    %eax,%edi
  48:   e8 00 00 00 00          callq  4d <main+0x4d>
  4d:   89 c2                   mov    %eax,%edx
  4f:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 55 <main+0x55>
  55:   89 c6                   mov    %eax,%esi
  57:   bf 00 00 00 00          mov    $0x0,%edi
  5c:   b8 00 00 00 00          mov    $0x0,%eax
  61:   e8 00 00 00 00          callq  66 <main+0x66>
  66:   b8 00 00 00 00          mov    $0x0,%eax
  6b:   c9                      leaveq
  6c:   c3                      retq

2.2.2 链接前后代码段对比

 现在我们对比连接前后的代码有何不同。

多了一些函数

 显然,可执行文件反编译出来的代码比原来两个可重定位文件的代码多了很多函数。在反编译出来的可执行代码中不仅包括源代码中定义的函数,还增加了一些函数,比如:__libc_start_main,__libc_csu_init等。这些函数是由链接器添加的。其中,部分函数执行于main函数之前,为main函数的执行提供执行环境。部分函数执行与main函数之后,作用是在main函数执行结束做一些收尾工作。关于这部分代码我们已经在Linux X86 程序启动 – main函数是如何被执行的?做过详细的分析,这里就不再赘述。

运行地址发生了变化

 前面谈到过,objdump会将代码段中的二进制指令反编译成其对应的汇编代码,并且根据这些代码所在的section的基地址计算出每条指令的地址,并将其显示出来,也就是输出信息中最左侧一列的数字。显而易见,在链接前后,所有指令的运行地址发生了变化。那么,经过链接之后的地址是怎么得来的呢?

运行地址也就是链接地址。实际上指的是,程序在运行过程中,该指令对应的内存地址。

我们再回到本系列的主题:程序的加载和运行。可执行程序生成之后,是保存在硬盘中的,当用户执行该程序的时候,该程序会被加载器按照program header table的描述将程序的代码段和数据段从硬盘加载到内存中。在使用MMU的机器上,CPU处理的地址是虚拟地址。同样的,加载到内存中的地址指的也是虚拟地址。由于虚拟内存的抽象,每个进程都认为其独占内存,因此,每个可执行程序总是可以被加载到相同的内存地址(虚拟地址),其实,这些内存地址都是位于各个可执行程序独自的内存空间的地址。但是,对于MMU来讲,这些相同的虚拟地址其实对应了不同的物理地址。而对于CPU来讲,指令是按照其虚拟地址一条条的被加载到CPU中运行的。

如上所述,加载器是按照program header table的描述来给程序代码段分配指令地址的。具体的过程如下:

 还记得我们在可执行文件详解中segment和section的对应关系吧?每个section按照这个mapping表顺序排列构成了不同的segment。其中第2个segment就是可执行文件的代码段。代码段中第一个section是.interp,其起始地址是0x400238,然后,加上.interp section的大小,就是下一个section .note.ABI-tag的起始地址。依次类推,对于可执行文件详解中的可执行文件add来讲,其.text的起始地址就是0x400430

为什么对于X86_64架构来讲,所有可执行文件的text segment的起始地址都是0x400000?

实际上是链接脚本规定的,在链接过程中,链接器会根据链接脚本的描述来构建可执行文件。对于X86_64来讲,其默认的链接脚本位于/usr/lib/ldscripts/elf_x86_64.x。在其中我们发现这句话:
__executable_start = SEGMENT_START("text-segment", 0x400000))
它指定了可执行的text segment应起始于0x400000

 根据上述objdump -d的输出,.text的第一个函数是_start,因此,_start的第一条指令地址就是0x400430。后面每个函数的地址等于它的上一个函数的地址加上该函数自身的字节数。这样,完成了给每个函数重定位(分配运行地址)的过程。

 函数中的每条指令的地址的重定位类似于函数重定位。函数的首地址即是第一条指令的首地址,后面每条指令的地址依次等于上一条指令的地址加上该指令的字节数。回忆编译过程分析中,在编译完成后,指令引用外部符号时,生成了对应的操作数和符号的占位符,此时,对于除动态链接库的符号外,其他的符号都已经有了确定的地址。因此,结合符号表我们就可以将类似的指令完成重定位。

 比如在本例中,对于main函数来讲,调用了两个外部函数addprintf,根据上面信息,add函数相关的代码已经确定在0x400526处。因此,该地址就是call指令的调用add的操作数。而上述代码显示的400400处似乎并非是printf函数真正的实现。没错,这是因为printf函数是属于libc的库函数,但是,我们知道对于动态链接来讲,在生成可执行文件时,并未将它所依赖的动态库的代码复制过来,而只是复制了相关的重定位信息和符号表,所以,此时依然不能确定printf函数的地址。而400400处的内存值只是一个跳板,等程序运行时,动态链接器会将相关动态链接库的代码链接进来,修改这个跳板处对应的值,就可以让跳转指令正确的跳转到printf函数真正的内存地址处执行了。稍后我们将会对该过程做详细的分析

三、未解决问题

 细心的读者可能发现了,所有可执行程序的第2个segment(text segment)起始地址都是0x400000。原则上,这个segment里的第一个section(.interp section)的起始地址就应该是0x400000,但是实际上,我们看到第一个section(.interp section)的起始地址是0x400238,猜测原因可能是基于安全性考虑,使能了地址空间布局随机化(Address-Space Layout Randomization, ASLR)。但是,如果是地址空间随机化原因导致的,不同程序的.interp sections的地址应该不一样才对,不然也没有随机的必要了。但是查看过几个可执行程序之后发现,它们的.interp的地址都是0x400238。这个原因到底是为什么呢?希望知道原因的大牛们不吝赐教!谢谢!

四、参考阅读

  1. Understanding ld-linux.so.2
  2. The ELF format – how programs look from the inside
  3. Introduction to the ELF Format (Part VII): Dynamic Linking / Loading and the .dynamic section

发表评论

电子邮件地址不会被公开。 必填项已用*标注