起因

一次偶然碰到一个诡异的bug,现象是同一份C++代码使用GCC4.4.x版本在开启优化前和优化后的结果不一样,优化后的代码逻辑不正确。

示例代码如下:

//main.cpp
#include <stdio.h> enum Type {
ERR_A = -1,
ERR_B = 0,
ERR_C = 1,
}; void func(Type tt) {
switch(tt){
case ERR_A:
printf("case ERR_A, tt = %d\n", tt);
break;
case ERR_B:
printf("case ERR_B, tt = %d\n", tt);
break;
case ERR_C:
printf("case ERR_C, tt = %d\n", tt);
break;
default:
printf("case default, tt = %d\n", tt);
break;
}
} int main(){
Type tt = (Type)4;
func(tt); //预期输出case default
tt = (Type)1;
func(tt); //预期输出case ERR_C
tt = (Type)-4;
func(tt); //预期输出case default
return 0;
}

将这段代码分别使用 g++ -O0g++ -O1 编译,结果让人诧异,在tt=4的时候,switch却跳到了1的分支。

$ g++ -O0 -g -o main main.cpp
$ ./main
case default, tt = 4
case ERR_C, tt = 1
case default, tt = -4
$ g++ -O1 -g -o main main.cpp
$ ./main
case ERR_C, tt = 1
case ERR_C, tt = 1
case default, tt = -4

排查过程

考虑到是有enum存在,可能是枚举超出定义范围而被GCC优化掉了,在网上找到一篇帖子,大意是讲enum是以int类型存储的,同时32bit在cpu中有更快的处理效率。 通过单步调试和watch命令也会发现tt的值一直是4,且没有被更改,因此可以排除enum undefined这种情况。

于是只能去看汇编代码了,事实证明这才是最有效的方式,比自己瞎猜要节省时间。

可以通过调试时使用disas命令查看汇编代码,也可以使用objdump直接看二进制的汇编代码。

对比下debug(上)和release(下)两种情况下的汇编代码。

# 未开启优化
(gdb) b 26
Breakpoint 1 at 0x400620: file main.cpp, line 26.
(gdb) r
...
(gdb) n
27 func(tt);
(gdb) s
func (tt=4) at main.cpp:10
10 switch(tt){
(gdb) disas /m
Dump of assembler code for function func(Type):
9 void func(Type tt){
0x00000000004005a4 <+0>: push %rbp
0x00000000004005a5 <+1>: mov %rsp,%rbp
0x00000000004005a8 <+4>: sub $0x10,%rsp
0x00000000004005ac <+8>: mov %edi,-0x4(%rbp) 10 switch(tt){
=> 0x00000000004005af <+11>: mov -0x4(%rbp),%eax
0x00000000004005b2 <+14>: test %eax,%eax
0x00000000004005b4 <+16>: je 0x4005d6 <func(Type)+50>
0x00000000004005b6 <+18>: cmp $0x1,%eax
0x00000000004005b9 <+21>: je 0x4005ec <func(Type)+72>
0x00000000004005bb <+23>: cmp $0xffffffffffffffff,%eax
0x00000000004005be <+26>: jne 0x400602 <func(Type)+94>
11 case ERR_A:
12 printf("case ERR_A, tt = %d\n", tt);
0x00000000004005c0 <+28>: mov -0x4(%rbp),%eax ... 14 case ERR_B:
15 printf("case ERR_B, tt = %d\n", tt);
0x00000000004005d6 <+50>: mov -0x4(%rbp),%eax ... 17 case ERR_C:
18 printf("case ERR_C, tt = %d\n", tt);
0x00000000004005ec <+72>: mov -0x4(%rbp),%eax ... 20 default:
21 printf("case default, tt = %d\n", tt);
0x0000000000400602 <+94>: mov -0x4(%rbp),%eax
# 开启O1优化选项
(gdb) b 26
Breakpoint 1 at 0x400611: file main.cpp, line 26.
(gdb) r
...
(gdb) n
case ERR_C, tt = 1
29 func(tt);
(gdb) s
func (tt=ERR_C) at main.cpp:9
9 void func(Type tt){
(gdb) disas /m
Dump of assembler code for function func(Type):
9 void func(Type tt){
=> 0x00000000004005a4 <+0>: sub $0x8,%rsp 10 switch(tt){
0x00000000004005a8 <+4>: test %edi,%edi
0x00000000004005aa <+6>: je 0x4005cb <func(Type)+39>
0x00000000004005ac <+8>: test %edi,%edi
0x00000000004005ae <+10>: jg 0x4005e1 <func(Type)+61>
0x00000000004005b0 <+12>: cmp $0xffffffffffffffff,%edi
0x00000000004005b3 <+15>: jne 0x4005f7 <func(Type)+83>
11 case ERR_A:
12 printf("case ERR_A, tt = %d\n", tt);
0x00000000004005b5 <+17>: mov $0xffffffff,%esi ... 14 case ERR_B:
15 printf("case ERR_B, tt = %d\n", tt);
0x00000000004005cb <+39>: mov $0x0,%esi ... 17 case ERR_C:
18 printf("case ERR_C, tt = %d\n", tt);
0x00000000004005e1 <+61>: mov $0x1,%esi ... 20 default:
21 printf("case default, tt = %d\n", tt);
0x00000000004005f7 <+83>: mov %edi,%esi ...

可以看到在O0时,汇编逻辑为:等于0时跳到case B,等于1跳到了case C,不等于-1跳到default, 等于-1到case A。

而在O1时,汇编逻辑为: 等于0时跳到case B,大于0直接跳到了case C,不等于-1跳到default, 等于-1到case A。

出错的原因就在于开启编译优化后,GCC对大于零的情况默认其为case C(1),这里推测是由于test是使用位运算,而cmp是使用加减运算,使用test提高了运算效率。 但是这种改变代码逻辑,让逻辑出错的优化显然是让人难以接受的。

官方解释

如此诡异的问题虽然找到了原因,但内心还是无法接受这是GCC犯的错误。

经过谷歌一番,找到了这篇帖子, 果然有人也踩到了同样的坑。

这是一个GCC4.4版本被反馈过的bug,尽管这个优化很不合理,但依然被作为一个"feature"被保留下来...

在高版本GCC中,使用-std=c++03 -fstrict-enum选项可以开启这个"特性",该特性假设编程者会保证enum的取值在其定义范围内。

最后,解决这个问题的方法有两种,在switch之前做一次enum的范围检查,或者使用更高版本GCC。

其他

最后的最后,附一个查询资料时看到的关于GCC对switch做的优化...

参考

  1. what is the size of an enum type data in C++? - https://stackoverflow.com/questions/8115550/what-is-the-size-of-an-enum-type-data-in-c
  2. Guard code after switch on enum is never reached - https://stackoverflow.com/questions/8679534/guard-code-after-switch-on-enum-is-never-reached/8679627
  3. Bug 41425 - switch with enums doesn't work - https://gcc.gnu.org/bugzilla/show_bug.cgi?id=41425
  4. Options Controlling C++ Dialect - https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Dialect-Options.html#index-fstrict-enums
  5. From Switch Statement Down to Machine Code - http://lazarenko.me/switch/

原文地址:https://lidawn.github.io/2018/09/02/gcc-bug/

排查GCC 4.4.X版本优化switch-enum的BUG的更多相关文章

  1. gcc都做了什么优化

    直接上程序: setjmp和longjmp是处理函数嵌套调用的,goto语句不能跨越函数,所以不选择goto. #include <setjmp.h> int setjmp(jmp_buf ...

  2. ubuntu 里切换 gcc,g++ 的版本

    https://askubuntu.com/questions/26498/choose-gcc-and-g-version https://stackoverflow.com/questions/7 ...

  3. GCC 7.3.0版本编译http-parser-2.1问题

    http-paser是一个用c编写的http消息解析器,地址:https://github.com/nodejs/http-parser,目前版本2.9 今天用gcc 7.3.0编译其2.1版本时,编 ...

  4. 版本优化-test

    版本优化 标签(空格分隔): 测试 需求经手人太多,直接提bug,开发不乐意,跟Leader确认不靠谱,跟PM确认,不熟悉流程,跟第三方PM确认靠谱了,结果被开发三言两语,变成了不改bug 而改需求 ...

  5. VS编译器优化诱发一个的Bug

    VS编译器优化诱发一个的Bug Bug的背景 我正在把某个C++下的驱动程序移植到C下,前几天发生了一个比较诡异的问题. 驱动程序有一个bug,但是这个bug只能 Win32 Release 版本下的 ...

  6. 【转】C 编译器优化过程中的 Bug

    C 编译器优化过程中的 Bug 一个朋友向我指出一个最近他们发现的 GCC 编译器优化过程(加上 -O3 选项)里的 bug,导致他们的产品出现非常诡异的行为.这使我想起以前见过的一个 GCC bug ...

  7. react native 0.56.0版本在windows下有bug不能正常运行

    react native的0.56.0版本在windows下有bug不能正常运行请init 0.55.4的版本 react-native init MyApp --version 0.55.4 注意v ...

  8. CentOS 6.6 升级GCC G++ (当前最新版本为v6.1.0) (完整)

    ---恢复内容开始--- CentOS 6.6 升级GCC G++ (当前最新GCC/G++版本为v6.1.0) 没有便捷方式, yum update....   yum install 或者 添加y ...

  9. Unity3D游戏GC优化总结---protobuf-net无GC版本优化实践

    protobuf-net优化效果图 protobuf-net是Unity3D游戏开发中被广泛使用的Google Protocol Buffer库的c#版本,之所以c#版本被广泛使用,是因为c++版本的 ...

随机推荐

  1. oracle 查看表空间以及剩余量

    --1.查看表空间的名称及大小 SELECT t.tablespace_name, round(SUM(bytes / (1024 * 1024)), 0) ts_size FROM dba_tabl ...

  2. f-stack中nginx配置后make出现error: ignoring return value of ‘ftruncate’

    问题 Nginx 配置后 make 出现error: src/os/unix/ngx_process_cycle.c: In function 'ngx_start_worker_processes' ...

  3. Vue04——vue自定义事件、Router、Vue-cli、发布上线

    一.Vue的自定义事件 点击任何一个按钮,按钮本身计数累加,但是每点击三个按钮中的一个,totalCounter 都要累加. <body> <div id="app&quo ...

  4. eclipse中svn插件装好后出现"位置错误"的处理

    错误现象: 验证位置时发生错误:"org.apache.subversion.javahl.ClientException:svn:E210004: Number is larger tha ...

  5. 树概念及使用connect by进行级联查询

    树 树,大家都见过,以这种形式的数据关系,就是树.下面看一张图,了解什么是根节点(树干).节点或分叉.叶(叶节点) connect by 级联查询 connect by可以用于级联查询,常用于对具有树 ...

  6. node多图或者单图上传

    <form id="form" enctype="multipart/form-data"> <input type="text&q ...

  7. PHP程序员的技术成长规划 第二阶段:提高阶段

    第二阶段:提高阶段 (中级PHP程序员) 重点:提高针对LNMP的技能,能够更全面的对LNMP有熟练的应用.目标:能够随时随地搭建好LNMP环境,快速完成常规配置:能够追查解决大部分遇到的开发和线上环 ...

  8. PHP中实现中文字串截取无乱码的方法

    [本文转自独占神林的日志:链接:http://yuninglovekefan.blog.sohu.com/176021361.html] 在PHP中,substr()函数截取带有中文字符串的话,可能会 ...

  9. 100-Days-Of-ML-Code 评注版(Day 3)

    Day3_Multiple_Linear_Regression(多元线性回归) 本文引用自 Multiple_Linear_Regression, 对其中内容进行了评注与补充说明. 回归分析是一种预测 ...

  10. Spark RDD理解

    目录 ----RDD简介 ----RDD操作类别 ----RDD分区 ----宽依赖和窄依赖作用 ----RDD分区划分器 ----RDD到调度 返回顶部 RDD简介 RDD是弹性分布式数据集(Res ...