举例讲解C语言链接器的符号解析机制
1. 符号分类
(1)全局符号:非静态全局变量,非静态函数
(2)外部符号:定义于其它模块,而被本模块引用的全局变量和函数
(3)本地符号:静态变量(包括全局和局部),静态函数
对于静态局部变量,编译器会为其生成唯一的名字。如x.fun1,x.fun2。本地符号对链接器来说是不可见的。
2. 符号决议
当编译器遇到一个不是本模块定义的符号时,会假设该函数由其它模块定义,并生成一个链接器符号表条目,交由链接器处理。如果链接器在它的任何输入模块都没有找到该符号,会给出一个类似undefined reference to 'xxx'的链接错误。而如果链接器在输入模块中找到了一个以上的外部符号定义,这个时候就需要链接器进行符号决议,链接器对多个外部符号定义可能并不报错甚至警告,而是按照它的规则去选择其中一个符号定义。
链接器将各个模块输出的全局符号,分类为强符号和弱符号:
(1)强符号:函数和已初始化的全局变量
(2)弱符号:为初始化全局变量
根据强弱符号的定义,链接器按照下面的规则处理多重定义的符号:
规则1:不允许有多个强符号定义
规则2:如果有一个强符号和多个弱符号,那么选择强符号
规则3:如果有多个弱符号,那么从这些弱符号中选择sizeof大的那个,如果大小相同,则选择先链接的那个
上面的规则是很多链接错误的根源,因为编译器在决议时可能默默地替你作出了决定,你并不知晓。根据上面的规则,可以引出下面几个经典例子:
例1:
// in lib1.c int x; void f() { x = 1235; } // in main1.c #include<stdio.h> void f(void); int x = 1234; int main(void) { f(); printf("x=%d\n", x); return 0; }
上面的代码中,main函数printf输出: x=1235。因为链接器通过规则2决议符号x的定义为main.c中的强符号定义,而lib.c的作者并不知情,他对x的使用和修改影响到了main.c。这种交互修改,相互影响将会很复杂,因为大家都以为自己在做对的事情,在用对的变量。而整个决议过程,链接器悄无声息地完成了。
例2:
// in lib2.c double x; void f() { x = -0.0; } // in main2.c #include<stdio.h> void f(void); int x = 1234; int y = 1235; int main() { f(); printf("x=0x%x y=0x%x \n", x, y); return 0; }
这种情况下,程序得到输出: x=0x0 y=0x80000000,而链接器(gcc ld)也终于给出一条警告:
ld: warning: tentative definition of '_x' with size 8 from 'obj/Debug/lib2.o' is being replaced by real definition of smaller size 4 from 'obj/Debug/main2.o'
链接器决议的是符号地址,而相邻的全局变量可能在.data段中的内存地址也相邻,因此也就引发了更复杂的问题。这一点和栈溢出很像,但是比栈溢出更复杂,因为问题出在多个模块之间,而不是在一个函数内部。
例3:
// in lib3.c struct { int a; int b; } x; void f() { x.a = 123; x.b = 456; printf("in f(): sizeof(x)=%d, (&x)=0x%08x\n", sizeof(x), &x); } // in main3.c #include<stdio.h> void f(void); int x; int y; int main() { f(); printf("in main(): sizeof(x)=%d, (&x)=0x%08x, (&x)=0x%08x, x=%d,y=%d \n", sizeof(x), &x, &y, x, y); return 0; }
程序输出:
in f(): sizeof(x)=8, (&x)=0x02489018 in main(): sizeof(x)=4, (&x)=0x02489018, (&y)=0x02489020, x=123,y=0
始终记住,外部符号决议的是地址,因此无论lib3.c和main3.c中,符号x地址都是唯一的,无论其被定义了几次。其次sizeof是编译器决议,与链接无关,编译器只看得到本模块的定义或声明。最后,由于符号x决议到lib3.c中的x,其size是8,因此main3.c中的y的地址比x大8,这是由链接器将lib3.o和main3.o合并后填入可执行文件的.data段的。因此y是无关变量,被初始化为0,注意和例2的区别。
3. 总结
由于符号决议容易引发的种种问题,我们在写C的时候应注意:
尽量用static属性隐藏变量和函数在模块内的声明,就像在C++中尽量用private保护类私有成员一样。
少定义弱符号,尽量初始化全局变量,这样链接器会根据规则1给出多个符号定义的错误。
为链接器设置必要选项,如gcc的 -fno-common,这样在遇到多重符号定义时,链接器会给出警告。
4. C++的符号决议
C++并不支持强弱符号同时存在,所有符号都只能有一个定义(函数重载通过改写函数符号来确保其唯一),因此在很大程度上避免了C中的链接器困扰。
相关文章
- 这篇文章主要为大家详细介绍了C语言实现放烟花的程序,有音乐播放,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-02-23
- 本篇文章主要介绍C语言中char的知识,并附有代码实例,以便大家在学习的时候更好的理解,有需要的可以看一下...2020-04-25
- 这篇文章主要介绍了详解如何将c语言文件打包成exe可执行程序,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-25
- 这篇文章主要介绍了C# 中 “$” 符号的作用以及用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-06-25
- free函数是释放之前某一次malloc函数申请的空间,而且只是释放空间,并不改变指针的值。下面我们就来详细探讨下...2020-04-25
- 这篇文章主要介绍了C语言中计算正弦的相关函数总结,包括正弦和双曲线正弦以及反正弦的函数,需要的朋友可以参考下...2020-04-25
详解C语言中的rename()函数和remove()函数的使用方法
这篇文章主要介绍了详解C语言中的rename()函数和remove()函数的使用方法,是C语言入门学习中的基础知识,需要的朋友可以参考下...2020-04-25- 这篇文章主要介绍了C语言中求和、计算平均值、方差和标准差的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-12-10
- 本篇文章主要讲解C语言 基本语法,这里提供简单的示例和代码来详细讲解C语言的基本语法,开始学习C语言的朋友可以看一下,希望能够给你带来帮助...2021-09-18
- 这篇文章主要介绍了C语言中send()函数和sendto()函数的使用方法,是C语言入门学习中的基础知识,需要的朋友可以参考下...2020-04-25
- 今天小编就为大家分享一篇C语言实现从文件读入一个3*3数组,并计算每行的平均值,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-04-25
error LNK2019: 无法解析的外部符号 问题的解决办法
error LNK2019: 无法解析的外部符号 问题的解决办法,需要的朋友可以参考一下...2020-04-25- 这篇文章主要介绍了C语言中memcpy 函数的用法详解的相关资料,需要的朋友可以参考下...2020-04-25
- 这篇文章主要介绍了使用C语言操作文件的基本函数整理,包括创建和打开以及关闭文件的操作方法,需要的朋友可以参考下...2020-04-25
- 这篇文章主要介绍了C# byte转为有符号整数实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-11-11
- 这篇文章主要介绍了C语言中查找字符在字符串中出现的位置的方法,分别是strchr()函数和strrchr()函数的使用,需要的朋友可以参考下...2020-04-25
- 很多同学在学习c语言的时候是不是会碰到a++和++a都有甚么作用啊。今天我们就来探讨下...2020-04-25
- 这篇文章主要对C语言中const关键字的用法进行了详细的分析介绍,需要的朋友可以参考下...2020-04-25
- 下面小编就为大家带来一篇C语言实现时间戳转日期的算法(推荐)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-04-25
- 这篇文章主要介绍了Java中正则表达式split()特殊符号使用详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-07-21