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

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

-fcommon

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

也就是使用了fcommon,未初始化的全局变量放在COMMON块,允许重复定义,链接期会合并为1个。

GCC 默认的链接器行为是,如果在链接过程中发现重复的符号,它会选择第一个找到的符号,并忽略后续的符号。这意味着,链接器会使用第一个定义的符号(包括它的数据类型),而忽略后续定义的符号。但是2个变量使用的内存初始地址是一样的,也就是一个int a和double a其实共用一块内存,int占4个字节,double占8个字节,这样其实可能导致问题。int a中可能还有一个int b,这个b占用了a后面的4个字节,2个变量(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属性的差异。

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

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

librdkafka的编译问题?

目前librdkafka的最新版本是2.5,最近需要升级这个库,于是我在十几个操作系统编译了一下这个库,有3个编译报错,其中一个是因为依赖的libssl没找到,于是我在lib64指定了一下libssl.so也就编译通过了,而官方最近刚修复了这个问题,也就是没找ssl依赖的情况下,也能编译成功,有点巧。

还有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 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,所以也没什么好说的。8的gcc版本一般为8.3,可以正常编译,后续其实要推动客户升级操作系统,毕竟后续会出现越来越多的兼容性问题。


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