C语言强符号和弱符号


作者:littlewhite

之前在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,说明链接器选择的是强符号

在写代码时应该尽量避免有不同类型的符号,否则会引发非常诡异且不易察觉的错误,为了避免可以采取如下措施:

  • 上策:消除所有的全局变量
  • 中策:将全局变量声明为static类型,并提供接口供访问
  • 下策:全局变量一定要初始化,哪怕初始化为0
  • 必备:打开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!

EOF


本文参考:

  1. 《程序员的自我修养》3.5.5章节
  2. http://blog.csdn.net/astrotycoon/article/details/8008629

发表评论

电子邮件地址不会被公开。 必填项已用*标注