1.链接器在符号解析时,是如何解决全局符号的重名问题的
前面说过,对于模块内部符号重名的情况,在编译时cc1编译器会检查出来,对于不同模块之间的全局符号重名情况,只能由链接器解析时来检查。
(1)模块之间全局符号重名的情况是无法避免的
为了方便工程的组织管理,程序肯定是某块化组织管理的,此时模块之间必然会涉及全局变量和函数的声明,
比如:
a.c
int fun(); int a; int main(void) { a = a + 100; fun(); }
b.c
int a = 100; int fun() { ... }
a.c中为了使用b.c中定义的a和fun,需要在a.c中进行声明,声明的符号与定义的符号是同名的,链接器将这两个文件链接为一个可执行文件,同名的符号就必须统一(合并)为一个,所以链接器进行符号解析时,重名符号的统一是必不可少的。
(2)链接器解析符号时,是如何解决全局符号重名的情况
链接器根据符号的强弱共存规则来解析。
1)全局符号的强弱之分
1)全局变量
(a)强符号:初始化了的全局变量
(b)弱符号:未初始化的全局变量
2)函数
(a)强符号:函数定义
(b)弱符号:函数声明
每个全局符号是强符号还是弱符号,在.symtab表中会有相应的记录。
2)重名符号的强弱共存规则
1)不允许多个同名的强符号同时存在,存在的话就报错
2)强符号只有一个,其它同名的都是弱符号的话,统一时,选强符号,舍弃弱符号
3)同名符号如果全都是弱符号的话,留其中某个,其它舍弃,留谁由链接器来决定
3)举例
(a)例子1
a.c
int a = 100; int fun() { a += 10; } int main(void) { static int a = 10; fun(); }
b.c
int a = 200; (extern) int fun() { a = a - 100; }
由于文件中定义的全局变量a和函数fun都是全局的强符号(extern修饰的),在各自的.symtab符号表中,a和fun都会被标记为全局强符号。
链接器进行符号解析时,会去检测这两个模块的.symtab表中的符号,发现这各自的a、fun都是全局强符号,根据强弱符号的共存规则,一山不容二虎,存在多个同名全局强符号时会报错。
如何解决:
· 将其中的某个强符号变为弱符号
int a = 100; ————> int a; int fun(){...} ————> int fun();
留强去弱。
· 使用static来解决
只要将其中的某个或者两个使用static修饰,将其变为本地符号,就不会存在冲突的情况。
int a = 100; ————> static int a = 200; int fun(){...} ————> static int fun();
旁注:全局变量和函数加static,目的是将全局符号变为本地符号。
局部变量加static,是将局部变量的从自动局部变量(栈)变为静态局部变量(.data/.bss)。
(b)例子2
a.c
int a; int main(void) { fun(); }
b.c
int a;
两个模块的a都是弱符号,统一符号时,留下其中一个即可。
(3)cc1编译器处理模块内符号重名的情况
链接器处理模块间的全局符号的重名时,是按照强弱符号的规则来处理的。
对于模块内部的符号重名问题,编译器cc1在编译时,其实也是按照强弱符号的规则来处理的。
1)例子1:
a.c
int a = 100; int a = 300; int fun() { } int fun() { } int main(void) { }
编译时会报重复定义的错误(强符号只能有一个)。
2)例子2:
a.c
int a; int a; int main(void) { } int a = 10;
没有错误,因为不存在强符号的冲突问题,编译时会统一为一个,.symtab只会记录一个a。
在经典的c讲解中,这种情况将int a解释成“int a = 100;这个定义”的声明,实际上本质关系是强弱符号关系。
只不过在单个模块中,弱符号(声明)可以将强符号(定义)的作用域提前。
3)例子3
a.c
static int a = 20; int a = 10; int main(void) { }
大家觉得会报错吗?
答:会报错,虽然一个是全局的,一个是本地的,但是由于都在一个模块中,因此编译时肯定报错。
但是如果它们两个是在不同的模块中的的话,链接器链接时就不会报错,这个前面讲过。
a.c
int a = 10; int main(void) { }
b.c
static int a = 20;
(4)再说说声明和定义
事实上,对于编译器、链接器来说,并不知道什么是声明、什么是定义,声明和定义只是学习C语言时会了表述的方便,我们自己的给的,编译器/链接器只知道强符号和弱符号,解析时是按照强弱符号的规则来处理的。
我们以全局变量为例,你会发现其实不太好明确的区分定义和声明。
1)能够区分的例子
(a)例子1
a.c
int a; int a = 100; ...
针对这种情况来说,我么认为后面有初始化的a是定义,前面那个没初始化的a是后面a的声明。
(b)例子2
工程中有两个.c
a.c
int a; ...
b.c
int a = 0; ...
前面说过,没有明写出extern,默认就是extern修饰的。
有初始化的是定义,没有初始化的是声明。
在以上两个例子中,都能明确的区分出定义和声明。
2)不能区分的例子
a.c
int a; int a; int a; ....
在这种情况下,到底那个是定义,那个是声明呢,不好讲了,你或许会说都是定义,都是声明,这就没有什么明确区分了。
又比如:
a.c
int a;
只有一个int a时,到底应该说成是定义,还是说成声明呢?
又比如工程中有两个.c
a.c
int a;
b.c
int a;
你告诉我那个是声明,那个是定义,也是同样的情况。
所以说对于编译器/链接器来说,它不关心什么是定义、什么是声明,也没办法以定义和声明来区分,人家只关心强弱符号。
不过以后为了表述的方便,我们还是会继续使用“定义”和“声明”这两个概念,只不过在理解了强弱符号之后,大家心里应该清楚本质到底是什么。
以后不会再严格区分定义和声明,像int a;这种未初始化的情况,我们可能即会说成是定义,也可能会说成是声明,只要大家理解了强弱符号,说成什么其实都无所谓,只要方便表达和理解即可。