18 {19     callFunc((funcPointer)testFunc);20     getchar();21 }

萬碼學(xué)堂,電腦培訓(xùn),計算機培訓(xùn),Java培訓(xùn),JavaEE開發(fā)培訓(xùn),青島軟件培訓(xùn),軟件工程師培訓(xùn)

在第12行代碼定義的callFunc函數(shù),它的參數(shù)是一個“返回值為void,參數(shù)為一個int型的函數(shù)指針”,并在內(nèi)部調(diào)用這個函數(shù)指針傳實參為1。

在地5行代碼定義了函數(shù)testFunc,它的參數(shù)為兩個int,同時為它定義了__stdcall的調(diào)用約定。

在main函數(shù)的19行中進行調(diào)用的時候,對testFunc使用了(funcPointer)進行強行類型轉(zhuǎn)換,并將它傳入callFunc作為實參進行調(diào)用。

 

x86平臺Debug版本運行這段程序的結(jié)果如下:

萬碼學(xué)堂,電腦培訓(xùn),計算機培訓(xùn),Java培訓(xùn),JavaEE開發(fā)培訓(xùn),青島軟件培訓(xùn),軟件工程師培訓(xùn)

程序因為異常停在第19行了。

 

X86平臺Release版本運行這段程序的結(jié)果如下:

萬碼學(xué)堂,電腦培訓(xùn),計算機培訓(xùn),Java培訓(xùn),JavaEE開發(fā)培訓(xùn),青島軟件培訓(xùn),軟件工程師培訓(xùn)

萬碼學(xué)堂,電腦培訓(xùn),計算機培訓(xùn),Java培訓(xùn),JavaEE開發(fā)培訓(xùn),青島軟件培訓(xùn),軟件工程師培訓(xùn)

程序雖然也執(zhí)行到結(jié)尾了,但是由于傳參不正確,所以結(jié)果不對,而且也無法正常停機。

 

X64平臺Debug版本和Release版本運行這段程序的結(jié)果類似如下:

萬碼學(xué)堂,電腦培訓(xùn),計算機培訓(xùn),Java培訓(xùn),JavaEE開發(fā)培訓(xùn),青島軟件培訓(xùn),軟件工程師培訓(xùn)

程序沒有發(fā)生異常,順利執(zhí)行完畢,只是運行結(jié)果不正確。

 

為什么是這樣一個結(jié)果呢?下面,本文就來細細講解。

在計算機中有兩個寄存器稱為ebpesp,它們分別稱為基址指針寄存器和堆棧指針寄存器。

esp和ebp分別指向當前運行函數(shù)的棧頂和棧底。

由于callFunc是以cdecl的方式進行聲明的,而testFunc是以stdcall的方式進行聲明的。因此在callFunc調(diào)用testFunc時,調(diào)用者(caller)負責將參數(shù)push進棧。因為此時testFunc已經(jīng)進行了強行類型轉(zhuǎn)換,因此編譯器認為它的輸入?yún)?shù)即為1個int,所以在入棧時callFunc將1個int壓入堆棧中,接著調(diào)用testFunc。當testFunc執(zhí)行完畢之后,由于它是stdcall所以由被調(diào)用者(callee)即testFunc自身負責參數(shù)的pop退棧。而此時,由于testFunc函數(shù)本身只有2個int型參數(shù),所以在出棧時即pop兩個int,導(dǎo)致了棧不平衡問題的產(chǎn)生?。ǘ以趫?zhí)行完testFunc之后,由于callFunc是cdecl類型的所以它仍然會再進行退棧的操作)如下圖所示。

萬碼學(xué)堂,電腦培訓(xùn),計算機培訓(xùn),Java培訓(xùn),JavaEE開發(fā)培訓(xùn),青島軟件培訓(xùn),軟件工程師培訓(xùn)

 此處,截取了實際程序的反匯編代碼進行分析:

萬碼學(xué)堂,電腦培訓(xùn),計算機培訓(xùn),Java培訓(xùn),JavaEE開發(fā)培訓(xùn),青島軟件培訓(xùn),軟件工程師培訓(xùn)

上圖是testFunc的反匯編,為了使反匯編看起來沒那么冗長作者將其中一些代碼注釋掉了??梢钥吹?,最后在結(jié)束時進行了ret 8的操作,即向上退8(兩個int的大?。?。(此處可以看到stdcall聲明的函數(shù)進行自行參數(shù)退棧的實現(xiàn))

萬碼學(xué)堂,電腦培訓(xùn),計算機培訓(xùn),Java培訓(xùn),JavaEE開發(fā)培訓(xùn),青島軟件培訓(xùn),軟件工程師培訓(xùn)

上圖是callFunc的反匯編,可以看到在調(diào)用子函數(shù)結(jié)束之后它進行了esp+4的操作,即退棧1個int(因為棧的地址空間是從大向小增長的所以是加操作)。

而且最后在它ret時是沒有跟參數(shù)的,代表cdecl的函數(shù)不進行自我參數(shù)退棧操作。

 

關(guān)于Debug和Release,X86和X64結(jié)果不一樣的原因

①在Debug版本下,Visual Studio的編譯器會自動在編譯參數(shù)中加入/RTC,即Runtime Check。啟用運行時錯誤檢查。其中包括了:堆棧指針驗證,該操作檢測堆棧指針損壞。 調(diào)用約定不匹配可能導(dǎo)致堆棧指針損壞。 例如,使用函數(shù)指針調(diào)用 DLL 中作為 __stdcall 導(dǎo)出的函數(shù),但將指向該函數(shù)的指針聲明為 __cdecl。此時編譯器會在每個函數(shù)的開始和結(jié)束處加入針對esp指針的檢查。詳見:MSDN_CL_編譯參數(shù)_RTC。

因此在Debug版本下會報出上文所提到的異常。而在Release版本下,因為默認不進行太多檢查即RTC被關(guān)閉,因此并不會出現(xiàn)彈出異常提示的情況。

Debug版反匯編代碼如下:

萬碼學(xué)堂,電腦培訓(xùn),計算機培訓(xùn),Java培訓(xùn),JavaEE開發(fā)培訓(xùn),青島軟件培訓(xùn),軟件工程師培訓(xùn)

可以從反匯編的代碼中看到,在進入子函數(shù)之前先將esp的值保存在esi中,當執(zhí)行完畢之后對比esi和現(xiàn)在的esp的值,即RTC。

②在X86版本下,在退棧時是以esp中的值為基址進行加減操作來進行的。而RTC又是對esp指針進行檢查,因此此時會報出異常。

而在X64版本下,在退棧時是以ebp中的值為基址進行加減操作來進行的,RTC檢查的是esp,毫不相關(guān),所以不會抱任何異常。

 

誠然,這只是一個小“缺陷”,很多人認為不必在意。但是小小的問題也會在某一刻產(chǎn)生巨大的隱患,造成整個軟件的崩潰。

http://www.cnblogs.com/saintlas/p/7093561.html