1. 程式人生 > >函式的堆疊呼叫

函式的堆疊呼叫

讓我們帶著問題來閱讀本篇文章

  1. 型參在哪裡開闢記憶體?
  2. 型參的入棧順序?
  3. 函式返回值怎麼帶出來?
  4. 函式的返回值為什麼會回退到棧裡?
  5. 函式呼叫結束為什麼會沿著呼叫點繼續執行?

我們先來了解一下堆與棧是怎樣的一種存在

什麼是棧?

棧用於維護函式呼叫的上下文,離開棧,函式就沒有辦法實現。棧通常在使用者空間的最高地址處分配,通常有數兆位元組大小。

棧在程式執行中具有舉足輕重的地位。最重要的是,棧儲存了一個函式呼叫所需要的的維護資訊,這常常被稱為堆幀棧或者活動記錄。堆疊幀一般包括以下幾個內容:

  • 幀棧是一個main函式的活動空間範圍。
  • 函式的返回地址和引數。
  • 臨時變數:包括函式的非靜態區域性變數以及編輯器自動生成的其他臨時變數。
  • 儲存的上下文:包括在函式呼叫前後需要保持不變暫存器。

什麼是堆?

堆是用來容納應用程式動態分配記憶體的記憶體區域,當程式使用malloc或new分配記憶體時,得到的記憶體來自堆裡,堆也可能沒有固定統一的儲存區域。堆一般比棧大很多,可能有幾十數百兆自己的容量。

認識函式堆疊呼叫的一些簡單指令

mov  移值指令

lea    移地址

push  用棧

pop    出棧

call    

  • 壓入下一行指令地址
  • jump到被呼叫方函式

認識暫存器的存在

暫存器是CPU內部的元件,暫存器擁有非常高的讀寫速度,所以在暫存器之間的資料傳送非常快。

暫存器的用途:

1.可將暫存器內的資料執行算術及邏輯運算。

2.存於暫存器內的地址可用來指向記憶體的某個位置,即定址。

3.可以用來讀寫資料到電腦的周邊裝置。

eax   是"累加器"(accumulator), 它是很多加法乘法指令的預設暫存器。

add eax,4;      eax+=4

sub eax,4;       eax-=4

ebx   是"基地址"(base)暫存器, 在記憶體定址時存放基地址。

ecx   是計數器(counter), 是重複(REP)字首指令和LOOP指令的內定計數器。

edx   總是被用來放整數除法產生的餘數。

ebp   棧底指標暫存器

esp   棧頂指標暫存器

pc    下一行指令暫存器

在i386中,一個函式的活動記錄用ebp(棧底指標暫存器)和esp(棧頂指標暫存器)這兩個暫存器劃定範圍。esp暫存器始終指向棧的頂部,同時也就指向了當前函式的活動記錄的頂部。而相對的,ebp的暫存器指向了函式活動記錄的一個固定範圍。

函式堆疊呼叫的實質

用下邊的簡單程式碼舉例瞭解一下

int Add(int a,int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int a=10;
	int b=20;
	int c = Add(a,b);
	printf("%d",c);
	return 0;
}

型參開闢記憶體:呼叫方(如下程式碼)

main 函式棧佈局:

int main()
{
011943A0  push        ebp  
011943A1  mov         ebp,esp  
011943A3  sub         esp,0E4h  
011943A9  push        ebx  
011943AA  push        esi  
011943AB  push        edi  
011943AC  lea         edi,[ebp-0E4h]  
011943B2  mov         ecx,39h  
011943B7  mov         eax,0CCCCCCCCh  
011943BC  rep stos    dword ptr es:[edi]  
	int a=10;
011943BE  mov         dword ptr [a],0Ah //在棧上開闢空間,存放[a] 
	int b=20;
011943C5  mov         dword ptr [b],14h  //開闢空間,存放[b]
	int c = Add(a,b);
011943CC  mov         eax,dword ptr [b]  //將[b]移入到暫存器eax中
011943CF  push        eax                //將eax壓棧
011943D0  mov         ecx,dword ptr [a]  //將a的值移入到暫存器ecx中,
011943D3  push        ecx                 //將ecx壓棧
011943D4  call        Add (0119110Eh) //call:呼叫被呼叫方函式(Add函式),壓入下一行指令地址
011943D9  add         esp,8           //棧頂指標移動
011943DC  mov         dword ptr [c],eax  
	printf("%d",c);
011943DF  mov         esi,esp  
011943E1  mov         eax,dword ptr [c]  //將[c]的值放入eax中
011943E4  push        eax                //eax壓棧
011943E5  push        119CC70h  
011943EA  call        dword ptr ds:[11A03B8h]  
011943F0  add         esp,8  
011943F3  cmp         esi,esp  
011943F5  call        __RTC_CheckEsp (011912D5h)  
	return 0;
011943FA  xor         eax,eax  
}
 

被呼叫方:(如下程式碼)

Add函式棧幀開闢:

 

int Add(int a,int b)
{
011928F3  sub         esp,0CCh  
011928F9  push        ebx  
011928FA  push        esi  
011928FB  push        edi  
011928FC  lea         edi,[ebp-0CCh]  
01192902  mov         ecx,33h  
01192907  mov         eax,0CCCCCCCCh  
0119290C  rep stos    dword ptr es:[edi]  
	int c = a + b;                    //將運算結果儲存到區域性變數中
0119290E  mov         eax,dword ptr [a]  
01192911  add         eax,dword ptr [b]  
01192914  mov         dword ptr [c],eax  
	return c;
01192917  mov         eax,dword ptr [c]  //通過暫存器eax將返回值帶回
}
0119291A  pop         edi  
0119291B  pop         esi  
0119291C  pop         ebx  
0119291D  mov         esp,ebp  //釋放Add函式的棧幀空間
0119291F  pop         ebp  
01192920  ret  

函式呼叫過程

  1. 開闢型參的記憶體並初始化為0xcccccccc
  2. 壓入下一行指令地址
  3. 壓入呼叫方棧底指標的值
  4. 開闢區域性變數所需要的棧空間並初始化

函式呼叫完成後的清棧

  1. 清理棧開闢的區域性變數
  2. pop ebp(棧底指標暫存器)   ebp回退到呼叫方棧底
  3. ret(pop pc)

ret包含兩個動作:
1、出棧 ,棧頂指標向下移。出棧元素是下一行指令地址,賦給PC暫存器,PC暫存器永遠存放下一行指令的地址。
2、ret執行完以跳轉到下一行指令的地址

清棧的意義:告訴暫存器記憶體可以再次被分配。

函式的呼叫約定:

(1)thiscall: 類成員方法的呼叫約定,this指標存放於CX暫存器,引數從右到左壓。

          內建型別的呼叫約定(Calling convention):

(2)_cdecl:C標準的呼叫。型參由呼叫方開闢,被呼叫方清理。每一個呼叫它的函式都包含清空堆疊的程式碼,所以產生的可執行檔案大小會比呼叫_stdcall函式的大。

(3)__stdcall:windows標準的呼叫約定,型參由呼叫方開闢,被呼叫方清理。

(4)__fastcall:快速呼叫約定,它是通過暫存器來傳送引數的(實際上,它用ECX和EDX傳送前兩個雙字(DWORD)或更小的引數,剩下的引數仍舊自右向左壓棧傳送,被呼叫的函式在返回前清理傳送引數的記憶體棧)。

約定的意義:

1)約定了函式符號的生成。(不同的函式約定,函式符號的生成不一樣)

2)約定了函式引數的壓棧順序。

3)約定了型參的開闢和清理方式。

接下來我們解決文章開頭的問題???

1、型參在哪裡開闢記憶體?

    型參在棧上開闢記憶體,由呼叫方開闢。

2、型參的入棧順序?

   型參從右向左入棧,如果從左至右的話,不知道實參的傳遞個數。

3、函式返回值怎麼帶出來?

  函式值存放到暫存器中,由暫存器帶回

eax帶回返回值:

(1)0<返回值<=4       由eax帶回

(2)4<返回值<=8       由eax edx  兩個暫存器帶回

(3)8<返回值             由臨時量帶回(非類型別)

非類型別的返回方式:

  • 臨時量地址壓棧
  • 返回值拷到臨時量裡
  • 臨時量帶回返回值

4、函式的返回值為什麼會回退到棧裡?

  因為壓棧壓的是呼叫方的棧底指標

5、函式執行完成後怎麼控制沿用點繼續執行?

  通過call指令繼續。

call指令的作用:

(1)壓入下一行指令的地址

(2)jump到被呼叫方函式

Tips:

實參與型參:

  • 型參:用來接收呼叫該方法時傳遞的引數,被呼叫時分配空間,呼叫結束後就釋放。
  • 實參:傳遞給被呼叫方法的值,預先建立並賦予確定值。
int Add(int a,int b)//型參,用來接收呼叫該方法時傳遞的引數
{
	int c = a + b;
	return c;
}
int main()
{
	int a=10;//實參,傳遞給被呼叫方法的值
	int b=20;
	int c = Add(a,b);
	printf("%d",c);
	return 0;
}

函式定義與函式說明:

  • 宣告:說明函式作用

int Add(int a,int b)  //求兩個整型數之和

  • 定義:定義函式的實現功能,實現函式功能時,函式的名稱,返回值,引數表必須要與此函式宣告時一致。

int Add(int a,int b)//實現函式
{
    int c = a + b;
    return c;
}
 

  • 當一個函式的定義在這個函式的呼叫之前,則不用宣告,否則需要宣告這個函式。

不需要宣告

int Add(int a,int b)
{
    int c = a + b;
    return c;
}
int main()
{
    int a=10;
    int b=20;
    int c = Add(a,b);
    printf("%d",c);
    return 0;
}

需要宣告

int Add(int a,int b);//函式宣告,需要加分號
int main()//被呼叫的函式在呼叫之後
{
    int a=10;
    int b=20;
    int c = Add(a,b);
    printf("%d",c);
    return 0;
}

int Add(int a,int b)
{
    int c = a + b;
    return c;
}