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;
- 养成良好的习惯,所有的全局变量都要初始化(变成强符号);
- 尽量不使用全局变量。