函式呼叫約定和堆疊

2021-08-22 01:41:46 字數 3634 閱讀 4622

編譯器一般使用堆疊實現函式呼叫。堆疊是儲存器的乙個區域,嵌入式環境有時需要程式設計師自己定義乙個陣列作為堆疊。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為每個執行緒自動維護乙個堆疊,堆疊的大小可以設定。編譯器使用堆疊來堆放每個函式的引數 區域性變數等資訊。函式呼叫經常是巢狀的,在同一時刻,堆疊中會有多個函式的資訊,每個函式占用...