排查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優化掉了,在網上找到一篇帖子
於是只能去看匯編代碼了,事實證明這才是最有效的方式,比自己瞎猜要節省時間。
可以通過調試時使用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/
原文地址:https://lidawn.github.io/2018/09/02/gcc-bug/
排查GCC 4.4.X版本優化switch-enum的BUG