首先来看一个简单的程序。下面是是两段程序,分别放在link.c
和bar.c
中。函数
/* link.c */
#include<stdio.h>
void f(void);
int x = 13;
int main() {
f();
printf("x=%d\n", x);
return 0;
}
/* bar.c */
int x;
void f() {
x = 12;
}
复制代码
如今咱们使用命令gcc -o linkbar link.c bar.c
将其编译为可执行文件,而后用./linkbar
运行文件,运行结果为:x=12
。微服务
有趣的事情就发生了,在link.c
中分明定义的是x = 13
,并且打印语句使用的也是本身模块的x
变量,怎么就变成了 12 了呢?ui
那问题是否是就出在bar.c
文件中呢?这个文件中也定义了x
变量,而且在f()
函数中将其赋值了,可是这个x
是在bar.c
中,link.c
中也有本身的x
变量,按理来讲它们应该是相互不影响的,让人疑惑!!!!spa
实际上这都是连接器搞的鬼,上面场景在工做中遇到的可能性不小,这种错误引入程序后,并不会当即表现出来,而是可能在其它你想不到的地方报错,试想一下,在一个拥有成百上千个模块的大型系统中,发生了这样的错误,而你也不知道错误的源头,让你定位出这个错误,其困难程度可想而知。code
要理清这个问题,须要去了解连接器是怎么工做的。咱们都知道,如今的系统愈来愈大,咱们将其分解成为更小的、更好管理的模块,能够独立地修改和编译这些模块(像不像微服务?),这样协做让咱们没必要将整个应用程序组织成一个巨大的源文件。内存
为了构造可执行文件,连接器须要完成符号解析
和重定位
两个主要任务,这里咱们主要看看符号解析。string
每一个符号都对应一个函数、一个全局变量或一个静态变量(C 中以static
声明的变量),符号解析就是要把每一个符号引用与符号定义关联起来,注意不包括局部变量哦。it
C 语言中 static 声明的变量和 Java、C++ 中的 private 声明同样,不带 static 的就是 public 类型的。io
你确定据说过可重定位
这个词,源程序在通过预处理、编译、汇编以后产生的就是可重定位目标文件
,怎么理解可重定位呢?简单来讲,就是说文件里面的代码段和数据的地址尚未最终肯定。编译
在每一个可重定位目标文件中都有一个符号表,这个符号表包含了可重定位目标文件本身定义和引用的符号信息。共有三种不一样的符号:一、由本身定义并能被其它模块引用的全局符号;二、由其它模块定义并被本身引用的全局符号;三、只能被本身引用的局部符号。
连接器解析符号引用是将每一个引用与它的输入的可重定位目标文件的符号表中的一个肯定的符号定义关联起来,可是不一样的可重定位目标文件可能有多个同名的全局符号,即多重定义的全局符号。
函数和已经初始化的全局变量是强符号
,未初始化的全局变量是弱符号
。根据强弱符号的定义,Linux 连接器使用三条规则来处理多重定义的符号。
看到这里,就明白为何会有开篇程序出现的那个错误了,由于它正好知足规则二,因此bar.c
中的x
变量实际上仍是link.c
中的x
变量。
尤为规则 2 和 3 的应用会带来一些不易察觉的运行时错误,这是很是难理解的,尤为是重复的符号中还有不一样的类型时,好比下面这个例子。
/* link.c */
#include<stdio.h>
void f(void);
int y = 12;
int x = 13;
int main() {
f();
printf("x=0x%x y=0x%x \n", x, y);
return 0;
}
/* bar.c */
double x;
void f() {
x = -0.0;
}
复制代码
在一台 x86-64/Linux 机器上,double
类型是 8 个字节,而int
类型是 4 个字节,假设系统中x
的地址是0x601020
,y
的地址是0x601024
,而bar.c
的赋值x = -0.0;
将会用负零的双精度浮点表示覆盖内存中x
和y
的位置。