排查GCC 4.4.X版本优化switch-enum的BUG
起因
一次偶然碰到一个诡异的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++ -O0 和 g++ -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做的优化...
参考
- 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
 - Guard code after switch on enum is never reached - https://stackoverflow.com/questions/8679534/guard-code-after-switch-on-enum-is-never-reached/8679627
 - Bug 41425 - switch with enums doesn't work - https://gcc.gnu.org/bugzilla/show_bug.cgi?id=41425
 - Options Controlling C++ Dialect - https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Dialect-Options.html#index-fstrict-enums
 - From Switch Statement Down to Machine Code - http://lazarenko.me/switch/
 
排查GCC 4.4.X版本优化switch-enum的BUG的更多相关文章
- gcc都做了什么优化
		
直接上程序: setjmp和longjmp是处理函数嵌套调用的,goto语句不能跨越函数,所以不选择goto. #include <setjmp.h> int setjmp(jmp_buf ...
 - ubuntu 里切换 gcc,g++ 的版本
		
https://askubuntu.com/questions/26498/choose-gcc-and-g-version https://stackoverflow.com/questions/7 ...
 - 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版本时,编 ...
 - 版本优化-test
		
版本优化 标签(空格分隔): 测试 需求经手人太多,直接提bug,开发不乐意,跟Leader确认不靠谱,跟PM确认,不熟悉流程,跟第三方PM确认靠谱了,结果被开发三言两语,变成了不改bug 而改需求 ...
 - VS编译器优化诱发一个的Bug
		
VS编译器优化诱发一个的Bug Bug的背景 我正在把某个C++下的驱动程序移植到C下,前几天发生了一个比较诡异的问题. 驱动程序有一个bug,但是这个bug只能 Win32 Release 版本下的 ...
 - 【转】C 编译器优化过程中的 Bug
		
C 编译器优化过程中的 Bug 一个朋友向我指出一个最近他们发现的 GCC 编译器优化过程(加上 -O3 选项)里的 bug,导致他们的产品出现非常诡异的行为.这使我想起以前见过的一个 GCC bug ...
 - 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 ...
 - CentOS 6.6 升级GCC G++ (当前最新版本为v6.1.0)  (完整)
		
---恢复内容开始--- CentOS 6.6 升级GCC G++ (当前最新GCC/G++版本为v6.1.0) 没有便捷方式, yum update.... yum install 或者 添加y ...
 - Unity3D游戏GC优化总结---protobuf-net无GC版本优化实践
		
protobuf-net优化效果图 protobuf-net是Unity3D游戏开发中被广泛使用的Google Protocol Buffer库的c#版本,之所以c#版本被广泛使用,是因为c++版本的 ...
 
随机推荐
- SwaggerUI用户手册
			
SwaggerUI是一个非常好用的API文档工具,最关键的是他还能在工具内调试API,简直爽的不要不要的~网上针对开发者的文档非常多,但是给用户的手册却非常少.所以我来简单写个用户手册,供没有使用过s ...
 - Oracle 11G 隐含参数“_controlfile_autobackup_delay”
			
RMAN设置控制文件自动备份,当发生数据库备份时,或建表空间,删除log文件等物理结构发生改变时,oracle会自动备份控制文件. Oracle 10g会立刻备份,Oracle 11g会有几分钟的延迟 ...
 - Web—09-正则表达式
			
正则表达式 1.什么是正则表达式: 能让计算机读懂的字符串匹配规则. 2.正则表达式的写法: var re=new RegExp('规则', '可选参数'); var re=/规则/参数; 3.规则中 ...
 - ios - 沙盒和NSBundle
			
沙盒 1.沙盒机制介绍 iOS中的沙盒机制是一种安全体系.每个iOS程序都有一个独立的文件系统(存储空间),而且只能在对应的文件系统中进行操作,此区域被称为沙盒.应用必须待在自己的沙盒里,其他应用不能 ...
 - OC中的内省(Introspection)方法
			
我们在写OC代码的时候经常用到:isKindOfClass: 一类的方法,但是对于它并没有一个了解,这里也是从网上搜索了一些内容,简单介绍并记录一下.这类方法就是属于OC的特性之一:内省. 内省(In ...
 - java获取服务器基本信息
			
实现步骤: (1)创建servlet BrowserServer (2)调用HttpServletRequest对象的getServerName()方法获取服务器名称 (3)调用HttpServlet ...
 - jquery购物车添加功能
			
<html> <head> <meta charset="UTF-8"> <title></title> <scr ...
 - ElasticSearch优化系列三:机器设置(内存)
			
heap参数设置优化 命令行修改 ./bin/elasticsearch -Xmx10g -Xms10g xmx-JVM最大允许分配的堆内存,按需分配 xms-JVM初始分配的堆内存 此值设置与-Xm ...
 - Linux C 语言之 Hello World 详解
			
目录 Linux C 语言之 Hello World 详解 第一个 C 语言程序 程序运行原理 编译,链接 运行时 链接库 编译器优化 Hello World 打印原理 stdout, stdin 和 ...
 - 【翻译】理解 LSTM 网络
			
目录 理解 LSTM 网络 递归神经网络 长期依赖性问题 LSTM 网络 LSTM 的核心想法 逐步解析 LSTM 的流程 长短期记忆的变种 结论 鸣谢 本文翻译自 Christopher Olah ...