C语言中的强符号和弱符号介绍
之前在extern“C”用法详解中已经提到过符号的概念,它是编译器对变量和函数的一种标记,编译器对C和C++代码在生产符号时规则也是不一样的,符号除了本身名字的区别外,还有强符号和弱符号之分
我们先看一段简单的代码
/*test.c*/ voidhello(); intmain() { hello(); return0; }
很显然,这段代码是没法链接通过的,它会报错undefinedreferencetohello,说的是hello未定义,因为这里我们只声明了函数hello,而没有定义它。但是我们把代码稍作修改如下
__attribute__((weak))voidhello(); intmain() { hello(); return0; }
这时你会发现,编译链接都可通过,但是运行会报错,因为这时我们将hello声明为了弱符号,在链接时弱符号会被链接器当做0,执行一个地址为0的函数当然会报错,改为如下代码就不会报错了,只是它没有任何输出
__attribute__((weak))voidhello(); intmain() { if(hello) hello(); return0; }
编译器认为,函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号,链接器在处理强符号和弱符号时有如下规则
1.不同目标文件中,不允许有同名的强符号
2.如果一个符号在某个目标文件中是强符号,在其它目标文件中为弱符号,选择强符号
3.如果一个符号在所有目标文件中都是弱符号,选择占用空间最大的,比如目标文件A中有doubleglobal_var,文件B中有intglobal_var,double占用8字节,大于int的4字节,A和B链接后,符号global占8字节
对此我们可以简单的验证一下,有如下两个文件
/*1.c*/ charglobal_var; intmain() { return0; } /*2.c*/ intglobal_var;
全局变量global_var在两个文件中都没有初始化,因此都是弱符号,执行编译命令gcc1.c2.c,用readelf查看符号表readelf-sa.out,为了查看方便我们只输出最后几行
Num: Value SizeType Bind Vis NdxName 62:0000000000600818 4OBJECT GLOBALDEFAULT 25global_var 63:0000000000400474 11FUNC GLOBALDEFAULT 13main 64:0000000000400358 0FUNC GLOBALDEFAULT 11_init
这里符号global_var占用的size是4,说明链接器选择的是占用空间更大的intglobal_var,我们再稍作修改,将1.c中的全局变量初始化,如下
/*1.c*/ charglobal_var=1; intmain() { return0; } /*2.c*/ intglobal_var;
这时1.c中的global_var为强符号,2.c中的global_var为弱符号,同样编译之后用readelf查看符号表readelf-sa.out如下
Num: Value SizeType Bind Vis NdxName 62:0000000000600818 1OBJECT GLOBALDEFAULT 25global_var 63:0000000000400474 11FUNC GLOBALDEFAULT 13main 64:0000000000400358 0FUNC GLOBALDEFAULT 11_init
此时符号global_var占用的size是1,说明链接器选择的是强符号
在写代码时应该尽量避免有不同类型的符号,否则会引发非常诡异且不易察觉的错误,为了避免可以采取如下措施:
1.上策:消除所有的全局变量
2.中策:将全局变量声明为static类型,并提供接口供访问
3.下策:全局变量一定要初始化,哪怕初始化为0
4.必备:打开gcc的-fno-common选项,它会禁止有不同类型的符号
说了这么多,好像在说应该尽量用强符号,那弱符号有什么用呢,所谓存在即合理,有时候我们甚至需要显示定义弱符号,这对库函数会非常有用,比如库中的弱符号可以被用户自定义的强符号覆盖,从而实现自定义的库版本,或者在使用某些扩展功能时,用户可以定义一个弱符号,当链接了该功能时,功能模块可以正常使用,如果去掉功能模块,程序也可正常链接,只是缺少某些功能而已,比如我们可以通过下面的代码判断程序是否链接了pthread库,从而决定执行什么样的操作
/*test.c*/ #include<stdio.h> #include<pthread.h> __attribute__((weak))intpthread_create( pthread_t*, constpthread_attr_t*, void*(*)(void*), void*); intmain() { if(pthread_create) { printf("Thisismulti-threadversion!\n"); } else { printf("Thisissingle-threadversion!\n"); } return0; }
编译运行结果如下
$gcctest.c $./a.out Thisissingle-threadversion! $gcctest.c-lpthread $a.out Thisismulti-threadversion!