链接器解析符号的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的
符号定义联系起来。对那些和引用定义在相同模块中的本地符号引用,符号解析很直接。编译
器只允许每个模块中每个本地符号只有一个定义。
不过对于全局符号的引用就稍微麻烦一些,当编译器遇到一个不是在当前模块中定义的符号
(变量或者函数名)时,它会假设该符号是在其他某个模块中定义的,生成一个链接器符号表
条目,并把它交给链接器处理。如果链接器在它的任何输入模块 中找不到这个被引用的符号,
它就输出一条错误信息并且终止。
在编译时,编译器向汇编器输出每个全局符号,或者是强(strong)或者是弱(weak),而汇编器
把这个信息隐含地编码在可重定位目标文件的符号表中。函数和已初始化的全局变量是强符号,
未初始化的全局变量是若符号。
根据强弱符号的定义,Unix链接器使用下面的规则来处理多重定义的符号:
规则1:不允许又多个强符号
规则2:如果有一个强符号和多个弱符号,那么选择强符号。
规则3:如果有多个弱符号,那么从弱符号中任意选择一个。
比如,如果试图编译和链接下面两个C模块:
//foo1.c
int main() {
return 0;
}
//bar1.c
int main() {
return 0;
}
链接器将会产生如图3.5.5.1所示的错误:
***图3.5.5.1***
相似的,链接器对于下面的模块也会生成一条错误信息,因为强符号x被定义了两次(规则1):
//foo2.c
int x = 15213;
int main() {
return 0;
}
//bar2.c
int x = 15213;
void f() {
}
链接器将会产生如图3.5.5.2所示的错误:
***图3.5.5.2***
然而,如果在一个模块里x未被初始化,那么链接器将安静地选择定义在另外一个模块中的强
符号(规则2):
//foo3.c
#include <stdio.h>
void f();
int x = 15312;
int main() {
f();
printf("x = %d
", x);
return 0;
}
//bar3.c
int x;
void f() {
x = 15313;
}
链接器将会产生如下结果:
***图3.5.5.3***
如果符号在两个或者多个模块中出现,并且是弱符号,分为如下两种情况:
1.这两个弱符号的大小相同
//foo4.c
#include <stdio.h>
int x;
int main() {
x = 15312;
f();
printf("x = %d
", x);
return 0;
}
//bar4.c
int x;
void f() {
x = 15313;
}
链接器将会产生如下效果:
***图3.5.5.4***
2.这两个弱符号的大小不相同
//foo5.c
#include <stdio.h>
int x;
int main() {
x = 15312;
f();
printf("x = %d
", x);
return 0;
}
//bar5.c
double x;
void f() {
x = 15313;
}
链接器将会产生如下效果:
***图3.5.5.5***
弱引用与强引用
目前,我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,
它们需要被正确的决议,如果没有找到该符号的决议,链接器就会报符号未定义错误,
这种被成为强引用(Strong Reference)。与之对应的还有一种称为弱引用(Weak Reference),
在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,
则链接器对于该引用不报错。一般对于未定义的弱引用,链接器默认其值为0,或者是一个特殊
的值。
在GCC中,我们可以通过使用"__attribute__((weakref))"这个扩展关键字来声明对一个外部
函数的引用为弱引用,比如又如下代码:
//test.c
__attribute__((weakref)) void foo();
int main() {
foo();
return 0;
}
弱引用和弱符号对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,
从而使得程序使用自定义版本的库函数;或者程序可以对某些扩展模块的引用定义为弱引用,
当我们将扩展模块与程序链接在一起的时候,功能模块就能正常使用。如果我们去掉某些功能
模块,那么程序也可以正常链接,只是缺少相应的功能。