gcc和clang編譯器處理前置自增表達式的區別

本文最先發佈於個人知乎回答: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。

相關文章
相關標籤/搜索