1. 程式人生 > >棧中函式呼叫原理詳解

棧中函式呼叫原理詳解

函式呼叫是程式設計中的重要環節,本文就函式呼叫的過程進行分析。

一、eip、ebp、esp介紹

 EIP,EBP,ESP都是系統的暫存器,裡面儲存的是些地址,我們系統中棧的實現上離不開他們三個。 我知道棧的資料結構主要特點是 後進先處。它還有兩個作用: 1.棧是用來儲存臨時變數,函式傳遞的中間結果。 2.作業系統維護的,對於程式設計師是透明的。

下面我們就通過一個小例子說說棧的原理。

先寫個小程式:

void fun(void)
{
   printf("helloworld");
}
void main(void)
{
  fun()
  printf("函式呼叫結束");
}

當程式進行函式呼叫的時候,我們經常說的是先將函式壓棧,當函式呼叫結束後,再出棧。這一切的工作都是系統幫我們自動完成的。

但在完成的過程中,系統會用到下面三種暫存器:EIP、ESP、EBP。

當呼叫fun函式開始時,三者的作用。

  1. EIP暫存器裡儲存的是CPU下次要執行的指令的地址。 也就是呼叫完fun函式後,讓CPU知道應該執行main函式中的printf("函式呼叫結束")語句了。
  2. EBP暫存器裡儲存的是是棧的棧底指標,通常叫棧基址,這個是一開始進行fun()函式呼叫之前,由ESP傳遞給EBP的。(在函式呼叫前你可以這麼理解:ESP儲存的是棧頂地址,也是棧底地址。)
  3. ESP暫存器裡儲存的是在呼叫函式fun()之後,棧的棧頂。並且始終指向棧頂。
當呼叫fun函式結束後,三者的作用:
  1. 系統根據EIP暫存器裡儲存的地址,CPU就能夠知道函式呼叫完,下一步應該做什麼,也就是應該執行main函式中的printf(“函式呼叫結束”)。
  2. EBP暫存器儲存的是棧底地址,而這個地址是由ESP在函式呼叫前傳遞給EBP的。等到呼叫結束,EBP會把其地址再次傳回給ESP。所以ESP又一次指向了函式呼叫結束後,棧頂的地址。

二、堆和棧

首先要清楚的是程式對記憶體的使用分為以下幾個區:

  1. 棧區(stack):由編譯器自動分配和釋放,存放函式的引數值,區域性變數的值等。操作方式類似於資料結構中的棧。
  2. 堆區(heap):一般由程式設計師分配和釋放,若程式設計師不釋放,程式結束時可能由作業系統回收。與資料結構中的堆是兩碼事,分配方式類似於連結串列。
  3. 全域性區(static):全域性變數和靜態變數存放在此。
  4. 文字常量區:常量字串放在此,程式結束後由系統釋放。
  5. 程式程式碼區:存放函式體的二進位制程式碼。

典型的記憶體區域分配如圖所示:


其次是堆和棧的申請方式:棧由系統自動分配,速度較快,在windows下棧是向低地址擴充套件的資料結構,是一塊連續的記憶體區域,大小是2MB。堆需要程式設計師自己申請,並指明大小,速度比較慢。在C中用malloc,C++中用new。另外,堆是向高地址擴充套件的資料結構,是不連續的記憶體區域,堆的大小受限於計算機的虛擬記憶體。因此堆空間獲取和使用比較靈活,可用空間較大。 

三、棧幀結構和函式呼叫過程

首先應該明白,棧是從高地址向低地址延伸的。每個函式的每次呼叫,都有它自己獨立的一個棧幀,這個棧幀中維持著所需要的各種資訊。暫存器ebp指向當前的棧幀的底部(高地址),暫存器esp指向當前的棧幀的頂部(地址地)。下圖為典型的存取器安排,觀察棧在其中的位置


入棧操作:push eax; 等價於 esp=esp-4,eax->[esp];如下圖


出棧操作:pop eax; 等價於 [esp]->eax,esp=esp+4;如下圖


我們來看下面這個C程式在執行過程中,棧的變化情況

void func(int m, int n) {
    int a, b;
    a = m;
    b = n;
}
main() {
...
    func(m, n);
L:  下一條語句
...
} 

在main呼叫func函式前,棧的情況,也就是說main的棧幀:


從低地址esp到高地址ebp的這塊區域,就是當前main函式的棧幀。當main中呼叫func時,寫成彙編大致是:

push m

push n; 兩個引數壓入棧

call func; 呼叫func,將返回地址填入棧,並跳轉到func


當跳轉到了func,來看看func的彙編大致的樣子:

__func:

push ebp; 這個很重要,因為現在到了一個新的函式,也就是說要有自己的棧幀了,那麼,必須把上面的函式main的棧幀底部儲存起來,棧頂是不用儲存的,因為上一個棧幀的頂部講會是func的棧幀底部。(兩棧幀相鄰的)

       mov ebp, esp; 上一棧幀的頂部,就是這個棧幀的底部;暫時先看現在的棧的情況


到這裡,新的棧幀開始了

sub esp, 8;  int a, b 這裡聲明瞭兩個int,所以esp減小8個位元組來為a,b分配空間

mov dword ptr [esp+4],[ebp+12];   a=m

mov dword ptr [esp], [ebp+8];b=n         

這樣,棧的情況變為:


ret 8 ;  返回,然後8是什麼意思呢,就是引數佔用的位元組數,當返回後,esp-8,釋放參數m,n的空間。由此可見,通過ebp,能夠很容易定位到上面的引數。當從func函式返回時,首先esp移動到棧幀底部(即釋放區域性變數),然後把上一個函式的棧幀底部指標彈出到ebp,再彈出返回地址到cs:ip上,esp繼續移動劃過引數,這樣,ebp,esp就回到了呼叫函式前的狀態,即現在恢復了原來的main的棧幀。