c/c++语言开发共享排查GCC 4.4.X版本优化switch-enum的BUG

起因 一次偶然碰到一个诡异的bug,现象是同一份C++代码使用 GCC4.4.x 版本在开启优化前和优化后的结果不一样,优化后的代码逻辑不正确。 示例代码如下: <! more 将这段代码分别使用 和 编译,结果让人诧异,在tt=4的时候,switch却跳到了1的分支。 排查过程 考虑到是有enum …


起因

一次偶然碰到一个诡异的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 = %dn", tt);             break;         case err_b:             printf("case err_b, tt = %dn", tt);             break;         case err_c:             printf("case err_c, tt = %dn", tt);             break;         default:             printf("case default, tt = %dn", 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 = %dn", tt);    0x00000000004005c0 <+28>:    mov    -0x4(%rbp),%eax  ...  14             case err_b: 15                  printf("case err_b, tt = %dn", tt);    0x00000000004005d6 <+50>:    mov    -0x4(%rbp),%eax  ...  17            case err_c: 18                  printf("case err_c, tt = %dn", tt);    0x00000000004005ec <+72>:    mov    -0x4(%rbp),%eax  ...  20              default: 21                  printf("case default, tt = %dn", 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 = %dn", tt);    0x00000000004005b5 <+17>:    mov    $0xffffffff,%esi  ...  14             case err_b: 15                  printf("case err_b, tt = %dn", tt);    0x00000000004005cb <+39>:    mov    $0x0,%esi  ...  17            case err_c: 18                  printf("case err_c, tt = %dn", tt);    0x00000000004005e1 <+61>:    mov    $0x1,%esi  ...  20              default: 21                  printf("case default, tt = %dn", 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版本被反馈过的,尽管这个优化很不合理,但依然被作为一个“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++? –
  2. guard code after switch on enum is never reached –
  3. bug 41425 – switch with enums doesn’t work –
  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 –

原文地址:

本文来自网络收集,不代表计算机技术网立场,如涉及侵权请点击右边联系管理员删除。

如若转载,请注明出处:https://www.ctvol.com/c-cdevelopment/608156.html

(0)
上一篇 2021年5月15日
下一篇 2021年5月15日

精彩推荐