菜鸟在C语言编译,链接时可能遇到的两个问题
最近在看 CSAPP (Computer Systems A Programmers Perspective 2nd) 的第七章 链接。学到了点东西,跟大家分享。下文中的例子都是出自CSAPP第七章。
另外,也可以结合酷壳上的这篇文章和之后的留言来看本文,理解会更加深刻一些。
1.问:如果在不同的C源文件中定义了相同名称的全局变量会有什么样的后果呢?
比如下面的这种情况:
有两个源文件foo3.c和bar3.c:
foo3.c
#include <stdio.h>
void f(void); int x = ; int main()
{
f();
printf("x = %d\n", x);
return ;
}
bar3.c
int x; void f()
{
x = ;
}
要解答这个问题,就得知道链接的时候,链接器(Linker)是如何解析多处定义的全局符号的:[ CSAPP 7.6.1]
在编译时,编译器输出每个全局符号(global symbols)给汇编器,这些符号要么是强(strong symbol)符号,要么是弱(weak symbol)符号。
函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。 根据强弱符号的定义,Unix链接器使用下面的规则来处理多处定义的符号:
规则1:不允许有多个强符号【否则,链接的时候会出错:multiple definition of 'xx'】。
规则2:如果有一个强符号和多个弱符号,那么选择强符号
规则3:如果有多个弱符号,那么从这些弱符号中任意选一个
所以根据规则2,bar3.c中的x是弱引用,在链接时,编译器会悄悄地会认为bar3.c处引用的是foo3.c中定义的x,所以在main调用f()之后,x = 15212;
小伙伴们如果看明白了,可以想想在IA32/Linux机器上,下面这段代码的输出是什么?
foo5.c
#include <stdio.h>
void f(void); int x = ;
int y = ; int main()
{
f();
printf("x = 0x%x y = 0x%x \n", x, y);
}
bar5.c
double x;
void f()
{
x = -0.0
}
对于上述C语言中多处定义的全局符号问题,有没有什么解决方案呢?
个人认为:
1.尽量减少全局变量的使用(有好多坑),凡是别的文件里用不着的全局变量,都加上static来限定其作用域都在本模块内,这样这个变量也就不是全局符号了,是本地符号。
2.可以在gcc的命令行里加上 -fno-common 参数,当遇到多重定义的全局变量时,会报错
2.在编译源代码的时候,是否会遇到链接器死活就是提示错误: undefined reference to 'foo'。仔细确认了好几遍,发现包含了这个函数需要的库的名称,也指定了库的路径,也通过命令:nm -s libfoo.a 看到libfoo静态库里是有foo这个符号的,但链接阶段就是会报错。【我当时遇到这个问题时,百思不得其解,快要跳脚骂娘了。后来还是找老鸟同事搞定的。】
上述这种情况,很有可能是命令行里,指定编译所需的静态链接库时,链接库的顺序有问题。举个例子:
foo.c 调用了libx.a和libz.a中的函数,而这两个库又需要调用liby.a中的函数,那么在,命令行中,libx.a和libz.a必须在liby.a之前,否则就会出现找不到符号定义的情况。为什么呢?请看解释[CSAPP 7.6.3]:
在符号解析的阶段,链接器从左到右按照文件在编译器驱动程序命令行上出现的相同顺序来扫描可重定位目标文件和存档文件。(驱动程序自动将命令行中所有的.c文件翻译为.o文件)在这次扫描中,链接器维持一个可重定位目标文件的集合
E,这个集合中的文件会被合并起来形成可执行文件,和一个未解析的符号(也就是,引用了但是尚未定义的符号)集合U,以及一个在前面输入文件中己定义的符号集合D。初始地,E, U和D
都是空的。
对于命令行上的每个输入文件f,链接器会判断f是一个目标文件(object file)还是一个存档文件(archive)。如果f是一个目标文件,那么链接器把f添加到E,修改U和D来反映f中的符号定义和引用,并继续下一个输入文件。如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D来反映m中的符号定义和引用。对存档文件中所有的成员目标文件都反复进行这个过程,直到U和D都不再发生变化。在此时,任何不包含在E 中的成员目标文件都被丢弃,而链接器将继续到下一个输入文件。
如果当链接器完成对命令行上输入文件的扫描后,U是非空的,那么链接器就会输出一个错误并终止。否则,它会合并和重定位E中的目标文件,从而构建输出的可执行文件。
上面这一大段挺绕的,简单来说,就一句话:如果在命令行中,定义一个符号的库出现在引用这个函数/变量的目标文件之前,那么引用就不能被解析,链接会失败。就像上面的例子。
update: 2013/10/20
另外,如果静态库之间不是相互独立的,也有相互引用,那么必须得正确安排好顺序。比如,foo.c调用libx.a和libz.a中的函数,而这两个库又调用 liby.a中的函数。那么命令行中libx.a和libz.a必须在liby.a之前:
gcc foo.c libx.a libz.a liby.a
如果需要满足依赖的需求,可以在命令行上重复库。比如,foo.c 调用 libx.a,该库又调用 liby.a,而 liby.a 又调用libx.a。那么libx.a 必须在命令行上重复出现:
gcc foo.c libx.a liby.a libx.a
如果还有不太明白的地方,可以去看看CSAPP的第七章。
强烈推荐大家有时间读读CSAPP这本书啊,收获会很大的。附上 老赵书托(3):深入理解计算机系统
如果您看了本篇博客,觉得对您有所收获,请点击右下角的“推荐”,让更多人看到!
菜鸟在C语言编译,链接时可能遇到的两个问题的更多相关文章
- VS编译链接时错误(Error Link2005)的解决方法
近期参与的项目中使用了公司另外一个同事提供的一个静态库文件.该静态库文件集成了CUDA, OpenCL两个库,用于做图形加速计算,提高视频解码拼接速度.但是在编译链接项目时,VS爆出如下错误: 1&g ...
- C语言编译链接
转载请标明: 编译链接是使用高级语言编程所必须的操作,一个源程序只有经过编译.链接操作以后才可以变成计算机可以理解并执行的二进制可执行文件. 编译是指根据用户写的源程序代码,经过词法和语法分析,将高级 ...
- opensips编译安装时可能遇到的问题
错误一: ERROR: could not load the script in /usr/local//lib64/opensips/opensipsctl/opensipsdbctl.pgsql ...
- 利用gcc编译链接时出现 ‘undefined reference to `std::ios_base::Init::Init()’ 解决
一般编译链接c++程序最好使用g++,若有如上的报错信息,需要在gcc后加上 -lstdc++ eg: gcc test.c -lstdc++ gcc和g++都是GNU的一个编译器. g++:后缀.c ...
- ubuntu 下 数学库编译链接时找不到各种数学问题解决方法 can not fon atan 等等
解决参考 http://askubuntu.com/questions/190246/ld-cannot-find-math-library you should use -lm at the end ...
- c语言编译预处理和条件编译执行过程的理解
在C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令.预处理命令属于C语言编译器,而不是C语言的组成部分.通过预处理命令可扩展C语言程序设计的环境. 一.预处理的工作方式 1.1. ...
- linux下 GCC编译链接静态库&动态库
静态库 有时候需要把一组代码编译成一个库,这个库在很多项目中都要用到,例如libc就是这样一个库, 我们在不同的程序中都会用到libc中的库函数(例如printf),也会用到libc中的变量(例如以后 ...
- GCC链接时库顺序问题
GCC或G++在编译链接时,如果命令行中含有库,则要特别注意了.根据<C专家编程>5.3节中的提示,GCC在链接时对命令行时的处理顺序是从左到右.证据是GCC的MAN: -l librar ...
- C语言编译和链接过程
1.程序的编译 一般而言,大多数编译系统都提供编译驱动程序(complier driver),根据用户需求调用语言预处理器,编译器,汇编器和链接器.例如有如下历程://main.c void swa ...
随机推荐
- Js 变量声明提升和函数声明提升
Js代码分为两个阶段:编译阶段和执行阶段 Js代码的编译阶段会找到所有的声明,并用合适的作用域将它们关联起来,这是词法作用域的核心内容 包括变量声明(var a)和函数声明(function a(){ ...
- React使用antd Table生成层级多选组件
一.需求 用户对不同的应用需要有不同的权限,用户一般和角色关联在一起,新建角色的时候会选择该角色对应的应用,然后对应用分配权限.于是写了一种实现的方式.首先应用是一个二级树,一级表示的是应用分组,二级 ...
- Android权限管理之RxPermission解决Android 6.0 适配问题
前言: 上篇重点学习了Android 6.0的运行时权限,今天还是围绕着Android 6.0权限适配来总结学习,这里主要介绍一下我们公司解决Android 6.0权限适配的方案:RxJava+RxP ...
- JS判断鼠标进入容器方向的方法和分析window.open新窗口被拦截的问题
1.鼠标进入容器方向的判定 判断鼠标从哪个方向进入元素容器是一个经常碰到的问题,如何来判断呢?首先想到的是:获取鼠标的位置,然后经过一大堆的if..else逻辑来确定.这样的做法比较繁琐,下面介绍两种 ...
- DDD 领域驱动设计-商品建模之路
最近在做电商业务中,有关商品业务改版的一些东西,后端的架构设计采用现在很流行的微服务,有关微服务的简单概念: 微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成.系统中的各个微服务可被独 ...
- [原] KVM 虚拟化原理探究(5)— 网络IO虚拟化
KVM 虚拟化原理探究(5)- 网络IO虚拟化 标签(空格分隔): KVM IO 虚拟化简介 前面的文章介绍了KVM的启动过程,CPU虚拟化,内存虚拟化原理.作为一个完整的风诺依曼计算机系统,必然有输 ...
- Exception in thread "main" java.lang.NoSuchMethodError: org.objectweb.asm.ClassWriter.<init>(I)V
在学习CGlib动态代理时,遇到如下错误: Exception in thread "main" java.lang.NoSuchMethodError: org.objectwe ...
- JavaScript中String对象的方法介绍
1.字符方法 1.1 charAt() 方法,返回字符串中指定位置的字符. var question = "Do you like JavaScript?"; alert(ques ...
- Atiti.大企业病与小企业病 大公司病与小公司病
Atiti.大企业病与小企业病 大公司病与小公司病 1. 大企业病,一般会符合机构臃肿 .多重领导 .人才流失的特点.1 2. 大企业病避免方法1 3. 小企业病 1 3.1.1. 表现1 4. 如何 ...
- Linux课堂笔记(一)
一.Linux应用领域及版本介绍. 1.服务器.嵌入式.桌面应用等. (1)在服务器领域中,需要安全和稳定,特别是越老的内核版本越安全.越稳定. (2)Linux主要分内核版和发行版. 内核版本2.6 ...