1. 程式人生 > >從棧不平衡問題 理解 calling convention

從棧不平衡問題 理解 calling convention

bug bsp iostream 增長 eight 輸入參數 borde 負責 出棧

最近在開發的過程中遇到了幾個很詭異的問題,造成了棧不平衡從而導致程序崩潰。

經過幾經排查發現是和調用規約(calling convention)相關的問題,特此分享出來。

首先,講一下什麽是調用規約

函數調用規約,是指當一個函數被調用時,函數的參數會被傳遞給被調用的函數和返回值會被返回給調用函數。函數的調用規約就是描述參數是怎麽傳遞由誰平衡堆棧的,當然還有返回值。

名稱 誰負責參數出棧 參數壓棧順序
Cdecl Caller(調用者) 從右往左
Pascal Callee(被調用者) 從左往右
Stdcall  Callee(被調用者) 從右往左
Fastcall Callee(被調用者) 從右往左
Thiscall Callee(被調用者) 從右往左

下面,本文給出一個簡短的代碼示例:

 1 #include<iostream>
 2 
 3 typedef void(*funcPointer)(int);
 4 
 5 void __stdcall testFunc(int i, int j)
 6 {
 7     std::cout << "i is:" << i << std::endl;
 8     std::cout << "j is:" << j << std::endl;
9 return ; 10 } 11 12 void callFunc(void(*func)(int)) 13 { 14 func(1); 15 } 16 17 void main() 18 { 19 callFunc((funcPointer)testFunc); 20 getchar(); 21 }

在第12行代碼定義的callFunc函數,它的參數是一個“返回值為void,參數為一個int型的函數指針”,並在內部調用這個函數指針傳實參為1。

在地5行代碼定義了函數testFunc,它的參數為兩個int,同時為它定義了__stdcall的調用約定。

在main函數的19行中進行調用的時候,對testFunc使用了(funcPointer)進行強行類型轉換,並將它傳入callFunc作為實參進行調用。

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

技術分享

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

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

技術分享

技術分享

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

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

技術分享

程序沒有發生異常,順利執行完畢,只是運行結果不正確。

為什麽是這樣一個結果呢?下面,本文就來細細講解。

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

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

由於callFunc是以cdecl的方式進行聲明的,而testFunc是以stdcall的方式進行聲明的。因此在callFunc調用testFunc時,調用者(caller)負責將參數push進棧。因為此時testFunc已經進行了強行類型轉換,因此編譯器認為它的輸入參數即為1個int,所以在入棧時callFunc將1個int壓入堆棧中,接著調用testFunc。當testFunc執行完畢之後,由於它是stdcall所以由被調用者(callee)即testFunc自身負責參數的pop退棧。而此時,由於testFunc函數本身只有2個int型參數,所以在出棧時即pop兩個int,導致了棧不平衡問題的產生!(而且在執行完testFunc之後,由於callFunc是cdecl類型的所以它仍然會再進行退棧的操作)如下圖所示。

技術分享

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

技術分享

上圖是testFunc的反匯編,為了使反匯編看起來沒那麽冗長作者將其中一些代碼註釋掉了。可以看到,最後在結束時進行了ret 8的操作,即向上退8(兩個int的大小)。(此處可以看到stdcall聲明的函數進行自行參數退棧的實現)

技術分享

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

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

關於Debug和Release,X86和X64結果不一樣的原因

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

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

Debug版反匯編代碼如下:

技術分享

可以從反匯編的代碼中看到,在進入子函數之前先將esp的值保存在esi中,當執行完畢之後對比esi和現在的esp的值,即RTC。

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

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

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

從棧不平衡問題 理解 calling convention