符号解析
链接器解析符号引用的方法是将每个引用与它输入的可重定向目标文件的符号表中的一个确定的符号定义联系起来。对于那些和引用定义在相同模块中的本地符号的引用,符号解析是非常简单明了的。编译器只允许每个模块中每个本地符号只有一个定义。编译器还确保静态本地变量,它们也会有本地链接器符号,拥有唯一的名字。
不过对全局符号的解析就棘手多了。当编译器遇到一个不是在当前模块中定义的符号(变量或符号)时,它会假设符号是在其他某个模块中定义的,生产一个链接器符号表条目,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个引用的符号,它就会输出一条(通常很难阅读的)错误信息并终止。比如,如果我们试着在一台Linux机器上编译和链接下面的源文件:
voidfoo(void);
intmain(){
foo();
return0;
}
那么编译器会没有障碍的运行,但是链接器无法解析对foo的引用时,它会终止:
unix>gcc -Wall -o2 -o linkerrorlinkerror.c
/tmp/ccSz5uti.o: In function 'main':
/tmp/ccSz5uti.o(.text+0x7):undefinedreference to 'foo'
collect2: ldreturned 1 exit stauts
对全局符号的符号解析很棘手,还因为多个目标文件可能会定义相同的符合.在这种情况中,链接器必须要么标志一个错误,要么以某种方法选出一个定义并抛弃其他定义。Unix系统采纳的方法涉及编译器、汇编器和链接器之间的协作,这样也可能给不警觉的程序员带来麻烦。
链接器解析多重定义的全局符号
在编译时,编译器向汇编器输出的每个全局符号,或者是强或者是弱,而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。
根据强弱符号的定义,Unix链接器使用下面的规则来处理多重定义的符号:
1)不允许有多个强符号
2)如果有一个强符号和多个弱符号,那么选择强符号
3)如果有多个弱符号,那么从这些弱符号中选择任意一个
例如,假设我们试图编译和链接下面两个c模块:
int main(){
return0:
} foo1.c
int main(){
return0;
} bar1.c
在这种情况下,链接器将生成一条错误信息,因为强符号main被定义了两次(规则1):
unix>gcc foo1.c bar1.c
****** :In function 'main':
****** : mutipledefinition of 'main'
****** : firstdefined here
相似的,链接器对于下面的模块也会生成一条错误信息,因为强符号x被定义了两次(规则2):
int x = 123;
int main(){
return0;
}foo2.c
int x = 123;
void f(){}bar2.c
然而,如果在和一个模块里x未被初始化,那么链接器将安静地选择定义在另一个模块中的强符号(规则2):
#include <stdio.h>
void f();
int x = 123;
int main(){
f();
printf("x= %d\n",x);
return0;
}foo3.c
int x;
void f(){
x= 124;
}bar3.c
运行时函数f将x的值由123改为124,这会给main函数的作者带来不受欢迎的意外!注意,链接器通常不会表明它检测到多个x的定义:
unix> gcc -o foobar3 foo3.c bar3.c
unin>./foobar3
x = 124
如果x有两个弱定义,也会发生相同的情况(规则3):
#include <stdio.h>
void f();
int x ;
int main(){
x=123;
f();
printf("x= %d\n",x);
return0;
}foo4.c
int x;
void f(){
x = 124;
}bar4.c
规则2和规则3的应用会造成一些不易察觉的运行时错误,对于不警惕的程序员来说,这是很难解释的,尤其当如果重复的符号定义还有不同的类型时,考虑下面的这个例子,其中x在一个模块中定义为int,在另一个模块中定义为double:
#include <stdio.h>
void f();
int x=15213;
int y = 15213;
int main(){
f();
printf("x= 0x%x y = 0x%x \n",x,y);
return0;
}foo5.c
double x;
void f(){x = -0.0;}bar5.c
在一台IA32/Linux机器上,double类型是8个字节,而int类型是4个字节。因此,bar5.c的第6行中的赋值x=-0.0将用负零的双精度浮点表示覆盖存储器中的x和y的位置。
linux>gcc -o foobar5 foo5.c bar5.c
linux>./foobar5
x = 0x0 y = 0x8000000
这是一个非常细微而令人讨厌的错误,尤其是因为它是默默发生的,编译系统不会给出警告,而且因为通常要在程序执行很久以后才表现出来,且远离错误的发生地。当你怀疑由此错误时,用像gcc -fno-common这样的选项调用链接器,在遇到多重定义的全局符号时,输出一条警告。