Ubuntu版本:12.04
Linux版本:3.5.0
GCC版本:4.6.3
一、先说结论
在C语言中,强符号:
- 非静态函数
- 初始化的非静态全局变量(包括显示初始化为0)
弱符号:
- 未初始化的非静态全局变量
- GCC的
__attribute__((weak))
来定义任何一个强符号
强弱符号是针对于定义来说的,不是针对符号的引用。
__attribute__((weak))
修饰引用时,可以不用链接,依据符号在实际语句中的作用将其替换成0,编译时还是需要做语法正确性检查。
虽然允许多个相同弱符号同时存在,但是这里的存在只是在同一个项目中不同的文件中相同弱符号允许存在,同一个文件中不允许相同的弱符号同时存在。
======================================================================================================
下面对上述内容进行举例说明,如有理解错误,望不吝指正。
二、当attribute修饰符号的引用/声明时——不做链接
当 __attribute__((weak))
修饰强符号(定义好的)的时候,可以将强符号变成弱符号,当__attribute__((weak))
修饰引用/声明时,可以避过链接,依据符号在实际语句中的作用将其替换成0,编译时候语法正确性检查还是需要的。
什么是定义?
变量定义、函数定义认为是定义,例如:int a; void func(void){}
什么是引用/声明?
外部变量、函数原型认为是引用/声明,例如:extern int a; void func(void)
2.1、变量声明
阶段一、
/* test.c文件 */
#include <stdio.h>
int main(int argc, const char *argv[])
{
a++;
return 0;
}
编译:gcc test.c
(错误原因:‘a’ 未申报 )
阶段二、
/* test.c文件 */
#include <stdio.h>
extern int a; //外部变量——声明
int main(int argc, const char *argv[])
{
a++;
return 0;
}
编译:gcc test.c
(错误原因:对‘a’的未定义引用 )
阶段三、
/* test.c文件 */
#include <stdio.h>
extern int __attribute__((weak)) a; //外部变量——声明
int main(int argc, const char *argv[])
{
a++;
return 0;
}
编译:gcc test.c
编译结果:通过,不做链接。
运行结果:Segmentation fault (core dumped),因为链接器会将此未定义的weak symbol赋值为0,所以出现段错误。
2.2、函数声明
__attribute__((weak))
修饰函数声明,同样也是避过链接,将对应的符号设置成0,依然在编译时会做语法正确性检查。
gcc编译器中函数不用进行原型声明的解释
gcc编译器如果没有写函数原型,编译器会把函数调用同时认为是声明,默认返回值为int。
如果写了函数原型,那么以函数原型去检查函数调用是否符合规定。
变量如果是外部引用的也必须说明,不然编译报错。
安全起见还是写一下函数原型!!!
参考:关于gcc编译器中函数不用进行原型声明的解释 🚀
阶段一、
/* test1.c 文件 */
#include <stdio.h>
void __attribute__((weak)) func(void);
int main(int argc, const char *argv[])
{
if(func()) // call 0
func; //函数首地址,这里不做任何操作,编译器会忽略该语句
return 0;
}
编译:gcc test1.c
(错误原因:func函数返回值为空,无法作为 if 条件判断的输入值)
阶段二、
/* test1.c 文件 */
#include <stdio.h>
int __attribute__((weak)) func(void);
int main(int argc, const char *argv[])
{
if(func()) // call 0
func; //函数首地址,这里不做任何操作,编译器会忽略该语句
//printf("%d\n", func()); //不会打印出数值0
return 0;
}
编译:gcc test1.c
编译结果:通过,不做链接。
三、链接.o文件
3.1、函数
3.1.1、多个强符号
/* main.c 文件 */
#inlude <stdio.h>
void func(void)
{}
int main(int argc, const char *argv[])
{
func();
return 0;
}
/* strong.c 文件 */
void func(void)
{}
编译结果:
3.1.2、一个强符号多个弱符号
/* main.c 文件 */
#inlude <stdio.h>
void func(void)
{
printf("hi\n");
}
int main(int argc, const char *argv[])
{
func();
return 0;
}
/* weak1.c 文件 */
void __attribute__((weak)) func(void)
{
printf("%s\n", __FILE__);
}
/* weak2.c 文件 */
void __attribute__((weak)) func(void)
{
printf("%s\n", __FILE__);
}
编译与运行结果:
3.1.3、多个弱符号
/* main.c 文件 */
#include <stdio.h>
int main(int argc, const char *argv[])
{
func();
return 0;
}
/* weak1.c */
#include <stdio.h>
void __attribute__((weak)) func(void)
{
printf("%s\n", __FILE__);
}
/* weak2.c */
#include <stdio.h>
void __attribute__((weak)) func(void)
{
printf("%s\n", __FILE__);
}
编译与运行结果:
3.2、变量
3.2.1、多个强符号
/* main.c */
#include <stdio.h>
int a = 10;
int main(int argc, const char *argv[])
{
printf("a = %d\n", a);
}
/* strong.c 文件 */
int a = 20;
编译结果:
3.2.2、一个强符号多个弱符号
3.2.2.1、弱符号全部在.data中
/* main.c 文件 */
#include <stdio.h>
int a = 10;
int main(int argc, const char *argv[])
{
printf("a = %d\n", a);
}
/* weak1.c 文件 */
int __attribute__((weak)) a = 20;
/* weak2.c 文件 */
int __attribute__((weak)) a = 30;
编译与运行结果:
(这里就不一一列举排列顺序了)
3.2.2.2、弱符号部分在.data中,部分在.common中
/* main.c 文件 */
#include <stdio.h>
int a = 10;
int main(int argc, const char *argv[])
{
printf("a = %d\n", a);
}
/* weak1.c 文件 */
int __attribute__((weak)) a = 20;
/* weak_co.c 文件 */
int a;
编译与运行结果:
(这里就不一一列举排列顺序了)
3.2.2.3、弱符号全部在.common中
/* main.c 文件 */
#include <stdio.h>
int a = 10;
int main(int argc, const char *argv[])
{
printf("a = %d\n", a);
}
/* weak_co1.c 文件 */
int a;
/* weak_co2.c 文件 */
int a;
编译与运行结果:
(这里就不一一列举排列顺序了)
3.2.3、多个弱符号
3.2.3.1、弱符号全部在.data中
/* main.c 文件 */
#include <stdio.h>
extern int a;
int main(int argc, const char *argv[])
{
printf("a = %d\n", a);
}
/* weak1.c 文件 */
int __attribute__((weak)) a = 20;
/* weak2.c 文件 */
int __attribute__((weak)) a = 30;
编译与运行结果:
结论: 多个在.data中的弱符号,选择最靠近前的弱符号,即编译命令中写在最前面的(从左到右)。
3.2.3.2、弱符号部分在.data中,部分在.common中
未初始化的变量,编译器将其作为common的一部分,既然没有初始化,所以很难从打印函数中区分出来使用哪个变量。这里就通过最后编译完成的可执行目标文件中变量占用空间的大小作为判断。
int :4字节
double : 8字节
long double : 12字节
/* main.c 文件 */
#include <stdio.h>
extern int value;
int main(int argc, const char *argv[])
{
printf("value = %d\n", value);
return 0;
}
/* weak1.c 文件 */
int __attribute__((weak)) value = 20;
/* weak_co.c 文件 */
double value;
生成.o文件:
gcc -c weak1.c weak_co.c
生成可执行目标文件:gcc main.c weak1.o weak_co.o
显示符号表中的项:readelf -s weak1.o
显示节头信息:readelf -S weak1.o
readelf命令用法参考:readelf命令使用说明 🚀
数字4表示该变量(符号)占用4字节。
数字2表示所属的段,这里在.data段。
数字8表示该变量(符号)占用8字节。
COM表示所属的段,这里在.common段。
从中可知,value占用的空间大小为8字节,最后选用 double value 定义的弱定义。
如果改变.o文件的顺序是否会改变呢? 不会
结论:当存在.data中的弱符号和.common中的弱符号同时存在时,无论.o文件顺序如何,选择.common中弱符号。
3.2.3.3、弱符号全部在.common中
/* main.c 文件 */
#include <stdio.h>
extern int value;
int main(int argc, const char *argv[])
{
printf("value = %d\n", value);
return 0;
}
/* weak_co1.c 文件 */
int value;
/* weak_co2.c 文件 */
double value;
如果改变.o文件的顺序是否会改变呢? 不会
结论:当多个.common中的弱符号同时存在时,无论.o文件顺序如何,选择.common中数据类型最大的弱符号。
结合
3.2.3.2
和3.2.3.3
两部分的结论可知,存在.data中的弱符号和.common中的弱符号,链接器选择.common中数据类型最大的。
四、链接库文件
生成静态库文件:ar qs lib库名.a xx.o xx.o
ar qs libw.a xx.o xx.o
ar qs libs.a xx.o xx.o
4.1、函数
4.1.1、多个强符号
/* main.c 文件 */
#include <stdio.h>
int main(int argc, const char *argv[])
{
func();
return 0;
}
/* strong1.c 文件 */
#include <stdio.h>
void func(void)
{
printf("strong1.c\n");
}
/* strong2.c 文件 */
#include <stdio.h>
void func(void)
{
printf("strong2.c\n");
}
/* strong3.c 文件 */
#include <stdio.h>
void func(void)
{
printf("strong3.c\n");
}
/* strong4.c 文件 */
#include <stdio.h>
void func(void)
{
printf("strong4.c\n");
}
允许多个相同强符号同时出现,选择最先输入的库文件。
依据最先输入到库中的.o文件可确定排序关系,即最先输入则最先被链接器找到。
结论: 允许同名强符号同时出现,依据编译命令和创建库命令,从左到右依次寻找库文件(库文件中从左到右依次寻找.o文件)查找第一个强符号。
4.1.2、多个强符号多个弱符号
/* main.c 文件 */
#include <stdio.h>
int main(int argc, const char *argv[])
{
func();
return 0;
}
/* strong1.c 文件 */
#include <stdio.h>
void func(void)
{
printf("strong1.c\n");
}
/* strong2.c 文件 */
#include <stdio.h>
void func(void)
{
printf("strong2.c\n");
}
/* weak1.c 文件 */
#include <stdio.h>
void __attribute__((weak)) func(void)
{
printf("weak1.c\n");
}
/* weak2.c 文件 */
#include <stdio.h>
void __attribute__((weak)) func(void)
{
printf("weak2.c\n");
}
结论: 依据编译命令和创建库命令,从左到右依次寻找库文件(库文件中从左到右依次寻找.o文件)查找第一个符号,不管强弱。
4.1.3、多个弱符号
/* main.c 文件 */
#include <stdio.h>
int main(int argc, const char *argv[])
{
func();
return 0;
}
/* weak1.c 文件 */
#include <stdio.h>
void __attribute__((weak)) func(void)
{
printf("weak1.c\n");
}
/* weak2.c 文件 */
#include <stdio.h>
void __attribute__((weak)) func(void)
{
printf("weak2.c\n");
}
/* weak3.c 文件 */
#include <stdio.h>
void __attribute__((weak)) func(void)
{
printf("weak3.c\n");
}
/* weak4.c 文件 */
#include <stdio.h>
void __attribute__((weak)) func(void)
{
printf("weak4.c\n");
}
结论: 依据编译命令和创建库命令,从左到右依次寻找库文件(库文件中从左到右依次寻找.o文件)查找第一个弱符号。
4.1.4、小结
综合上面的例子,链接的时候,在静态库中,使用第一个找到的符号,后面的就不搜索了,即使出现多个强符号,也不管。
不过我们也可以强制链接器搜索所有的库,办法如下:--whole-archive
gcc和ld 中的参数 --whole-archive 和 --no-whole-archive
首先 --whole-archive 和 --no-whole-archive 是ld专有的命令行参数,gcc 并不认识,要通gcc传递到 ld,需要在他们前面加 -Wl,字串(中间不能有空格)。
–whole-archive 可以把 在其后面出现的静态库包含的函数和变量输出到动态库,–no-whole-archive 则关掉这个特性。
在–whole-archive作用下的库里不能有函数同名,强符号不可以,弱符号可以同名。
参考链接:gcc和ld 中的参数 --whole-archive 和 --no-whole-archive 🚀
4.1.1
中的例子,出现强符号重复定义的错误。
4.1.2
中的例子,一个强符号,多个弱符号,无论顺序如何,选择强符号。
4.1.3
中的例子,多个弱符号,选择最先出现的符号。
💡 加上 --whole-archive
用法同链接.o文件一样。
4.2、变量
4.2.1、多个强符号
/* main.c 文件 */
#include <stdio.h>
extern int a;
int main(int argc, const char *argv[])
{
printf("a = %d\n", a);
return 0;
}
/* strong1.c */
int a = 10;
/* strong2.c */
int a = 20;
小结:
- 存在多个强符号不会出现重定义;
- 选择第一个出现的变量强符号。
当使用 --whole-archive
,会出现多重定义。
4.2.2、一个强符号多个弱符号
4.2.2.1、弱符号全部在.data中
/* main.c 文件 */
#include <stdio.h>
extern int a;
int main(int argc, const char *argv[])
{
printf("a = %d\n", a);
}
/* strong1.c 文件 */
int a = 10;
/* weak1.c 文件 */
int __attribute__((weak)) a = 20;
/* weak2.c 文件 */
int __attribute__((weak)) a = 30;
结论: 选择第一个出现的变量符号,不管强弱。
当使用 --whole-archive
,无论顺序如何,始终选择强符号。
4.2.2.2、弱符号部分在.data中,部分在.common中
/* main.c 文件 */
#include <stdio.h>
extern int value;
int main(int argc, const char *argv[])
{
printf("value = %d\n", value);
return 0;
}
/* strong1.c 文件 */
int value = 10;
/* weak1.c 文件 */
int __attribute__((weak)) value = 20;
/* weak_co.c 文件 */
double value;
结论:
- 如果弱符号(.data段中)排最前面,选弱符号;
- 如果强符号排最前面,选强符号;
- 如果弱符号(.common段中)排最前面,选后面的强符号。
当使用 --whole-archive
,无论顺序如何,始终选择强符号。
4.2.2.3、弱符号全部在.common中
/* main.c 文件 */
#include <stdio.h>
extern int value;
int main(int argc, const char *argv[])
{
printf("value = %d\n", value);
return 0;
}
/* strong1.c 文件 */
int value = 10;
/* weak_co1.c 文件 */
int value;
/* weak_co2.c 文件 */
double value;
结论: 选择强符号。
当使用 --whole-archive
,无论顺序如何,始终选择强符号。
4.2.3、多个弱符号
4.2.3.1、弱符号全部在.data中
/* main.c 文件 */
#include <stdio.h>
extern int value;
int main(int argc, const char *argv[])
{
printf("value = %d\n", value);
return 0;
}
/* weak1.c 文件 */
int __arrtibute__((weak)) value = 10;
/* weak2.c 文件 */
int __arrtibute__((weak)) value = 20;
结论: 选择最先找到的符号。
当使用 --whole-archive
,选择最先出现的符号。
4.2.3.2、弱符号部分在.data中,部分在.common中
/* main.c 文件 */
#include <stdio.h>
extern int value;
int main(int argc, const char *argv[])
{
printf("value = %d\n", value);
return 0;
}
/* weak1.c 文件 */
int __attribute__((weak)) value = 10;
/* weak_co1.c 文件 */
double value;
结论: 选择最先找到的符号。
当使用 --whole-archive
,选择.COMMON中的符号。
/* weak1.c 文件 */
double __attribute__((weak)) value = 10;
/* weak_co1.c 文件 */
int value;
4.2.3.3、弱符号全部在.common中
/* main.c 文件 */
#include <stdio.h>
extern int value;
int main(int argc, const char *argv[])
{
printf("value = %d\n", value);
return 0;
}
/* weak1.c 文件 */
int value;
/* weak_co1.c 文件 */
double value;
结论: 选择最先找到的符号。
当使用 --whole-archive
,选择.COMMON中类型最大的符号。
💡 加上 --whole-archive
用法同链接.o文件一样。
五、强弱符号存在,如果避免错误?
1、上策:想办法消除全局变量。全局变量会增加程序的耦合性,对他要控制使用。如果能用其他的方法代替最好。
2、中策:实在没有办法,那就把全局变量定义为static,它是没有强弱之分的。而且不会和其他的全局符号产生冲突。至于其他文件可能对他的访问,可以封装成函数。把一个模块的数据封装起来是一个好的实践。
3、下策:把所有的符号全部都变成强符号。所有的全局变量都初始化,记住,是所有的,哪怕初始值是0都行。如果一个没有初始化,就可能会和其他人产生冲突,尽管别人初始化了。(自己写代码测试一下)。
4、必备之策:GCC提供了一个选项,可以检查这类错误:-fno-common
。(只是对在COMMON块中的弱定义)
转自于:C语言中的强符号与弱符号 🚀
六、说明:COMMON 节
对于C程序而言,经过预编译、编译、汇编、链接过程,完成从高级语言到机器码的转换,其中汇编和链接后的代码为以ELF格式的目标文件(Linux环境下),而ELF格式包含有ELF头、节头部表、.text、.data、.bss等。
- .text:已编译程序的机器代码。
- .data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在 .data 节中,也不出现在 .bss 节中。
- .bss:未初始化的静态C变量,以及所有被初始化为0的全局和静态C变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。
- .common:未初始化的全局变量。这是伪节,存在于符号表中。
强符号(变量)由于
__attribute__((weak))
的修饰转变为弱符号,但是在编译的时候被划分到.data 段。对于函数而言,转变为弱符号,依然在 .text 段。
编译器和链接器支持一种叫 COMMOM块(Common Block)
的机制,这种机制用来解决 一个弱符号定义在多个目标文件中,而它们的类型又不同(即大小不同) 的情况。
如果没有初始化,那么编译时将其存储在common块,等到链接时再将其放入到.bss段。(这点不同的编译器会有所不同,有的编译器会直接把没有初始化的全局变量放在.BSS段,而没有COMMON块机制)
为什么这么处理?
我们可以想到,当编译器将一个编译单元编译成目标文件的时候,如果该编译单元包含了弱符号(未初始化的全局变量就是典型的弱符号),那么该弱符号最终所占空间的大小此时是未知的,因为有可能其他编译单元中同符号名称的弱符号所占的空间比本编译单元该符号所占的空间要大。所以编译器此时无法为该弱符号在BSS段分配空间,因为所需要的空间大小此时是未知的。但是链接器在链接过程中可以确定弱符号的大小,因为当链接器读取所有输入目标文件后,任何一个弱符号的最终大小都可以确定了,所以它可以在最终的输出文件的BSS段为其分配空间。所以总体来看,未初始化的全局变量还是被放在BSS段。 ------摘自《程序员的自我修养》
转自于:C语言中的强符号与弱符号 🚀