C++ 反彙編:關於函數調用約定
函數是任何一門高級語言中必須要存在的,使用函數式編程可以讓程式可讀性更高,充分發揮了模組化設計思想的精髓,今天我將帶大家一起來探索函數的實現機理,探索編譯器到底是如何對函數這個關鍵字進行實現的,並使用彙編語言模擬實現函數編程中的參數傳遞調用規範等。
說到函數我們必須要提起調用約定這個名詞,而調用約定離不開棧的支援,棧在記憶體中是一塊特殊的存儲空間,遵循先進後出原則,使用push與pop指令對棧空間執行數據壓入和彈出操作。棧結構在記憶體中佔用一段連續存儲空間,通過esp與ebp這兩個棧指針暫存器來保存當前棧起始地址與結束地址,每4個位元組保存一個數據。
一般編譯器實現調用調用約定無外乎以下這幾種:
- CDECL:C/C++默認的調用約定,調用方平棧,不定參數的函數可以使用,參數通過堆棧傳遞.
- STDCALL:被調方平棧,不定參數的函數無法使用,參數默認全部通過堆棧傳遞.
- FASTCALL32:被調方平棧,不定參數的函數無法使用,前兩個參數放入(ECX, EDX),剩下的參數壓棧保存.
- FASTCALL64:被調方平棧,不定參數的函數無法使用,前四個參數放入(RCX, RDX, R8, R9),剩下的參數壓棧保存.
- System V:類Linux系統默認約定,前八個參數放入(RDI,RSI, RDX, RCX, R8, R9),剩下的參數壓棧保存.
當棧頂指針esp小於棧底指針ebp時,就形成了棧幀,棧幀中可以定址的數據有局部變數,函數返回地址,函數參數等。不同的兩次函數調用,所形成的棧幀也不相同,當由一個函數進入另一個函數時,就會針對調用的函數開闢出其所需的棧空間,形成此函數的獨有棧幀,而當調用結束時,則清除掉它所使用的棧空間,關閉棧幀,該過程通俗的講叫做棧平衡。而如果棧在使用結束後沒有恢復或過度恢復,則會造成棧的上溢或下溢,給程式帶來致命錯誤。
cdecl 調用者平棧: cdecl是C/C++默認調用約定,該調用方式在函數內不進行任何平衡參數操作,而是在退出函數後對esp執行加4操作,從而實現棧平衡。
該約定會採用複寫傳播優化,將每次參數平衡的操作進行歸併,在函數結束後一次性平衡棧頂指針esp,且不定參數函數可使用此約定。
stdcall 被調用者平棧: stdcall與cdecl只在參數平衡上有所不同,其餘部分都一樣,但該約定不定參數函數無法使用。
cdecl調用方式的函數在同一作用域內多次被調用,會在效率上比stdcall高一些,因為它可以使用複寫傳播優化,而stdcall在函數內平衡棧,無法使用複寫傳播優化。
fastcall 被調用者平棧: fastcall效率最高,它可利用暫存器傳遞參數,一般前兩個或前四個參數用暫存器傳遞,其餘參數傳遞則轉換為棧傳遞,此約定不定參數函數無法使用。
對於32位來說使用ecx,edx傳遞前兩個參數,後面的用堆棧傳遞。
對於64位則會使用RCX,RDX,R8,R9傳遞前四個參數,後面的用堆棧傳遞。
使用esp定址: 在O2編譯器選項中,為了提高程式執行效率,只要棧頂是穩定的,就可以不再使用ebp指針,而是利用esp指針直接訪問局部變數,這樣可節省一個暫存器資源。
如下一段彙編程式碼,我們找到當前ESP基地址。
可以看到,esp+18就是第一個傳入參數,那麼程式在編譯時,其實已經算出來了。
使用esp定址後,不必每次進入函數後都調整棧底ebp,從而減少了ebp的使用,因此可以有效提升程式執行效率。
但每次訪問都需要計算,如果在函數執行過程中esp發生了變化,再次訪問變數就需要重新計算偏移了。
參考文獻:《C++反彙編與逆向分析技術揭秘》