[toc]
在可执行文件详解详细的介绍了可执行文件的各个section的内容和功能,但是唯独.text
section没有被提到。其实.text
section的内容,我们在上一篇文章Linux X86 程序启动 – main函数是如何被执行的?中已经分析的很详细了。这篇文章将从链接的角度来探讨.text
的指令的生成过程,尤其是引用到外部动态链接库代码的指令生成过程。
一、 静态链接和动态链接
链接过程实际上是把多个可重定位文件合并成一个可执行文件的过程。这个过程中最重要的两个步骤是符号解析和重定位。所谓的符号解析,就是将符号的定义和引用关联起来,而重定位就是给所有符号和指令添加运行时的地址的过程。这个两个过程具体来讲就是:
- 符号解析,就是根据编译器生成的可重定位文件中的符号表,来使符号定义和符号引用一一对应并关联。
- 重定位,就是将每个符号和内存中的一个位置关联起来,然后修改代码中所有对这些符号的引用,使它们指向这个内存位置。
很多时候我们可能会复用多次某个函数或者某个模块,如果每次都需要把它对应的代码找出来,然后重新编译的话,这样效率是很低的。静态链接库的出现正好解决了这个问题。其基本原理是,将常用的函数或者模块单独编译成其对应的可重定位文件,然后再将其封装成一个单独的.a文件(静态链接库)。在链接的时候,根据解析的规则把使用到的模块打包到最终生成的可执行文件内。这个过程称为静态链接。但是,这样同时带来了两个问题:
- 如果更新了某个静态库,那么必须重新链接该可执行文件。
- 如果每个可执行程序都把库函数的代码复制到内存中其对应进程的文本段,那么这将是极大的浪费。
于是,动态链接库应运而生。动态库在编译时使用了位置无关码,在运行和加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。动态链接的过程有别于静态链接,在创建可执行文件时,动态链接库的代码并未被复制到可执行文件中,而只是复制了一些符号表和重定位信息,而在运行时,要先执行动态链接,加载动态链接库的文本段和代码段到内存,修改符号表和重定位信息,然后再继续执行真正的可执行文件的代码段。
二、可执行文件代码段
本篇文章仍然使用上篇文章中的add.c
和main.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.o
和main.o
的.text section将会被合并到最终生成的可执行文件add
的.text中。上面列举出add
的.text
section反编译出来的代码。我们使用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函数来讲,调用了两个外部函数add
和printf
,根据上面信息,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
。这个原因到底是为什么呢?希望知道原因的大牛们不吝赐教!谢谢!
0x400000 到0x400238这一段里面是什么啊
动态链接命令错了哟,是不是应该是 `gcc -o add add.c main.c`
也可以这么写,-o 后面跟的参数是动态链接生成的文件名字,可以是任意有效字符串。