以下内容均为网络上整理的资料
一.链接是什么?
我们在这里可以去引用csapp 的定义 : 链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行。 了解这句话的含义,我们首先需要明白linux 系统上的一般编译过程:
需要强调的是,链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器(loader)加载到存储器并执行时;甚至执行于运行时(run time),由应用程序来执行。
二.链接过程有什么?
① 符号解析。目标文件定义和引用符号,符号解析的目的是将每个符号引用和一个符号定义联系起来;
②重定位:把每个符号定义与一个存储器位置联系起来,然后修改对这些符号的引用,是的他们指向这个存储器位置,从而实现重定位。
为了理解这一过程,我们需要补充一些基础知识
1.目标文件
我们讨论的目标文件是基于Unix系统下的ELF文件
解释:
.text:已编译程序的机器码;
.rodata:只读数据(read-only-data);
.data:已初始化的全局C变量;
.bss:未初始化的全局C变量(better save space);
.symtab:一个符号表(定义和引用的函数和全局变量信息);
.rel.text:代码重定位条目, 一个.text节中位置的列表,需要修改的位置;
.rel.data: 被模块引用或定义的任何全局变量的重定位信息;
.debug:一个调试符号表;
.line:原始C源程序中的行号和.text机器指令的映射;
.strtab: 一个字符串表
2.符号和符号表
定义和引用函数和全局变量的信息
3.符号解析
简单来说,链接器使得所有模块中的每一个符号都只有一个定义。链接器的作用主要就是把代码中的每一个符号引用和符号定义联系起来。
这里有可能出现问题的是,当多个模块(头文件之类的)定义同一个符号的时候,我们的链接器应该会怎么做?
首先在编译的时候,编译器会向汇编器输出每一个全局符号,分为强符号和弱符号,汇编器会把这个信息存放在可重定位目标文件的符号表里。其中函数和已初始化过得全局变量为强符号,未初始化的全局变量为弱符号。
GNU链接器ld所采用的的规则如下
1.不允许有多个强符号
2.如果有一个强符号和多个弱符号,则选择强符号
3.如果有多个弱符号,则随便选择一个
接下来我们来做几个实验吧
第一个
#include <stdio.h>
int n = 2;
int main()
{
printf("%d\n",n);
return 0;
}
//link1.c
int n = 3;
//link2.c
让我们来看看结果
可以很清楚的看到不允许同时定义两个强符号存在
#include <stdio.h>
int n ;
int main()
{
printf("%d\n",n);
return 0;
}
//link1.c
int n = 3;
//link2.c
初始化过的n 是强符号,被优先选择了。当我们怀疑代码由此原因因此,我们可以在编译的时候加上 -fno-common 这个参数
这样我们遇到多重定义的符号的时候,都会给出一条警告信息,不关强弱符号。
4.重定位
重定位是修正代码段和数据段对于每一个符号的引用,使得他们指向正确的运行位置
我们之前在ELF文件中提及过,.rel.text代表的是代码重定位条目; .rel.data是已经初始化数据的重定位条目
说明
offset 是需要修改的引用节的偏移量;
symbol 标志被修改引用应该指向的符号
type 告诉链接器如何修改新的引用
ELF有11种不同的重定位类型:我们只关心常用的两种
R_386_PC32(相对地址引用)和R_386_32(绝对地址引用)
在这里就不详细讨论了