1. 程式人生 > >關於函式引數入棧的思考

關於函式引數入棧的思考

程式碼開發執行環境: VS2017+Win32+Debug

1.呼叫約定簡介

首先,要實現函式呼叫,除了要知道函式的入口地址外,還要向函式傳遞合適的引數。向被調函式傳遞引數,可以有不同的方式實現。這些方式被稱為“呼叫規範”或“呼叫約定”。C/C++中常見的呼叫規範有__cdecl、__stdcall、__fastcall和__thiscall。

1.1__cdecl呼叫約定

又稱為C呼叫約定,是C/C++預設的函式呼叫約定,它的定義語法是:

int function (int a ,int b)           // 不加修飾就是C呼叫約定
int __cdecl function
(int a,int b) // 明確指出C呼叫約定

約定的內容有:
(1)引數入棧順序是從右向左;
(2)在被呼叫函式 (Callee) 返回後,由呼叫方 (Caller)調整堆疊。

C呼叫約定允許函式的引數個數不固定,這也是C語言的一大特色,因為每個呼叫的地方都需要生成一段清理堆疊的程式碼,所以最後生成的目標檔案較__stdcall、__fastcall呼叫方式要大。

1.2__stdcall呼叫約定

又稱為標準呼叫約定,申明語法是:

int __stdcall function(int a,int b)

約定的內容有:
(1)引數從右向左壓入堆疊;
(2)函式自身清理堆疊;
(3)函式名自動加前導下劃線,後面緊跟一個@符號,其後緊跟引數的尺寸;
(4)函式引數個數不可變。

1.3__fastcall呼叫約定

又稱為快速呼叫方式。其宣告語法為:

int __fastcall function(int a,int b);

和__stdcall類似,它約定的內容有:
(1) 函式的第一個和第二個DWORD引數(或者尺寸更小的)通過ecx和edx傳遞,其他引數通過從右向左的順序壓棧;
(2)被呼叫者清理堆疊;
(3)函式名定義規則同__stdcall。

注意:不同編譯器編譯的程式規定的暫存器不同。在Intel 386平臺上,使用ECX和EDX暫存器。使用__fastcall方式無法用作跨編譯器的介面。

1.4__thiscall呼叫約定

是唯一一個不能明確指明的函式修飾符,因為__thiscall不是關鍵字。它是C++類成員函式預設的呼叫約定。由於成員函式呼叫還有一個this指標,因此必須特殊處理,__thiscall意味著:
(1) 引數從右向左入棧;
(2) 如果引數個數確定,this指標通過ecx傳遞給被呼叫者。如果引數個數不確定,this指標在所有引數壓棧後被壓入堆疊;
(3)對引數個數不定的,呼叫者清理堆疊,否則函式自己清理堆疊。

2.cout<<++i<<- -i<< i++;輸出結果的討論

在Visual C++的函式呼叫規範中,如果函式的任何一個引數表示式包含自增(自減)運算,所有這些運算會在第一個push操作之前全部完成,然後再完成其他的運算並將結果入棧。考察如下程式。

#include <iostream>
using namespace std;
int main(int argc,char* argv[])
{
    int i=10;
    cout<<++i<<--i<<i++;
    return 0;
}

按照“正常”思維,標準輸出操作符<<是從左向右結合的,所以應該依次計算表示式++i,–i和i++的值,那麼最終應該依次輸出11,10,和10。但是在Visual C++中執行結果是11,11和10。考察此程式的彙編程式碼,發現語句cout<<++i<<--i<<i++;所對應的彙編程式碼是:

00EF6ED5  mov         eax,dword ptr [i]  
00EF6ED8  mov         dword ptr [ebp-0D0h],eax  //儲存i的值

00EF6EDE  mov         ecx,dword ptr [i]  
00EF6EE1  add         ecx,1                     //變數i自增1
00EF6EE4  mov         dword ptr [i],ecx  

00EF6EE7  mov         edx,dword ptr [i]  
00EF6EEA  sub         edx,1                     //變數i自減1
00EF6EED  mov         dword ptr [i],edx         

00EF6EF0  mov         eax,dword ptr [i]  
00EF6EF3  add         eax,1                     //變數i自增1
00EF6EF6  mov         dword ptr [i],eax  

00EF6EF9  mov         esi,esp  
00EF6EFB  mov         ecx,dword ptr [ebp-0D0h]  
00EF6F01  push        ecx                       //將儲存的數值10入棧

00EF6F02  mov         edi,esp  
00EF6F04  mov         edx,dword ptr [i]  
00EF6F07  push        edx                       //將變數i入棧

00EF6F08  mov         ebx,esp  
00EF6F0A  mov         eax,dword ptr [i]  
00EF6F0D  push        eax                       //將變數i入棧

//獲取cout物件地址,this指標通過ecx傳遞
00EF6F0E  mov         ecx,dword ptr [[email protected]std@@3[email protected][email protected]@std@@@1@A (0F002E0h)]

//cout<<++i;
00EF6F14  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0F002E8h)]
00EF6F1A  cmp         ebx,esp
00EF6F1C  call        __RTC_CheckEsp (0EF12DFh)

//cout<<--i;
00EF6F21  mov         ecx,eax  
00EF6F23  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0F002E8h)]
00EF6F29  cmp         edi,esp  
00EF6F2B  call        __RTC_CheckEsp (0EF12DFh)  

//cout<<i++;
00EF6F30  mov         ecx,eax  
00EF6F32  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0F002E8h)]
00EF6F38  cmp         esi,esp  
00EF6F3A  call        __RTC_CheckEsp (0EF12DFh)  

這段彙編程式碼比較複雜,先解釋關鍵的地方。首先,雖然<<運算子是從左向右結合,但在<<運算子構成的鏈式操作中,各表示式的入棧順序還是從右向左,只有這樣才能實現<<運算從左向右進行。所以,先計算的是表示式i++的值。因為i自增之後無法提供入棧的值,所以另外開闢了一個記憶體單元dword ptr [ebp-0D0h]來存放第一個入棧的表示式的值。

接著計算--i的值,自減運算完成之後,編譯器認為i的值可以直接作為引數入棧,所以並沒有開闢別的記憶體單元存放這一個入棧引數的值。

再接下來計算++i情形跟計算- -i類似。這些操作完成之後,分別將dword ptr [ebp-0D0h]處的值、最終的i和i入棧。再三次呼叫cout.operator<<函式將它們輸出。所以程式的最終結果是11,11,10。

彙編程式碼中cmp ebx,espcall __RTC_CheckEsp (0EF12DFh)表示VC編譯器提供了執行時刻對程式正確性/安全性的一種動態檢查,可以在專案屬性的C++選項中開啟來啟用Runtime Check。開啟與開啟步驟如下圖:
這裡寫圖片描述

在程式中cout.operator<<執行完後,會將物件cout的地址存放在暫存器eax中作為該函式的返回值。由於在Visual C++中,呼叫物件的成員函式之前會先將物件的地址存放在暫存器ecx中,所以在下一次呼叫cout.operator<<之前,會先將eax的值送入ecx中。

如果生成Release版本,發現輸出結果變成10,10和10。這是編譯器對程式碼所做的優化導致的結果。

從上面的程式中,我們可以看出,自增(自減)運算雖然可以使表示式更為緊湊,但很容易帶來副作用。過分追求小的技巧正式很多程式缺陷的緣由,應該編寫哪些可讀性較好的程式碼,避免那些看似簡單但蘊藏危機的表示式。

假設i的值是10,執行語句i=i++;之後,i的值是多少呢?其實,這樣的程式碼在不同的編譯器中有著不同的實現,輸出結果是不一樣的,所以,編寫這樣的程式碼沒有什麼意思,且儘量避免。

參考文獻

[1]陳剛.C++高階進階教程[M].武漢:武漢大學出版社,2008.
[2]百度百科.__stdcall
[3]百度百科.__cdecl