C语⾔中的强符号和弱符号介绍
之前在extern “C” ⽤法详解中已经提到过符号的概念,它是编译器对变量和函数的⼀种标记,编译器对C和C++代码在⽣产符号时规则也是不⼀样的,符号除了本⾝名字的区别外,还有强符号和弱符号之分
我们先看⼀段简单的代码
复制代码代码如下:
/* test.c */
void hello();
int main()
{
hello();
return 0;
}
很显然,这段代码是没法链接通过的,它会报错undefined reference to hello,说的是hello未定义,因为这⾥我们只声明了函数hello,⽽没有定义它。但是我们把代码稍作修改如下
复制代码代码如下:
__attribute__((weak)) void hello();
int main()
{
hello();
return 0;
}
这时你会发现,编译链接都可通过,但是运⾏会报错,因为这时我们将hello声明为了弱符号,在链接时弱符号会被链接器当做0,执⾏⼀个地址为0的函数当然会报错,改为如下代码就不会报错了,只是它没有任何输出
复制代码代码如下:
__attribute__((weak)) void hello();
int main()
{
if(hello)
hello();
return 0;
}
编译器认为,函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号,链接器在处理强符号和弱符号时有如下规则
1.不同⽬标⽂件中,不允许有同名的强符号
2.如果⼀个符号在某个⽬标⽂件中是强符号,在其它⽬标⽂件中为弱符号,选择强符号段拼音
3.如果⼀个符号在所有⽬标⽂件中都是弱符号,选择占⽤空间最⼤的,⽐如⽬标⽂件A中有double global_var,⽂件B中有int global_var,double占⽤8字节,⼤于int的4字节,A和B链接后,符号global占8字节
对此我们可以简单的验证⼀下,有如下两个⽂件
复制代码代码如下:
/* 1.c */
char global_var;
int main()
{
return 0;
}
/* 2.c */
int global_var;
全局变量global_var在两个⽂件中都没有初始化,因此都是弱符号,执⾏编译命令gcc 1.c 2.c,⽤readelf查看符号表readelf -s a.out,为了查看⽅便我们只输出最后⼏⾏
复制代码代码如下:
Num:    Value          Size Type    Bind  Vis      Ndx Name
62: 0000000000600818    4 OBJECT  GLOBAL DEFAULT  25 global_var
63: 0000000000400474    11 FUNC    GLOBAL DEFAULT  13 main
64: 0000000000400358    0 FUNC    GLOBAL DEFAULT  11 _init
这⾥符号global_var占⽤的size是4,说明链接器选择的是占⽤空间更⼤的int global_var,我们再稍作修改,将1.c中的全局变量初始化,如下
复制代码代码如下:
/* 1.c */
char global_var = 1;
int main()
{
return 0;
}
/* 2.c */
int global_var;
这时1.c中的global_var为强符号,2.c中的global_var为弱符号,同样编译之后⽤readelf查看符号表readelf -s a.out如下
复制代码代码如下:
Num:    Value          Size Type    Bind  Vis      Ndx Name
62: 0000000000600818    1 OBJECT  GLOBAL DEFAULT  25 global_var中原五黄
63: 0000000000400474    11 FUNC    GLOBAL DEFAULT  13 main
64: 0000000000400358    0 FUNC    GLOBAL DEFAULT  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)) int pthread_create(
pthread_t*,
const pthread_attr_t*,
void*(*)(void*),
void*);
int main()
{
if (pthread_create)
{
printf("This is multi-thread version!\n");      }害羞的小姑娘
else
{
printf("This is single-thread version!\n");      }
return 0;
}
编译运⾏结果如下
复制代码代码如下:
$ gcc test.c
陈楚生好听的歌$ ./a.out
This is single-thread version!
收件箱
$ gcc test.c -lpthread
$ a.out大王具足虫
This is multi-thread version!