排查GCC 4.4.X版本优化switch-enum的BUG

栏目: 服务器 · 编程工具 · 发布时间: 5年前

内容简介:一次偶然碰到一个诡异的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++ -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 4.4.X版本优化switch-enum的BUG》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

第二曲线:跨越“S型曲线”的二次增长

第二曲线:跨越“S型曲线”的二次增长

[英]查尔斯·汉迪(Charles Handy) / 苗青 / 机械工业出版社 / 2017-6 / 49.00

S型曲线是每个组织和企业在预测未来时一定会参考的工具,一切事物的发展都逃不开S型曲线(“第一曲线”)。 然而,从公司组织、企业治理、市场的变化,到个人职业发展、社会人际关系以及未来的教育与社会价值,多维度地探讨这个世界需要重新以不同的角度来思考问题,不能够总是停留在“第一曲线”的世界。 如果组织和企业能在第一曲线到达巅峰之前,找到带领企业二次腾飞的“第二曲线”,并且第二曲线必须在第一曲......一起来看看 《第二曲线:跨越“S型曲线”的二次增长》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具