本文最先發佈於個人知乎回答:https://www.zhihu.com/questio...函數
今天恰好有學弟學妹來問我相似的問題,就藉着這個問題回答一下:
基本環境:Linux下的gcc和clang(沒看版本,應該是最新)工具
先附上源程序調試
#include <stdio.h> int main(int argc, char *argv[]) { int sum,i=2; sum=(++i)+(++i)+(++i)+(++i); printf("%d %d\n",sum,i); return 0; }
是和題主同樣的問題,使用gcc編譯該程序:code
gcc -g -o test-gcc test.c
獲得可執行程序test-gcc,執行後輸出orm
19 6
使用clang編譯該程序:ip
clang -g -o test-clang test.c
clang提示警告:內存
test.c:4:7: warning: multiple unsequenced modifications to 'i' [-Wunsequenced] sum=(++i)+(++i)+(++i)+(++i); ^ ~~ 1 warning generated.
獲得可執行程序test-clang,執行後輸出get
18 6
看完了現象,那麼本質緣由如何呢?咱們藉助IDA逆向分析工具來觀察test-gcc和test-clang這兩個可執行程序,我使用的是IDA Pro 7.0的macOS版本,還不知道IDA Pro是什麼的同窗能夠百度搜一搜,會獲得答案。編譯器
咱們首先分析「結果正常」的test-clang,將test-clang導入進IDA Pro 7.0 64-bit,定位到關鍵彙編代碼(我添加了註釋):it
;子程序開始(主函數開始) main proc near ;定義了四個雙字變量,由於是64位系統,因此這些變量都是8個字節的 var_10= dword ptr -10h var_C= dword ptr -0Ch var_8= dword ptr -8 var_4= dword ptr -4 ;程序初始化 push rbp mov rbp,rsp sub rsp,10h ;進行printf的格式化參數初始化,能夠忽略 mov rdi,offset format ; "%d %d\n" ;將var_4變量賦值爲0,var_C變量賦值爲2 mov [rbp+var_4],0 mov [rbp+var_C],2 ;將var_C加1,這裏要藉助寄存器eax來加 ;eax中的值如今是3,var_C=3 mov eax,[rbp+var_C] add eax,1 mov [rbp+var_C],eax ;將var_C再加1,藉助了另外一個寄存器ecx來加 ;ecx中的值如今是4,var_C=4 mov ecx,[rbp+var_C] add ecx,1 mov [rbp+var_C],ecx ;eax和ecx如今相加了,結果送入eax ;eax中的值爲3+4=7 add eax,ecx ;將var_C再加1,藉助了寄存器ecx來加 ;ecx中的值如今爲5,var_C=5 mov ecx,[rbp+var_C] add ecx,1 mov [rbp+var_C],ecx ;再將上面用到的eax加上了ecx ;如今eax中的值爲7+5=12 add eax,ecx ;將var_C再加1,藉助了寄存器ecx來加 ;ecx中的值如今爲6,var_C=6 mov ecx,[rbp+var_C] add ecx,1 mov [rbp+var_C],ecx ;再將上面用到的eax加上了ecx ;如今eax中的值爲12+6=18 add eax,ecx ;將上面eax中的18送入變量var_8 mov [rbp+var_8],eax ;輸出var_8和var_C,分別爲18 6 mov esi,[rbp+var_8] mov edx,[rbp+var_C] mov al,0 call _printf ;用於函數返回 xor ecx,ecx mov [rbp+var_10],eax mov eax,ecx add rsp,10h pop rbp retn main endp
這裏面clang把咱們的C語言代碼按題主手算的方法來編譯爲了彙編代碼,我這裏所說的變量var_C等,實際訪問的時候是使用的[rbp+var_C],這個是彙編中的尋址方式(基址寄存器+偏移量),若是不懂的話能夠略過,就理解爲變量var_C便可。
咱們將這段彙編代碼,按照彙編流程的思惟,轉化爲C語言代碼:
#include <stdio.h> int main(int argc, char *argv[]) { int var_4=0,var_C=2,var_8,eax,ecx; eax=++var_C; ecx=++var_C; eax+=ecx; ecx=++var_C; eax+=ecx; ecx=++var_C; eax+=ecx; var_8=eax; printf("%d %d",var_8,var_C); return 0; }
結果顯然是
18 6
分析完了test-clang,咱們再按照一樣的方式分析一下test-gcc,就會發現狀況有所不一樣:
;子程序開始(主函數開始) main proc near ;定義兩個變量,由於使用了-g附加調試信息 ;因此IDA分析出就是咱們源程序中的sum和i變量 sum= dword ptr -8 i= dword ptr -4 ;程序初始化 push rbp mov rbp,rsp sub rsp,10h ;設置變量i的值爲2 mov [rbp+i],2 ;變量i連續加了兩次1,執行後i=4 add [rbp+i],1 add [rbp+i],1 ;將變量i的值送入eax,eax爲4 mov eax,[rbp+i] ;將eax+eax的所在內存內容的地址送入edx ;這句話就至關於edx=(eax+eax),edx=8 ;只是編譯以後變成了複雜的寫法 lea edx,[eax+eax] ;變量i繼續加1,執行後i=5 add [rbp+i],1 ;將變量i的值送入eax,eax=5 mov eax,[rbp+i] ;eax+edx的值放入edx,edx=8+5=13 add edx,eax ;變量i繼續加1,執行後i=6 add [rbp+i],1 ;將變量i的值送入eax,eax=6 mov eax,[rbp+i] ;eax+edx的值放入eax,eax=6+13=19 add eax,edx ;將eax的值送入變量sum mov [rbp+sum],eax ;調用printf輸出sum和i,分別爲19 6 mov edx,[rbp+i] mov eax,[rbp+sum] mov esi,eax mov edi,offset format ; "%d %d\n" mov eax,0 call _printf ;程序返回 mov eax,0 leave retn main endp
能夠看出來gcc的編譯思路比較清奇,因此才致使了意想不到的結果,咱們一樣將彙編語言的代碼,按照彙編語言的思惟,轉換爲C語言代碼:
#include <stdio.h> int main(int argc, char *argv[]) { int sum,i,eax,edx; i=2; ++i; ++i; eax=i; edx=eax+eax; eax=++i; edx+=eax; eax=++i; eax+=edx; sum=eax; printf("%d %d",sum,i); return 0; }
結果顯然是:
19 6
至此咱們找到了不一樣編譯器運行結果不一樣的緣由,是由於gcc和clang在編譯這同一段C語言代碼的時候,把他們按照不一樣的思路轉化爲了彙編代碼,因此執行結果纔不一樣,顯然clang的轉化方式更能符合正常人的思惟,因此如今更推薦使用clang,用clang來代替gcc。