【GCC编译优化系列】multiple-definition

描述

【GCC编译优化系列】这种让人看不懂的multiple-definition真的有点让人头疼

1 写在前面

有印象的朋友应该记得我之前写过一篇 关于GCC编译报错及对应解决办法,在该文的 3.5.3 章节有提到几种很典型的 multiple-definition 链接错误,也简要分析了其出现问题的原因及对应解决方法。

multiple-definition 在GCC编译报错里面,它的报错本质是 重复定义,可能是函数重复定义,也可能是变量重复定义。

但今天我要介绍的这个 multiple-definition 跟常规遇到的还不太一样,否则这个问题就不值得我写篇文章来做记录了,详细请看下文。

2 问题描述

事情是这样的,前几天一个同事给我报了一个我们SDK的问题,我想着加快复现问题,于是我找了他要他的应用代码,拿到我的编译环境环境来编译复现。

结果,好巧不巧,拿他代码一编译,居然给我报错了,而且这个报错把我整不会了!朋友,请看:

/home/xxx/compiler/riscv64_unkown_elf_gcc10.2.0/Linux64/bin/../lib/gcc/riscv64-unknown-elf/10.2.0/../../../../riscv64-unknown-elf/bin/ld: /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(user_app.o):/home/xxx/user_app/user_app.h:76: multiple definition of `mcu_ota_t'; /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(app_entry.o):/home/xxx/user_app/user_app.h:76: first defined here
/home/xxx/compiler/riscv64_unkown_elf_gcc10.2.0/Linux64/bin/../lib/gcc/riscv64-unknown-elf/10.2.0/../../../../riscv64-unknown-elf/bin/ld: /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(user_app.o):/home/xxx/user_app/user_app.h:70: multiple definition of `notify_state_t'; /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(app_entry.o):/home/xxx/user_app/user_app.h:70: first defined here
/home/xxx/compiler/riscv64_unkown_elf_gcc10.2.0/Linux64/bin/../lib/gcc/riscv64-unknown-elf/10.2.0/../../../../riscv64-unknown-elf/bin/ld: /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(user_app.o):/home/xxx/user_app/user_app.h:60: multiple definition of `wifi_state_t'; /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(app_entry.o):/home/xxx/user_app/user_app.h:60: first defined here
/home/xxx/compiler/riscv64_unkown_elf_gcc10.2.0/Linux64/bin/../lib/gcc/riscv64-unknown-elf/10.2.0/../../../../riscv64-unknown-elf/bin/ld: /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(user_app.o):/home/xxx/user_app/user_app.h:15: multiple definition of `frame_num_t'; /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(app_entry.o):/home/xxx/user_app/user_app.h:15: first defined here
collect2: error: ld returned 1 exit status

里面error提示的 multiple definition 异常亮眼,但是又让人摸不着头脑,这有点不按常理出牌!

要知道,他的应用代码明明都可以release版本的呀,而我的编译环境肯定也没有问题,毕竟 sample app 在我这都是可以编译通过的,所谓我大胆推测问题很有可能出在他们的应用代码上,而编译报错也的确提示是应用代码的问题。

3 场景复现

为了准确描述这个问题,排除其他的排除干扰因素,我把相关C代码和头文件捋一下。 整个应用部分的工程包括2个C代码和2个头文件:

主要处理应用入口的app_entry.c:

/* app_entry.c */

#include "sdk.h"       //SDK的统一头文件
#include "app_entry.h" //app_entry的头文件
#include "user_app.h"  //user_app的头文件

/* called by lower SDK */
int app_entry_main(void)
{
    /* some code */

    /* call user_app */
    user_app_init();

    /* some code */

    return 0;
}

appentry对应的头文件appentry.h:

#ifndef __APP_ENTRY_H__
#define __APP_ENTRY_H__

/* external functions */
extern int app_entry_main(void);

#endif /* end of __APP_ENTRY_H__ */

主要处理用户应用逻辑的user_app.c:

/* user_app.c */

#include "sdk.h"       //SDK的统一头文件
#include "app_entry.h" //app_entry的头文件
#include "user_app.h"  //user_app的头文件

/* other functions */

/* called by app_entry */
int user_app_init(void)
{
    /* some code */

    return 0;
}

userapp对饮的头文件userapp.h:

#ifndef __USER_APP_H__
#define __USER_APP_H__

/* some enum definition */
enum {
    UART_FRAME_1 = 0x01,
    UART_FRAME_2,
    UART_FRAME_3,
    UART_FRAME_4,
    UART_FRAME_5,
} frame_num_t;

enum {
    WIFI_STATE_1 = 0x01,
    WIFI_STATE_2,
    WIFI_STATE_3,
    WIFI_STATE_4,
    WIFI_STATE_5.
} wifi_state_t;

enum {
    NOTIFY_STATE_1 = 0x01,
    NOTIFY_STATE_2,
    NOTIFY_STATE_3,
    NOTIFY_STATE_4,
    NOTIFY_STATE_5,
} notify_state_t;

enum {
    MCU_OTA_NO_BIN = 0x00,
    MCU_OTA_DOWNLOAD_OK,
    MCU_OTA_DOWNLOAD_FAIL,
} mcu_ota_t;

/* external functions */
extern int user_app_init(void);

#endif /* end of __USER_APP_H__ */

另外,补充说明一下,我们使用的是交叉编译工具是针对RISCV架构的 riscv64-unknown-gcc

简化之后,应用代码大概就是如上面所示,就这样的代码给报错了,有点纳闷。

4 深入分析

4.1 可能性分析

头文件被重复包含了?

我看到这个报错的第一反应是,难道头文件被重复包含了?

比如在某个头文件中定义了一个变量(假设真有这么写的),如果它的头文件没有按照标准的 ifndef 的那种写法来写,那么当这个头文件被一个C文件直接或间接包含多次的时候,这个定义的变量就会存在多个副本,这个时候就会报 “multiple definition”。

可是,我仔细检查过user_app.h的头部写法,是正确的,不存在这种问题。

某个C文件里面存在多个xxx_t的副本?

这一种也是可能的,比如a.h中定义了一个xxx_t,然后b.h中也定义了同名的xxx_t,这时候某个C文件同时包含了a.h和b.h,那么xxx_t在这个C文件中就有两个定义。

这个时候,通过查看预处理后的文件(.i)文件就可以看得出来,是否存在这种情况。

如何打开生成预编译后的文件,可以参考 这篇文章的 4.2.2 章节介绍。

以本案例中的 mcu_ota_t 为例,很显然,并不存在这种情况,只有一个定义呢。

xxx@ubuntu:~/user_app$ 
xxx@ubuntu:~/user_app$ find . -name user_app.i
./out/user_app@xxxevb/modules/home/xxx/user_app/user_app.i
xxx@ubuntu:~/user_app$ cat ./out/user_app@xxxevb/modules/home/xxx/user_app/user_app.i | grep -nw mcu_ota_t
5547:}mcu_ota_t;
xxx@ubuntu:~/user_app$ 

4.2 分析map文件

既然是 multiple definition,那么我搜搜看!

给我上 grep大法,不搜不知道,一搜吓一跳。以 mcuotat 为例:

xxx@ubuntu:~/user_app$ grep -rsnw mcu_ota_t
user_app.h:77:}mcu_ota_t;
out/user_app@xxxevb/modules/home/xxx/user_app/user_app.i:5547:}mcu_ota_t;
out/user_app@xxxevb/modules/home/xxx/user_app/user_app.s:2488:        .globl  mcu_ota_t
out/user_app@xxxevb/modules/home/xxx/user_app/user_app.s:2554:        .section        .sbss.mcu_ota_t,"aw",@nobits
out/user_app@xxxevb/modules/home/xxx/user_app/user_app.s:2555:        .type   mcu_ota_t, @object
out/user_app@xxxevb/modules/home/xxx/user_app/user_app.s:2556:        .size   mcu_ota_t, 1
out/user_app@xxxevb/modules/home/xxx/user_app/user_app.s:2557:mcu_ota_t:
out/user_app@xxxevb/modules/home/xxx/user_app/user_app.s:3361:        .4byte  mcu_ota_t
out/user_app@xxxevb/modules/home/xxx/user_app/user_app.s:9661:        .string "mcu_ota_t"
Binary file out/user_app@xxxevb/modules/home/xxx/user_app/user_app.o matches
Binary file out/user_app@xxxevb/modules/home/xxx/user_app/app_entry.o matches
out/user_app@xxxevb/modules/home/xxx/user_app/app_entry.i:3807:}mcu_ota_t;
out/user_app@xxxevb/modules/home/xxx/user_app/app_entry.s:87: .globl  mcu_ota_t
out/user_app@xxxevb/modules/home/xxx/user_app/app_entry.s:94: .section        .sbss.mcu_ota_t,"aw",@nobits
out/user_app@xxxevb/modules/home/xxx/user_app/app_entry.s:95: .type   mcu_ota_t, @object
out/user_app@xxxevb/modules/home/xxx/user_app/app_entry.s:96: .size   mcu_ota_t, 1
out/user_app@xxxevb/modules/home/xxx/user_app/app_entry.s:97:mcu_ota_t:
out/user_app@xxxevb/modules/home/xxx/user_app/app_entry.s:574:        .4byte  mcu_ota_t
out/user_app@xxxevb/modules/home/xxx/user_app/app_entry.s:1136:       .string "mcu_ota_t"
out/user_app@xxxevb/binary/user_app@xxxevb.map:1811: .sbss.mcu_ota_t
out/user_app@xxxevb/binary/user_app@xxxevb.map:1879: .sbss.mcu_ota_t
out/user_app@xxxevb/binary/user_app@xxxevb.map:47777:mcu_ota_t                                         /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(app_entry.o)
Binary file out/user_app@xxxevb/libraries/user_app.a matches
Binary file out/user_app@xxxevb/libraries/user_app.stripped.a matches
GCC

map文件清晰地显示,在BSS段中有个object叫 mcuotat,要知道在BSS段中出现,这玩意就是global的东西了。

这什么意思?

意思就是编译器已经把mcuotat当做一个 全局变量 了。

那么我们来梳理一下,当userapp.h里面定义了一个 mcuotat 的全局变量,这个userapp.h同时被appentry.c和userapp.c包含,自然在这两个C文件中,都有这个mcuotat全局变量的副本存在;那么根据 【经验总结】一文带你了解C代码到底是如何被编译的 提及的,在链接阶段,编译器就会去查找并链接它们,这个时候多个同名全局变量,肯定是不允许的,自然而然,就报了 “multiple definition” 错误。

4.3 扒一扒基础语法

为此,我特意去查了一下C语言教科书,找了一些关于C语言的枚举定义的介绍,再学习了一下。

果然userapp.h中的那几个 xxxt 枚举并不是一种规范写法,倒不是说不可以这么写,只是这样写之后容易造成干扰,严重的情况下还会导致语法错误。

相关学习资料,可以看 参考链接 附录里面的文章。

GCC

4.4 GCC的版本差异

那么问题来了,为何同事的编译环境没报错,而我的编译环境报错了呢?

原来,前段时间我们的SDK因一个厂商私有库比较新,特意升级了GCC的版本,由 riscv64unkownelfgcc8.3.0 升级到了 riscv64unkownelfgcc10.2.0,而应用那边还没来得及升级这个版本。所以才造成了这样的冲突。

至于为何两个版本有差异呢?后面我也会提到,其实8.3.0版本对这种写法是有报 警告 的,而10.2.0版本是报 错误 的;在我们的编译环境中,除编译器版本不一样外,其他由构建层传入的所有编译选项都是一模一样的。

那么,下面分析下究竟的差异在哪里。

4.4.1 对比map文件和汇编代码

如下图所示:

GCC

汇编文件显示,两者编译出来的段分布是不一样的,一个在.common段,一个在.global段;

而map文件中,在.global段的被分配到了 .sbss段中,作为全局的object而存在;所以就报了 mutiple definiton 的错误。

这个简单分析,基本就可以确定是在编译阶段引入的问题,而不是在链接阶段引入的问题,所以后面的排查中,应重点关注编译选项,而不是链接选项。

4.4.2 如何查看GCC默认使用的编译选项

如何你将 “**” 这几个关键字去搜索,你很大概率拿到的是这个链接,它的方法是这样的:

echo "" | gcc -v -x c++ -E -

然后查看输出的内容中的:COLLECTGCCOPTIONS

对应我这边,替换掉对应的gcc版本,8.2.0和10.3.0版本的输出分别是:

GCC8.3.0    COLLECT_GCC_OPTIONS='-v' '-E' '-march=rv64imafdc' '-mabi=lp64d'
GCC10.2.0   COLLECT_GCC_OPTIONS='-v' '-E' '-march=rv64imafdc' '-mabi=lp64d' '-march=rv64imafdc'

眼看,压根看不出差异,对不对。那我倒怀疑是方法有问题。

我想起之前写过 一篇文章关于GCC默认链接选项 的,里面倒是提到了取默认参数的蛛丝马迹,立马实践下。

这时候你先准备一个简单得不能再简单的helloworld.c:

#include 

int main(void)
{
    printf("hello world\r\n");
    return 0;
}

然后在对应的目录执行(注意替换gcc的路径):

arm-none-gcc -v -Q hello.c

这个方法是我自己实践摸索总结出来的参数组合,全网估计还没人这么用!

这个方法可以顺利取得GCC默认使能的参数,留意输出的 options enabled 即可!

4.4.3 对比GCC的默认使能的编译选项

为了深究这个报错问题,我使用关键字 "mutiple definition 10.2.0",找到这么一个 有效链接,里面描述的情景,基本跟我的差不多。

GCC

摘抄里面的一段话,理解下:

The issue can be fixed with adding-fcommon to compiler options.

A common mistake in C is omitting extern when declaring a global variable in a header file. If the header is included by several files it results in multiple definitions of the same variable. In previous GCC versions this error is ignored. GCC 10 defaults to -fno-common, which means a linker error will now be reported. To fix this, use extern in header files when declaring global variables, and ensure each global is defined in exactly one C file. If tentative definitions of particular variables need to be placed in a common block, attribute((common)) can be used to force that behavior even in code compiled without -fcommon. As a workaround, legacy C code where all tentative definitions should be placed into a common block can be compiled with -fcommon.

顺着这个编译选项,我找到了GCC 10.x版本的 编译选项在线说明文档,摘抄下里面关于 -fcommon 选项 和 -fno-common 选项的说明,大家理解下:

  1. -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 theextern 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.

回过头来,根据前面取得的默认编译参数,我们对比下两个GCC版本的默认选项,我们果然发现了 -fcommon 有差别.

GCC

左边是8.3.0版本,它默认使能了 -fcommon 这个参数就决定了 mcuotat 编译到 .common 段;从而链接的时候,并不会报警告,而仅仅是报了一个 warning: multiple common of 警告。

而右边的10.2.0版本没有 -fcommon,根据在线说明可知,10.x版本默认是关闭了该选项,即使用的是 -fno-common,所以 mcuotat 编译到了 .global 段;这就直接导致在链接的时候,报了 mutiple-definiton 错误,因为位于 .global 段是不能有多份一样的定义。

按照这个分析,在10.2.0版本中,手动加上 -fcommon 选项,编译也不会报 mutiple-definiton 错误。

是否真是如此,留个小疑问,有心读者可以自行验证验证。

4.4.4 得出结论

综上几个步骤下来,基本可以得出一个结论,外围调用GCC发起编译、链接等能看得见的步骤里,两个版本的参数都是一模一样的,很显然不是因为上层传入的编译选项导致的;经过精准地资料辅助分析,得出是 GCC 10.2.0 版本默认使用的 -fno-common 选项惹的祸,但它的本意初衷是好的,只不过不被程序猿所熟知而已。

一个看似简单的 mutiple-definiton 问题,绕了一圈,终于发现、理解并有效解决地解决这个问题。

5 修复验证

5.1 问题修复

明白了上面的基础语法和GCC的编译特性之后,修复的方法就很简单了,只需要把 user_app.h 中所有的枚举定义加上一个 typedef,正如 C语言--enum,typedef enum 枚举类型详解 所介绍的方法三那样。

修改后的代码如下:

#ifndef __USER_APP_H__
#define __USER_APP_H__

/* some enum definition */
typedef enum {
    UART_FRAME_1 = 0x01,
    UART_FRAME_2,
    UART_FRAME_3,
    UART_FRAME_4,
    UART_FRAME_5,
} frame_num_t; //注意:此处的frame_num_t为枚举型enum frame_num_t的别名

typedef enum {
    WIFI_STATE_1 = 0x01,
    WIFI_STATE_2,
    WIFI_STATE_3,
    WIFI_STATE_4,
    WIFI_STATE_5.
} wifi_state_t; //注意:此处的wifi_state_t为枚举型enum wifi_state_t的别名

typedef enum {
    NOTIFY_STATE_1 = 0x01,
    NOTIFY_STATE_2,
    NOTIFY_STATE_3,
    NOTIFY_STATE_4,
    NOTIFY_STATE_5,
} notify_state_t; //注意:此处的notify_state_t为枚举型enum notify_state_t的别名

typedef enum {
    MCU_OTA_NO_BIN = 0x00,
    MCU_OTA_DOWNLOAD_OK,
    MCU_OTA_DOWNLOAD_FAIL,
} mcu_ota_t; //注意:此处的mcu_ota_t为枚举型enum mcu_ota_t的别名

/* external functions */
extern int user_app_init(void);

#endif /* end of __USER_APP_H__ */

主要的核心修改,就是把enum的写法纠正了,我跟对应的应用开发的童鞋聊过,他说可能就是写代码的时候 偷懒 了点,压根没写到这样的写法有啥不妥,最最最重要的是 riscv64unkownelf_gcc8.3.0 的默认编译参数,放任了这种有问题的写法(仅仅是编译警告,而不是编译错误),从而没有在第一时间暴露出来,造成代码的语法隐患。

riscv64unkownelf_gcc8.3.0 版本的编译输出,注意其实这里是有 警告 的!

Making user_app@xxxevb.elf
/home/xxx/compiler/riscv64_unkown_elf_gcc8.3.0/Linux64/bin/../lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(user_app.o) and /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(app_entry.o): warning: multiple common of `mcu_ota_t'
/home/xxx/compiler/riscv64_unkown_elf_gcc8.3.0/Linux64/bin/../lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(user_app.o) and /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(app_entry.o): warning: multiple common of `notify_state_t'
/home/xxx/compiler/riscv64_unkown_elf_gcc8.3.0/Linux64/bin/../lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(user_app.o) and /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(app_entry.o): warning: multiple common of `wifi_state_t'
/home/xxx/compiler/riscv64_unkown_elf_gcc8.3.0/Linux64/bin/../lib/gcc/riscv64-unknown-elf/8.3.0/../../../../riscv64-unknown-elf/bin/ld: /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(user_app.o) and /home/xxx/user_app/out/user_app@xxxevb/libraries/user_app.a(app_entry.o): warning: multiple common of `frame_num_t'

Making user_app@xxxevb.bin
Making user_app@xxxevb.hex

5.2 问题验证

代码修复之后,使用 riscv64unkownelfgcc8.3.0 版本的GCC和 riscv64unkownelfgcc10.2.0版本的GCC,均一次编译通过,这才是正统的C语言写法,容不得半点偷懒啊!

GCC

同时,我们再分析下问题修复之后,map文件里面对这几个定义的变化,以 mcuotat 为例:

GCC

如同我们所预料的,加上typedef之后,这个mcuotat已经是一个枚举类型的别名,并不是一个变量,自然在map文件肯定找不到它,但是原来的那种写法能找到的原因是,它那种是定义了一个全局变量叫 mcuotat。这才是两者的本质区别。

6 经验总结

  • 严谨地写好每一行代码:了解每一行代码背后的基础语法,温故而知新。
  • 对比确认是个好方法:选择适当的比较方法,找出差异,往往差异的地方就是解决问题的突破口。
  • 回归问题的本质:暂且认为 编译器的报错是不会骗人的,在这个基础之上,逐步从问题报错的表面往里面深究,为何会是 “multiple definition”,何时才会出现这种错误?
  • typedef 是个好东西,用好它:熟悉它的基础语法,每一种写法的搭配代表什么含义,理解并应用它,很重要。
  • 认真对待每一个编译器提示的 编译警告:保不准这些警告哪天就把你带入坑里,使用GCC的-Werror是个好选择,把警告当错误处理,有助于你写出更为严谨的代码。
  • GCC的默认编译参数:这个了解非常有必要,不然下次遇到好端端的代码编译不过,就没辙了。

7 参考链接

  • 【经验科普】实战分析C工程代码可能遇到的编译问题及其解决思路
  • 【经验总结】一文带你了解C代码到底是如何被编译的
  • 【C语言之结构体】如何定义结构体并定义结构体变量
  • 【C语言之枚举】如何定义枚举并定义枚举变量
  • 【C语言之typedef】typedef的基本用法

8 更多分享

欢迎关注我的github仓库01workstation,日常分享一些开发笔记和项目实战,欢迎指正问题。

同时也非常欢迎关注我的CSDN主页和专栏:

【CSDN主页:架构师李肯】

【RT-Thread主页:架构师李肯】

【C/C++语言编程专栏】

【GCC专栏】

【信息安全专栏】

【RT-Thread开发笔记】

【freeRTOS开发笔记】

有问题的话,可以跟我讨论,知无不答,谢谢大家。

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分