gcc编译选项--fcommon
Multiple Definition 错误
最近在龙蜥操作系统上编译时,出现了 multiple definition 错误(重复定义错误)。查看代码后,发现有几个变量确实是重复定义了。
问题原因:在一个 .h 文件中定义了一个变量,多个 .c 文件又包含了这个 .h 文件,导致该变量在多个 .c 文件中重复定义,链接时报错。
修复方法:在 .h 文件中的变量统一加上 extern 声明,然后只有一个 .c 文件负责定义和初始化该变量。这样可以避免重复定义,编译也能顺利通过。
GCC 版本差异
代码确实存在问题,但在其他操作系统上并不会报错,只是出现警告,因此很好奇这是为什么。网上搜索后,发现了一篇很好的文章:https://club.rt-thread.org/ask/article/5fb1ecf297a83492.html
关键发现:GCC 版本在 10 版本后,默认关闭了 -fcommon 选项,改为使用 -fno-common。让我们来看看这个选项的描述:
1 | |
文章已经讲解得很清楚,这里总结一下我的理解:
-fno-common(GCC 10+ 默认)
- 作用:当使用
-fno-common选项时,编译器将未初始化的全局变量放置在 BSS 段中 - 链接器行为:未初始化的全局变量会被视为强符号。在链接阶段,如果相同的未初始化全局变量在多个编译单元中被定义,链接器会将其视为重复定义,并报告错误。也就是说,链接器不会合并这些变量的定义,任何多个定义的冲突都会导致链接错误
-fcommon(GCC 9 及以下默认)
- 作用:当使用
-fcommon选项时,编译器会将未初始化的全局变量放入一个COMMON块中 - 链接器行为:
COMMON块是一种特殊的内存区域,允许链接器在链接阶段合并同名的未初始化全局变量的定义。即使这些变量在多个编译单元中被定义,链接器会将它们合并为一个单一的变量对象
总结:使用 -fcommon 时,未初始化的全局变量放在 COMMON 块,允许重复定义,链接期会合并为 1 个。
潜在问题
GCC 默认的链接器行为是,如果在链接过程中发现重复的符号,它会选择第一个找到的符号,并忽略后续的符号。这意味着,链接器会使用第一个定义的符号(包括它的数据类型),而忽略后续定义的符号。
但是,两个变量使用的内存初始地址是一样的。例如,一个 int a 和 double a 其实共用一块内存:
int占 4 个字节double占 8 个字节
这样可能导致严重问题。假设 int a 后面还有一个 int b,b 占用了 a 后面的 4 个字节。如果同时存在 double a,那么 b 和 double a 会共用一块内存,肯定会有问题。
使用 -fno-common 后:未初始化的全局变量现在会放到 BSS 段,属于强符号,不允许重复定义,从而避免了上述问题。
内存模型
C语言程序的内存通常被分为几个主要区域:
- Text段(代码段):
- 作用:存放程序的可执行代码,包括所有函数和指令。
- 属性:通常是只读的,以防止程序代码在运行时被修改,并且可以在多个进程间共享。
- Data段(数据段):
- 作用:存放已初始化的全局变量和静态变量。
.data段:包含初始化的全局和静态变量。.rodata段:包含只读的初始化数据(如字符串常量)。
- 作用:存放已初始化的全局变量和静态变量。
- BSS段:
- 作用:存放未初始化的全局变量和静态变量。程序加载时,这部分内存会被自动初始化为0。
- 特点:在可执行文件中不占用实际空间,只有在程序运行时才分配内存。
- 堆(Heap):
- 作用:动态分配内存区域,通过函数如
malloc()和free()进行管理。用于存放动态分配的数据结构。 - 管理:需要程序员手动管理内存分配和释放,避免内存泄漏和悬挂指针。
- 作用:动态分配内存区域,通过函数如
- 栈(Stack):
- 作用:存放局部变量、函数参数和返回地址。每个函数调用时会在栈上分配一个栈帧。
- 特点:栈内存的分配和释放由系统自动管理。栈大小通常有限,如果超出栈空间会导致栈溢出。
强符号/弱符号
对于全局变量来说,如果初始化了不为0的值,那么该全局变量则被保存在data段,如果初始化的值为0,那么将其保存在bss段,如果没有初始化,则将其保存在common段,等到链接时再将其放入到bss段。关于第三点不同编译器行为会不同,有的编译器会把没有初始化的全局变量直接放到bss段,也就是gcc的-fcommon与-fno-common属性的差异。
绝大多数情况下,函数和已初始化的变量是强符号,而未初始化的变量是弱符号。对于它们,下列三条规则适用:
- 同名的强符号只能存在一个。
- 一个强符号可以和多个同名的弱符号共存,但调用时会选择强符号的值。
- 有多个弱符号时,链接器可以选择其中任意一个。
librdkafka 的编译问题
目前 librdkafka 的最新版本是 2.5,最近需要升级这个库,于是在十几个操作系统上编译测试。发现有 3 个编译报错:
SSL 依赖问题:其中一个是因为依赖的
libssl没找到,在lib64指定了libssl.so后编译通过。官方最近刚修复了这个问题,即使没找到 SSL 依赖也能编译成功,有点巧合。typedef 重复定义问题:还有 2 个操作系统报了
redefinition of typedef错误:- SUSE SP4
- CentOS 6
在 GitHub 上搜索后,发现这是老问题,这个项目以前经常会遇到这个错误,看起来已经修复过了,但现在还会有错误?
关键发现:这 2 个操作系统的 GCC 都是 4.4,版本比较低。而其他编译成功的操作系统,GCC 版本有 4.8、7.3、8.3、10.3、12.3。也就是说,旧版本的 GCC 反而会报错。
代码分析
找了一个变量查看代码:
1 | |
在 rdkafka_int.h 和 rdkafka_op.h 都定义了这个类型,然后 rdkafka_op.c 都包含了这两个头文件,很明显是重复定义了。
C 语言标准差异
ISO/IEC 9899:1999(C99 之前) 6.7.3 中描述:
1 | |
C99 标准 中:
1 | |
结论:
- 以前的 C 标准不允许重复声明
- C99 开始允许了,因此代码这么写没有问题
- 只是老版本的 GCC 遵循老的 C 标准,编译会报错
但是 librdkafka 明确表示以后不支持 CentOS 6 和 7,所以也没什么好说的。CentOS 8 的 GCC 版本一般为 8.3,可以正常编译。后续需要推动客户升级操作系统,毕竟后续会出现越来越多的兼容性问题。