引入:理解链接过程
由一个.c
源文件得到一个二进制可执行文件需要经历预处理、编译、汇编和链接:
-
预处理:包括头文件的包含、宏定义的扩展、条件编译的选择等
gcc -E hello.c
-
编译:经过词法分析、语法分析、语义分析,将源代码翻译成汇编代码
gcc -S hello.c
-
汇编:把作为中间结果的汇编代码翻译成了机器代码,即目标代码
gcc -c hello.s
代码在链接之前经历:源码文件(.c)---> 汇编代码(.s)---> 目标文件(.o),此时得到的目标文件还不是可以运行的二进制文件,利用 readelf -e hello.o
可以看到hello.o
只有elf头和节头,没有程序头,它不是一个可执行文件,它是一个可重定位文件,需要经过链接将它制作成可执行文件
链接:由于项目很可能有多个文件(模块)相互依赖、相互引用,因此需要通过链接处理好各个模块之间的相互引用关系。链接过程做的事包括:
- 符号决议
- 地址空间分配
- 符号重定位
Q:为什么要链接?gcc -c
产生的编译结果的elf文件不是已经包含了能够执行的汇编代码吗?
A:编译过程只针对单个文件进行,将高级程序语言的代码翻译成机器码,而没有考虑具体的运行时,如程序的加载地址、外部的变量符号函数等。举个例子:
1.c
:
#include <stdio.h>
extern int a;
int main()
{
a += 1;
printf("hello
");
return 0;
}
2.c
:
int a = 1;
源文件1.c
引用了2.c
中的变量a
。编译它们得到1.o
和2.o
:
gcc -c 1.c 2.c
然后利用objdump -d 1.o
查看反汇编代码:
qxy@qxy-XPS-13-9360:~/Desktop/test$ objdump -d 1.o
1.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # a <main+0xa>
a: 83 c0 01 add $0x1,%eax
d: 89 05 00 00 00 00 mov %eax,0x0(%rip) # 13 <main+0x13>
13: bf 00 00 00 00 mov $0x0,%edi
18: e8 00 00 00 00 callq 1d <main+0x1d>
1d: b8 00 00 00 00 mov $0x0,%eax
22: 5d pop %rbp
23: c3 retq
此时编译得到的代码只是单纯的对源文件的c代码进行转译,而并没有得到正确的变量a
的值。同时也可以查看该目标代码的elf头:
qxy@qxy-XPS-13-9360:~/Desktop/test$ readelf -h 1.o
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 736 (bytes into file)
标志: 0x0
本头的大小: 64 (字节)
程序头大小: 0 (字节)
Number of program headers: 0
节头大小: 64 (字节)
节头数量: 13
字符串表索引节头: 12
它的入口点地址是0x0
,也不是程序最终的入口虚拟地址
然后再使用gcc -o hello 1.o 2.o
将两个目标文件进行链接,得到可执行文件hello
,此时再看main
函数的反汇编代码和文件的elf头:
qxy@qxy-XPS-13-9360:~/Desktop/test$ objdump -d hello
...
00000000004005fd <main>:
4005fd: 55 push %rbp
4005fe: 48 89 e5 mov %rsp,%rbp
400601: 8b 05 29 0a 20 00 mov 0x200a29(%rip),%eax # 601030 <a>
400607: 83 c0 01 add $0x1,%eax
40060a: 89 05 20 0a 20 00 mov %eax,0x200a20(%rip) # 601030 <a>
400610: bf b4 06 40 00 mov $0x4006b4,%edi
400615: e8 d6 fe ff ff callq 4004f0 <puts@plt>
40061a: b8 00 00 00 00 mov $0x0,%eax
40061f: 5d pop %rbp
400620: c3 retq
400621: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
400628: 00 00 00
40062b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
...
qxy@qxy-XPS-13-9360:~/Desktop/test$ readelf -h hello
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x400500
程序头起点: 64 (bytes into file)
Start of section headers: 6488 (bytes into file)
标志: 0x0
本头的大小: 64 (字节)
程序头大小: 56 (字节)
Number of program headers: 9
节头大小: 64 (字节)
节头数量: 30
字符串表索引节头: 29
显而易见有几处发生了变化:文件从重定位文件变成了可执行文件,入口地址变更为一个合适的虚拟地址,add
语句中找到了正确的变量a
。这就是前文所说的“符号决议,地址空间和分配符号重定位”。链接器在重定位的过程中对目标文件中未定义的部分发生修改
静态链接与动态链接
静态链接: 所有目标文件和外部库静态地绑定在一起。在最终的可执行文件中,所有符号都被解析出来,运行时不依赖任何外部库
动态链接: 外部内容没有被完整地拷贝进最终的可执行文件,而是在运行时动态地加载。程序运行时必须能够找到这些库,解析动态链接进来的符号引用,然后才能真正开始执行程序
继续以上述代码举例。首先是静态库:
$ gcc -c 1.c 2.c
$ ar rv lib2static.a 2.o
$ gcc -o hello_static 1.o -L. -l2static
将2.o
打包成静态库,然后链接静态库得到可执行文件hello_static
由于静态库的内容已经完整的整合到hello_static
中,因此可以在任何目录下执行hello_static
,无论当前目录中有没有lib2static.a
静态库libxx.a
相当于是若干个x.o
的整合包,对他们进行归档形成一个静态库文件。链接一个静态库与链接库中包含的所有.o
文件效果等同
然后是动态库:
$ gcc -c 1.c 2.c
$ gcc --shared 2.o -o lib2dynamic.so
$ gcc -o hello_dynamic 1.o -L. -l2dynamic
由于动态库目录指定为当前目录,链接得到的可执行文件hello_dynamic
只能在与动态库lib2dynamic.so
同目录下运行。此时如果重命名、删除lib2dynamic.so
,或将hello_dynamic
拷贝到没有lib2dynamic.so
的目录下运行,会得到错误:
qxy@qxy-XPS-13-9360:~/Desktop/test$ ./hello_dynamic
./hello_dynamic: error while loading shared libraries: lib2dynamic.so: cannot open shared object file: No such file or directory
因为程序在执行的过程中动态加载动态库的内容,但发现找不到这个文件