内容简介:一次偶然碰到一个诡异的bug,现象是同一份C++代码使用示例代码如下:将这段代码分别使用
一次偶然碰到一个诡异的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 4.4.X版本优化switch-enum的BUG》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。