本次我们要解释的是链接是如何运行的,下面先上一个小小的例子
Linux初步运行代码
大多数编译器提供编译器驱动程序,它代表用户在需要的时候调用语言处理器,编译器,汇编器,和链接器。
它将main.c翻译成为一个ASCII码文件的中间文件main.i,之后驱动编译器翻译器将其翻译成一个ASCII汇编语言文件,main.s,之后驱动汇编器再将其翻译成为一个可重定位的目标文件main.o,驱动程序以相同的过程生成sum.o,最后运行链接器ld,将这些.o文件组合起来,构成可执行文件prog(注意shell调用操作系统中一个叫做loader的函数,将prog中的代码和数据复制到内存中,然后将控制转移到程序的开头)
静态链接器,输入的可重定位的文件有各种不同的代码和数据节(section)组成
链接器有两大重要任务:符号解析和重定位。
目标文件纯粹是字节块的集合。
readelf命令可以解析可重定位的目标文件,可执行的目标文件,和共享文件
下面是可重定位的目标文件
ELF头描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。
例如:
.text:已编译程序的机器代码
.rodata:只读数据
.data:已初始化的全局和静态C变量
.bss:未初始化的全局和静态C变量
.symtab:符号表
.rel.text:一个.text节中的位置的列表
.rel.data:被模块引用或者定义的所有全局变量的重定位信息
.debug:调试符号表
.line:原始C源程序中行号和.text节中极其指令的映射
.strtab:一个字符串表
常用命令
readelf
一般用于查看ELF格式的文件信息,常见的文件如在Linux上的可执行文件,动态库(.so)或者静态库(.a) 等包含ELF格式的文件
参数说明
1、选项 -h(elf header),显示elf文件开始的文件头信息。
2、选项 -l(program headers),segments 显示程序头(段头)信息(如果有数据的话)
3、选项 -S(section headers),sections 显示节头信息(如果有数据的话)。
4、选项 -g(section groups),显示节组信息(如果有数据的话)
5、选项 -t,section-details 显示节的详细信息(-S的)
6、选项 -s,symbols 显示符号表段中的项(如果有数据的话)
objdump
使用 objdump 查看目标文件或者可执行的目标文件
例子:
objdump -d add.o
# objdump -d add.o
add.o: file format elf32-i386
Disassembly of section .text:
00000000 <add_int>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 45 0c mov 0xc(%ebp),%eax
6: 8b 55 08 mov 0x8(%ebp),%edx
9: 01 d0 add %edx,%eax
b: 5d pop %ebp
c: c3 ret
nm
使用 nm 显示二进制目标文件的符号表,包括符号地址、符号类型、符号名等
例子:
nm add.o
# nm add.o
00000000 T add_int
与静态库链接
所有的编译系统提供一个机制,把所有相关的目标模块打包成一个单独的文件,称之为静态库(static library)
在链接的时候,链接器将只复制被程序引用的目标模块,这减少了可执行文件在磁盘和内存的空间
在Linux系统中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中,存档文件是一组连接起来的可重定位目标文件的集合
下例:
使用AR工具建立静态库
图片中先用gcc编译了main2.c文件
而后链接,注意,这里静态库要放在最后
-static 告诉编译器,链接器应该构建一个完全链接的可执行目标文件。
tips:为什么要把静态库放在最后?
这与链接器使用静态库解析引用的方式有关,即,采用前面无法解释的符号,放给后面进行解释,而如果反转,静态库中的符号无法解释,就会出错。
动态链接
首先我们要先明确一下,无论是静态链接和动态链接,他们都是链接,都不会把库中其他无用的函数加载进来,而动态链接解决的问题是函数重复调用,内存被重复占用的问题,所以提出了动态链接的概念
动态链接的步骤中往往有静态链接,只是静态链接生成的部分的可重定位的目标文件,而剩下的一部分,靠动态链接运行时动态加载。因为只是映射和共享,所以可以解决上面的问题。
重定位
由于袁老师的视频我们可以知道,链接的两大步骤就是符号解析和重定位,重定位可以理解为
1.合并相同的节(数据节和代码节)
2.对定义的符号进行重定位
3.对引用的符号进行重定位
符号解析得到的内容实际上是U、E、D
具体内容如下:
对每一个输入文件来说,首先判断是不是库文件。
如果不是库文件,就是目标文件 f。就能把目标文件放入 E 中,根据 f 中未解析符号和定义符号判断后分别放入 U、D 中
比如说,f 中有一个未解析符号 k,如果 D 中存在对它的定义,那么就可以建立联系。如果没有就放入 U 中。
如果是库文件,会试图把所有 U 中的符号与库文件中的符号匹配,匹配上了就从 U 放入 D 中。并把匹配上的模块放入 E 中。一直重复直到 U D 不再变化。库文件剩下的内容直接就不管了。
如果往 D 中放入了一个已经存在的符号或者扫描完所有文件后 U 还是非空,则链接器会停止并报错。否则将 E 中内容经过重定位后合并,生成可执行文件
IA-32 架构中有两种重定位的方式,对应着 r_type
R_386_PC32
R_386_32
他们都是位置无关属性的代码,具体可参照袁老师的视频,讲的非常详细