C语言强符号和弱符号

1、强符号、弱符号定义

        编译器在编译源程序时,无论你是变量名、函数名,在它眼里,都是一个符号而已,用来表征一个地址。编译器会将这些符号集中,存放到一个叫符号表的 section 中。

        那么对于两个.c文件中存在的同名的变量,编译器该怎么选择呢?这个时候,就需要引入强符号和弱符号的概念了

强符号:函数名、初始化的全局变量名;
弱符号:未初始化的全局变量名。

       两种符号的不同组合可能有不同的效果:
强符号 + 强符号 : 编译时会提示“重复定义的错误”
强符号 + 弱符号 : 选择强符号
弱符号 + 弱符号 : 默认的,链接器使用第一个找到的符号
。(ps: 有些说是链接器选择占用内存空间最大的那个,个人验证并不是这样)

下面的实验环境为:
gcc version 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC)
 

2、变量的强符号和弱符号

        先通过变量的强符号和弱符号来研究上面的几个组合的特性。

//func.c
int a = 1;

//main.c
#include <stdio.h>

int a = 10;

int main()
{
    printf("a = %d.
", a);
    return 0;
}

编译时提示:

/tmp/ccJyARMQ.o:(.data+0x0): multiple definition of `a'
/tmp/cccYwric.o:(.data+0x0): first defined here
//func.c
int a = 1;
int b;
int c __attribute__((weak)) = 3;
int d __attribute__((weak)) = 'z';

void func()
{
    printf("func: a = %d, b = %d, c = %d, d = %d.
", a, b, c, d);
}

//main.c
#include <stdio.h>

int a;
int b = 9;
int c = 8;
char d __attribute__((weak)) = 'a';

int main()
{
    printf("main: a = %d, b = %d, c = %d, d = %d.
", a, b, c, d);
    func(a, b, c);
    return 0;
}

gcc -o main main.c func.c输出结果为:

main: a = 1, b = 9, c = 8, d = 97.
func: a = 1, b = 9, c = 8, d = 97.

gcc -o main func.c main.c 输出结果为:

main: a = 1, b = 9, c = 8, d = 122.
func: a = 1, b = 9, c = 8, d = 122.

a,b变量看出,强符号 + 弱符号最后会选择强符号。
c变量说面可以使用__attribute__((weak))声明变量为弱符号。
d变量说明了都是弱符号的情况下,链接器使用第一个找到的符号
再次我们验证第三个特性,在增加一个文件test.c文件

//test.c
char d char d __attribute__((weak)) = 'h';

先编译不进行链接gcc -c test.c func.c main.c,会分别生成三个.o文件,调整顺序:

$gcc -o main test.o main.o func.o
$ ./main
main: a = 1, b = 9, c = 8, d = 104.
func: a = 1, b = 9, c = 8, d = 104.
$ gcc -o main main.o func.o test.o
$ ./main
main: a = 1, b = 9, c = 8, d = 97.
func: a = 1, b = 9, c = 8, d = 97.
$ gcc -o main func.o test.o main.o
$ ./main
main: a = 1, b = 9, c = 8, d = 122.
func: a = 1, b = 9, c = 8, d = 122.

符合规则,要是这里我们修改test.c文件里面为强符号会怎么样呢?

//test.c
char d = 'h';
得到结果与期望相同,均是选择强符号。
$ gcc -o main test.o main.o func.o // gcc -o main main.o func.o test.o或者gcc -o main func.o test.o main.o
$ ./main
main: a = 1, b = 9, c = 8, d = 104.
func: a = 1, b = 9, c = 8, d = 104.
$ gcc -o main main.o func.o test.o

3、函数的强符号和弱符号

// main.c
#include <stdio.h>

void func()
{
    printf("function from %s.
", __FILE__);
}

int main()
{
    func();
    return 0;
}

// func.c
#include <stdio.h>

void func()
{
    printf("function from %s.
", __FILE__);
}

编译报错,重复定义

/tmp/ccv2g1Fo.o: In function `func':
func.c:(.text+0x0): multiple definition of `func'
/tmp/ccQXcJwI.o:main.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

修改func.c为

//func.c
#include <stdio.h>

void __attribute__((weak)) func()
{
    printf("function from %s.
", __FILE__);
}

执行结果如下:

$ gcc -o main  main.c func.c
$ ./main
function from main.c.
$ gcc -o main  func.c main.c
$ ./main
function from main.c.
$ gcc -c main.c func.c
$ gcc -o main main.o func.o
$ ./main
function from main.c.
$ gcc -o main func.o main.o
$ ./main
function from main.c.

目前上面发生的都在预料之中,现在我们在看看下面的例子:

// main.c
#include <stdio.h>
int main()
{
    func();
}
//func.c
#include <stdio.h>

int d __attribute__((weak)) = 'z';

void __attribute__((weak)) func()
{
    printf("func: d = %d, %s.
", d, __FILE__);
}

//test.c
#include <stdio.h>

char d = 'h';

void func()
{
    printf("test: d = %d, %s.
", d, __FILE__);
}

执行结果如下:

$ gcc -c main.c func.c test.c
$ ar qs libf.a func.o
ar: creating libf.a
$ ar qs libt.a test.o
ar: creating libt.a
$ gcc -o main main.o -L. -lf -lt
$ ./main
func: d = 122, func.c.
$ gcc -o main main.o -L. -lt -lf
$ ./main
test: d = 104, test.c.

看,这里好像就不符合前面的规则所说的了,原因就是GCC(准确地说是链接器)对待库是不一样的 —— 默认的,链接器使用第一个找到的符号,后面的就不搜索了

4、弱符号的用途

问题:我们不确定外部模块是否提供一个函数func,但是我们不得不用这个函数,即自己模块的代码必须用到func函数

extern int func(void);
...................
int a = func();

我们不知道func函数是否被定义了;
这会导致2个结果:

  • 外部存在这个函数func,并且EXPORT_SYMBOL(func),那么在我自己的模块使用这个函数func,正确。
  • 外部其实不存在这个函数,那么我们使用func,程序直接崩溃。

        所以这个时候,attribute((weak)) 派上了用场。我们可以在自己的模块定义一个弱符号函数,当函数被声明为一个弱符号时,会有一个奇特的地方:当链接器找不到这个函数的定义时,也不会报错。编译器会将这个函数名,即弱符号,设置为0或一个特殊的值。只有当程序运行时,调用到这个函数,跳转到0地址或一个特殊的地址才会报错
 

int  __attribute__((weak))  func(......)
{
    ..........
}

...................
if(func)
{
    int a = func();
}

        将本模块的func转成弱符号类型,如果遇到强符号类型(即外部模块定义了func),那么我们在本模块执行的func将会是外部模块定义的func。

        如果外部模块没有定义,那么,将会调用这个弱符号,也就是在本地定义的func,直接返回了一个1(返回值视具体情况而定)

        相当于增加了一个默认函数。

        原理:连接器发现同时存在弱符号和强符号,有限选择强符号,如果发现不存在强符号,只存在弱符号,则选择弱符号。如果都不存在:静态链接,恭喜,编译时报错,动态链接:对不起,系统无法启动。

        weak属性只会在静态库(.o .a )中生效,动态库(.so)中不会生效。
 

5、弱符号带来的问题

// test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

double a;

void print_char(char *v, int len)
{
    int i = 0;
    for(i = 0; i < len; i++)
        printf("%x", v[i]);
    printf("
");
}

void func_test()
{
    printf("int test.c: &a = %p, a = %lf, sizeof(a) = %d
", &a, a, sizeof(a));
    print_char((char*)&a, sizeof(a));
    memset(&a, 0, sizeof(a));
    return;
}

// main.c
#include <stdio.h>

int a = 0xFF00FF00;
int b = 0x00FF00FF;

void func_main()
{
    printf("int main.c: &a = %p, a = %x, sizeof(a) = %d
", &a, a, sizeof(a));
    printf("int main.c: &b = %p, b = %x, sizeof(b) = %d
", &b, b, sizeof(b));

}

int main()
{
    func_main();
    func_test();
    func_main();
    return 0;
}

执行结果如下:

$ gcc -o main main.c test.c
/bin/ld: Warning: alignment 4 of symbol `a' in /tmp/ccYA9vPA.o is smaller than 8 in /tmp/ccFEeiTw.o
$ ./main
int main.c: &a = 0x601044, a = ff00ff00, sizeof(a) = 4
int main.c: &b = 0x601048, b = ff00ff, sizeof(b) = 4
int test.c: &a = 0x601044, a = 0.000000, sizeof(a) = 8
0ffffffff0ffffffffffffffff0ffffffff0
int main.c: &a = 0x601044, a = 0, sizeof(a) = 4
int main.c: &b = 0x601048, b = 0, sizeof(b) = 4

        我们可以看到,在main.c和test.c都有一个变量a,在main.c中的为强符号,在test.c中的为弱符号。因为在test.c中a没有初始化,所以根据规则②得知:编译器选择main.c中的a的值初始化那片内存。不要误认为在test.c中使用global_var1时是用的main.c中的global_var1,我之前错误得这样认为。其实是这样的:main.c中的global_var1和test.c中的global_var1引用的时同一块内存区域,只是在两个文件中代表的意义不同 ---- 在main.c中代表一个int型变量,在test.c中代表一个double型变量,它们的起始地址相同,但占用内存空间是不同的, 在main.c中占用4个字节,在test.c中占用8个字节,这点从上图的两个sizeof输出结果中可以得到验证

解决这种问题的方法:

  • GCC编译时增加选项-fno-common;
  • 养成良好的习惯,所有的全局变量都要初始化(变成强符号);
  • 尽量不使用全局变量。