編譯器一般使用堆疊實現函式呼叫。堆疊是儲存器的乙個區域,嵌入式環境有時需要程式設計師自己定義乙個陣列作為堆疊。windows為每個執行緒自動維護乙個堆疊,堆疊的大小可以設定。編譯器使用堆疊來堆放每個函式的引數、區域性變數等資訊。
函式呼叫經常是巢狀的,在同一時刻,堆疊中會有多個函式的資訊,每個函式占用乙個連續的區域。乙個函式占用的區域被稱作幀(frame)。
編譯器從高位址開始使用堆疊。 假設我們定義乙個陣列a[1024]作為堆疊空間,一開始棧頂指標指向a[1023]。如果棧裡有兩個函式a和b,且a呼叫了b,棧頂指標會指向函式b的幀。如果函式b返回。棧頂指標就指向函式a的幀。如果在棧裡放了太多東西造成溢位,破壞的是a[0]上面的東西。
在多執行緒(任務)環境,cpu的堆疊指標指向的儲存器區域就是當前使用的堆疊。切換執行緒的乙個重要工作,就是將堆疊指標設為當前執行緒的堆疊棧頂位址。
不同cpu,不同編譯器的堆疊布局、函式呼叫方法都可能不同,但堆疊的基本概念是一樣的。
函式呼叫約定包括傳遞引數的順序,誰負責清理引數占用的堆疊等,例如 :
引數傳遞順序
誰負責清理引數占用的堆疊
__pascal
從左到右
呼叫者__stdcall
從右到左
被調函式
__cdecl
從右到左
呼叫者呼叫函式的**和被調函式必須採用相同的函式的呼叫約定,程式才能正常執行。在windows上,__cdecl是c/c++程式的預設函式呼叫約定。
在有的cpu上,編譯器會用暫存器傳遞引數,函式使用的堆疊由被調函式分配和釋放。這種呼叫約定在行為上和__cdecl有乙個共同點:實參和形引數目不符不會導致堆疊錯誤。
不過,即使用暫存器傳遞引數,編譯器在進入函式時,還是會將暫存器裡的引數存入堆疊指定位置。引數和區域性變數一樣應該在堆疊中有一席之地。引數可以被理解為由呼叫函式指定初值的區域性變數。
不同的cpu,不同的編譯器,堆疊的布局可能是不同的。本文以x86,vc++的編譯器為例。
vc++編譯器的已經不再支援__pascal, __fortran, __syscall等函式呼叫約定。目前只支援__cdecl和__stdcall。
採用__cdecl或__stdcall呼叫方式的程式,在剛進入子函式時,堆疊內容是一樣的。esp指向的棧頂是返回位址。這是被call指令壓入堆疊的。下面是引數,左邊引數在上,右邊引數在下(先入棧)。
如前表所示,__cdecl和__stdcall的區別是:__cdecl是呼叫者清理引數占用的堆疊,__stdcall是被調函式清理引數占用的堆疊。
由於__stdcall的被調函式在編譯時就必須知道傳入引數的準確數目(被調函式要清理堆疊),所以不能支援變引數函式,例如printf。而且如果呼叫者使用了不正確的引數數目,會導致堆疊錯誤。
通過檢視彙編**,__cdecl函式呼叫在call語句後會有乙個堆疊調整語句,例如:
a = 0x1234;
b = 0x5678;
c = add(a, b);
對應x86彙編:
mov dword ptr [ebp-4],1234h
mov dword ptr [ebp-8],5678h
mov eax,dword ptr [ebp-8]
push eax
mov ecx,dword ptr [ebp-4]
push ecx
call 0040100a
add esp,8
mov dword ptr [ebp-0ch],eax
__stdcall的函式呼叫則不需要調整堆疊:
call 00401005
mov dword ptr [ebp-0ch],eax
函式
int __cdecl add(int a, int b)
產生以下彙編**(debug版本):
push ebp
mov ebp,esp
sub esp,40h
push ebx
push esi
push edi
lea edi,[ebp-40h]
mov ecx,10h
mov eax,0cccccccch
rep stos dword ptr [edi]
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0ch]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret // 跳轉到esp所指位址,並將esp+4,使esp指向進入函式時的第乙個引數
再檢視__stdcall函式的實現,會發現與__cdecl函式只有最後一行不同:
ret 8 // 執行ret並清理引數占用的堆疊
ta = (tadd)add; // tadd定義:typedef int (__cdecl *tadd)(int a, int b);
c = ta(a, b);
產生以下彙編**:
mov [ebp-10h],0040100a
mov esi,esp
mov ecx,dword ptr [ebp-8]
push ecx
mov edx,dword ptr [ebp-4]
push edx
call dword ptr [ebp-10h]
add esp,8
cmp esi,esp
call __chkesp (004011e0)
mov dword ptr [ebp-0ch],eax
__chkesp **如下。如果esp不等於函式呼叫前儲存的值,就會轉到錯誤處理**。
004011e0 jne __chkesp+3 (004011e3)
004011e2 ret
004011e3 ;錯誤處理**
__chkesp的錯誤處理會彈出對話方塊,報告函式呼叫造成esp值不正確。 release版本的彙編**要簡潔得多。也不會增加 __chkesp。如果發生esp錯誤,程式會繼續執行,直到「遇到問題需要關閉」。
4補充說明
函式呼叫約定只是「呼叫函式的**」和被呼叫函式之間的關係。
假設函式a是__stdcall,函式b呼叫函式a。你必須通過函式宣告告訴編譯器,函式a是__stdcall。編譯器自然會產生正確的呼叫**。
如果函式a是__stdcall。但在引用函式a的地方,你卻告訴編譯器,函式a是__cdecl方式,編譯器產生__cdecl方式的**,與函式a的呼叫約定不一致,就會發生錯誤。
以delphi呼叫vc函式為例,delphi的函式預設採用__pascal約定,vc的函式預設採用__cdecl約定。我們一般將vc的函式設為__stdcall,例如:
int __stdcall add(int a, int b);
在delphi中將這個函式也宣告為__stdcall,就可以呼叫了:
function add(a: integer; b: integer): integer;
stdcall; external 'a.dll';
因為考慮到可能被其它語言的程式呼叫,不少api採用__stdcall的呼叫約定。
函式呼叫約定和堆疊
編譯器一般使用堆疊實現函式呼叫。堆疊是儲存器的乙個區域,嵌入式環境有時需要程式設計師自己定義乙個陣列作為堆疊。windows為每個執行緒自動維護乙個堆疊,堆疊的大小可以設定。編譯器使用堆疊來堆放每個函式的引數 區域性變數等資訊。函式呼叫經常是巢狀的,在同一時刻,堆疊中會有多個函式的資訊,每個函式占用...
函式呼叫約定和堆疊
編譯器一般使用堆疊實現函式呼叫。堆疊是儲存器的乙個區域,嵌入式環境有時需要程式設計師自己定義乙個陣列作為堆疊。windows為每個執行緒自動維護乙個堆疊,堆疊的大小可以設定。編譯器使用堆疊來堆放每個函式的引數 區域性變數等資訊。函式呼叫經常是巢狀的,在同一時刻,堆疊中會有多個函式的資訊,每個函式占用...
函式呼叫約定和堆疊
編譯器一般使用堆疊實現函式呼叫。堆疊是儲存器的乙個區域,嵌入式環境有時需要程式設計師自己定義乙個陣列作為堆疊。windows為每個執行緒自動維護乙個堆疊,堆疊的大小可以設定。編譯器使用堆疊來堆放每個函式的引數 區域性變數等資訊。函式呼叫經常是巢狀的,在同一時刻,堆疊中會有多個函式的資訊,每個函式占用...