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
2
3
4
5
6
-fcommon
In C code, this option controls the placement of global variables defined without an initializer, known as tentative definitions in the C standard. Tentative definitions are distinct from declarations of a variable with the extern keyword, which do not allocate storage.

The default is -fno-common, which specifies that the compiler places uninitialized global variables in the BSS section of the object file. This inhibits the merging of tentative definitions by the linker so you get a multiple-definition error if the same variable is accidentally defined in more than one compilation unit.

The -fcommon places uninitialized global variables in a common block. This allows the linker to resolve all tentative definitions of the same variable in different compilation units to the same object, or to a non-tentative definition. This behavior is inconsistent with C++, and on many targets implies a speed and code size penalty on global variable references. It is mainly useful to enable legacy code to link without errors.

文章已经讲解得很清楚,这里总结一下我的理解:

-fno-common(GCC 10+ 默认)

  • 作用:当使用 -fno-common 选项时,编译器将未初始化的全局变量放置在 BSS 段中
  • 链接器行为:未初始化的全局变量会被视为强符号。在链接阶段,如果相同的未初始化全局变量在多个编译单元中被定义,链接器会将其视为重复定义,并报告错误。也就是说,链接器不会合并这些变量的定义,任何多个定义的冲突都会导致链接错误

-fcommon(GCC 9 及以下默认)

  • 作用:当使用 -fcommon 选项时,编译器会将未初始化的全局变量放入一个 COMMON 块中
  • 链接器行为COMMON 块是一种特殊的内存区域,允许链接器在链接阶段合并同名的未初始化全局变量的定义。即使这些变量在多个编译单元中被定义,链接器会将它们合并为一个单一的变量对象

总结:使用 -fcommon 时,未初始化的全局变量放在 COMMON 块,允许重复定义,链接期会合并为 1 个。

潜在问题

GCC 默认的链接器行为是,如果在链接过程中发现重复的符号,它会选择第一个找到的符号,并忽略后续的符号。这意味着,链接器会使用第一个定义的符号(包括它的数据类型),而忽略后续定义的符号。

但是,两个变量使用的内存初始地址是一样的。例如,一个 int adouble a 其实共用一块内存:

  • int 占 4 个字节
  • double 占 8 个字节

这样可能导致严重问题。假设 int a 后面还有一个 int bb 占用了 a 后面的 4 个字节。如果同时存在 double a,那么 bdouble 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属性的差异。

绝大多数情况下,函数和已初始化的变量是强符号,而未初始化的变量是弱符号。对于它们,下列三条规则适用:

  1. 同名的强符号只能存在一个。
  2. 一个强符号可以和多个同名的弱符号共存,但调用时会选择强符号的值。
  3. 有多个弱符号时,链接器可以选择其中任意一个。

librdkafka 的编译问题

目前 librdkafka 的最新版本是 2.5,最近需要升级这个库,于是在十几个操作系统上编译测试。发现有 3 个编译报错:

  1. SSL 依赖问题:其中一个是因为依赖的 libssl 没找到,在 lib64 指定了 libssl.so 后编译通过。官方最近刚修复了这个问题,即使没找到 SSL 依赖也能编译成功,有点巧合。

  2. 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
typedef struct rd_kafka_toppar_s rd_kafka_toppar_t;

rdkafka_int.hrdkafka_op.h 都定义了这个类型,然后 rdkafka_op.c 都包含了这两个头文件,很明显是重复定义了。

C 语言标准差异

ISO/IEC 9899:1999(C99 之前) 6.7.3 中描述:

1
2
3
If an identifier has no linkage, there shall be no more than one declaration of the identifier
(in a declarator or type specifier) with the same scope and in the same name space, except
for tags as specified in 6.7.2.3.

C99 标准 中:

1
2
3
If the same qualifier appears more than once in the same specifier-qualifier-list, either
directly or via one or more typedefs, the behavior is the same as if it appeared only
once.

结论

  • 以前的 C 标准不允许重复声明
  • C99 开始允许了,因此代码这么写没有问题
  • 只是老版本的 GCC 遵循老的 C 标准,编译会报错

但是 librdkafka 明确表示以后不支持 CentOS 6 和 7,所以也没什么好说的。CentOS 8 的 GCC 版本一般为 8.3,可以正常编译。后续需要推动客户升级操作系统,毕竟后续会出现越来越多的兼容性问题。


gcc编译选项--fcommon
https://zjfans.github.io/2024/08/17/gcc编译选项--fcommon/
作者
张三疯
发布于
2024年8月17日
许可协议