當高階語言函式被編譯成機器碼時,有乙個問題就必須解決:因為cpu沒有辦法知道乙個函式呼叫需要多少個、什麼樣的引數。即計算機不知道怎麼給這個函式傳遞引數,傳遞引數的工作必須由函式呼叫者和函式本身來協調。為此,計算機提供了一種被稱為棧的資料結構來支援引數傳遞。
函式呼叫時,呼叫者依次把引數壓棧,然後呼叫函式,函式被呼叫以後,在堆疊中取得資料,並進行計算。函式計算結束以後,或者呼叫者、或者函式本身修改堆疊,使堆疊恢復原裝。在引數傳遞中,有兩個很重要的問題必須得到明確說明:
1) 當引數個數多於乙個時,按照什麼順序把引數壓入堆疊;
2) 函式呼叫後,由誰來把堆疊恢復原裝。
3)函式的返回值放在什麼地方
在高階語言中,通過函式呼叫規範(calling conventions)來說明這兩個問題。常見的呼叫規範有:
stdcall
cdecl
fastcall
thiscall
naked call
stdcall呼叫規範
stdcall很多時候被稱為pascal呼叫規範,因為pascal是早期很常見的一種教學用計算機程式語言,其語法嚴謹,使用的函式呼叫約定是stdcall。在microsoft c++系列的c/c++編譯器中,常常用pascal巨集來宣告這個呼叫約定,類似的巨集還有winapi和callback。
stdcall呼叫規範宣告的語法為:
int __stdcall function(int a,int b)
stdcall的呼叫約定意味著: 1)引數從右向左壓入堆疊;
2)函式自身修改堆疊;
3) 函式名自動加前導的下劃線,後面緊跟乙個@符號,其後緊跟著引數的尺寸。
以上述這個函式為例,引數b首先被壓棧,然後是引數a,函式呼叫function(1,2)呼叫處翻譯成組合語言將變成: push 2 第二個引數入棧
push 1 第乙個引數入棧
call function 呼叫引數,注意此時自動把cs:eip入棧
而對於函式自身,則可以翻譯為: push ebp 儲存ebp暫存器,該暫存器將用來儲存堆疊的棧頂指標,可以在函式退出時恢復
mov ebp,esp 儲存堆疊指標
mov eax,[ebp + 8h] 堆疊中ebp指向位置之前依次儲存有ebp,cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0ch] 堆疊中ebp + 12處儲存了b
mov esp,ebp 恢復esp
pop ebp
ret 8
而在編譯時,這個函式的名字被翻譯成_function@8
注意不同編譯器會插入自己的彙編**以提供編譯的通用性,但是大體**如此。其中在函式開始處保留esp到ebp中,在函式結束恢復是編譯器常用的方法。
從函式呼叫看,2和1依次被push進堆疊,而在函式中又通過相對於ebp(即剛進函式時的堆疊指標)的偏移量訪問引數。函式結束後,ret 8表示清理8個位元組的堆疊,函式自己恢復了堆疊。
cdecl呼叫規範
cdecl呼叫約定又稱為c呼叫約定,是c語言預設的呼叫約定,它的定義語法是:
int function (int a ,int b) // 不加修飾就是c呼叫約定
int __cdecl function(int a,int b) // 明確指出c呼叫約定
cdecl呼叫約定的引數壓棧順序是和stdcall是一樣的,引數首先由有向左壓入堆疊。所不同的是,函式本身不清理堆疊,呼叫者負責清理堆疊。由於這種變化,c呼叫約定允許函式的引數的個數是不固定的,這也是c語言的一大特色。對於前面的function函式,使用cdecl後的彙編碼變成:
呼叫處push 1
push 2
call function
add esp,8 注意:這裡呼叫者在恢復堆疊
被呼叫函式_function處
push ebp 儲存ebp暫存器,該暫存器將用來儲存堆疊的棧頂指標,可以在函式退出時恢復
mov ebp,esp 儲存堆疊指標
mov eax,[ebp + 8h] 堆疊中ebp指向位置之前依次儲存有ebp,cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0ch] 堆疊中ebp + 12處儲存了b
mov esp,ebp 恢復esp
pop ebp
ret 注意,這裡沒有修改堆疊
msdn中說,該修飾自動在函式名前加前導的下劃線,因此函式名在符號表中被記錄為_function。
由於引數按照從右向左順序壓棧,因此最開始的引數在最接近棧頂的位置,因此當採用不定個數引數時,第乙個引數在棧中的位置肯定能知道,只要不定的引數個數能夠根據第乙個後者後續的明確的引數確定下來,就可以使用不定引數,例如對於sprintf函式,定義為:
int sprintf(char* buffer,const char* format,...)
由於所有的不定引數都可以通過format確定,因此使用不定個數的引數是沒有問題的。
fastcall呼叫規範
fastcall呼叫約定和stdcall類似,它意味著: 1) 函式的第乙個和第二個dword引數(或者尺寸更小的)通過ecx和edx傳遞,其他引數通過從右向左的順序壓棧;
2) 被呼叫函式清理堆疊;
3) 函式名修改規則同stdcall。
其宣告語法為:int __fastcall function(int a,int b)
thiscall呼叫規範
thiscall是唯一乙個不能明確指明的函式修飾,因為thiscall不是關鍵字。它是c++類成員函式預設的呼叫約定。由於成員函式呼叫還有乙個this指標,因此必須特殊處理,thiscall意味著:
1) 引數從右向左入棧;
2) 如果引數個數確定,this指標通過ecx傳遞給被呼叫者;如果引數個數不確定,this指標在所有引數壓棧後被壓入堆疊;
3) 對引數個數不定的,呼叫者清理堆疊,否則函式自己清理堆疊。
為了說明這個呼叫約定,定義如下類和使用**:class a
;int a::function1 (int a,int b)
int a::function2(int a,...)
return result;
}void callee()
callee函式被翻譯成彙編後就變成: // 函式function1呼叫
0401c1d push 2
00401c1f push 1
00401c21 lea ecx,[ebp-8]
00401c24 call function1 注意,這裡this沒有被入棧
// 函式function2呼叫
00401c29 push 3
00401c2b push 2
00401c2d push 1
00401c2f push 3
00401c31 lea eax,[ebp-8] 這裡引入this指標
00401c34 push eax
00401c35 call function2
00401c3a add esp,14h
可見,對於引數個數固定情況下,它類似於stdcall,不定時則類似cdecl
naked call呼叫規範
這是乙個很少見的呼叫約定,一般程式設計者建議不要使用。編譯器不會給這種函式增加初始化和清理**,更特殊的是,不能用return返回返回值,只能用插入彙編返回結果。這一般用於實模式驅動程式設計,假設定義乙個求和的加法程式,可以定義為: __declspec(naked) int add(int a,int b)
注意,這個函式沒有顯式的return返回值,返回通過修改eax暫存器實現,而且連退出函式的ret指令都必須顯式插入。上面**被翻譯成彙編以後變成: mov eax,[ebp+8]
add eax,[ebp+12]
ret 8
注意這個修飾是和__stdcall及cdecl結合使用的,前面是它和cdecl結合使用的**,對於和stdcall結合的**,則變成: __declspec(naked) int __stdcall function(int a,int b)
至於這種函式被呼叫,則和普通的cdecl及stdcall呼叫函式一致。
函式呼叫約定導致的常見問題
如果定義的約定和使用的約定不一致,則將導致堆疊被破壞,導致嚴重問題,下面是兩種常見的問題:1) 函式原型宣告和函式體定義不一致
2) dll匯入函式時宣告了不同的函式約定
:-)
函式呼叫規範
當高階語言函式被編譯成機器碼時,有乙個問題就必須解決 因為cpu沒有辦法知道乙個函式呼叫需要多少個 什麼樣的引數。即計算機不知道怎麼給這個函式傳遞引數,傳遞引數的工作必須由函式呼叫者和函式本身來協調。為此,計算機提供了一種被稱為棧的資料結構來支援引數傳遞。函式呼叫時,呼叫者依次把引數壓棧,然後呼叫函...
函式呼叫規範
當高階語言函式被編譯成機器碼時,有乙個問題就必須解決 因為cpu沒有辦法知道乙個函式呼叫需要多少個 什麼樣的引數。即計算機不知道怎麼給這個函式傳遞引數,傳遞引數的工作必須由函式呼叫者和函式本身來協調。為此,計算機提供了一種被稱為棧的資料結構來支援引數傳遞。函式呼叫時,呼叫者依次把引數壓棧,然後呼叫函...
函式呼叫規範 cdecl和
cdecl stdcallc 和c 程式的預設呼叫規範 為了使用這種呼叫規範,需要你明確的加上 stdcall 或winapi 文字。即 return type stdcallfunction name argument list 在被呼叫函式 callee 返回後,由呼叫方 caller 調整堆疊...