1. 程式人生 > >排查GCC 4.4.X版本優化switch-enum的BUG

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

大於 官方 each lock 處理 eat ... overflow default

起因

一次偶然碰到一個詭異的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