函數堆棧調用過程

從內存的角度詳細的分析C語言中的函數調用過程:

首先寫一個測試用的代碼:


#include <stdio.h>

int add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}

int main()
{
	int a = 1, b = 2;
	int c = 0;

	c = add(a, b);

	return 0;
}

這是一個簡單的的求和函數。


其次,讓我們確定一下,程序是從哪裏開始運行的:

調試程序,按一下F10(博主用的VS2013),

進入main函數:


然後進調試--->窗口--->調用堆棧(用來顯示函數的調用關係)。


發現正在調用main這個函數,但現在我想知道是誰在調用main函數,F10一路走到return 0,接着換F11(逐語句調試),然後會發現,main函數返回後,我們來到了這裏:

再看看此時的調用堆棧:

直接來看,現在運行的函數是__tmainCRTStartup(),這個函數又被mainCRTStratup()調用,而我們剛剛是從main()函數返回來的,所以,main()函數是由__tmainCRTStartup()這個函數調用的。

瞭解了main()函數是被誰調用後,我們可以進一步分析這其中的細節了!

現在重新F10進入調試,到這一步:

進入main()函數後還沒有執行任何一條語句,我們 右擊-->轉到反彙編:


看到了彙編語言的代碼,圖中的ebp和esp是什麼東西呢?我們知道,調用函數的時候操作系統要給這個函數分配一段內存空間,之前又說了main()函數是由—__tCRTStartup()函數調用的,所以請看:


mainCRTStratup()函數調用__tmainCRTStra()函數的時候就會從棧上爲__tmainCRTStra()分配類似圖中這麼一塊空間,把這塊空間叫做棧幀。我們知道棧是由高地址向低地址擴展的。其中ebp叫做棧底指針,esp叫做棧頂指針(當然也有其它叫法)。ebp,esp本身是一個寄存器,其中存放了地址時,我們就稱之爲指針!

現在再來看彙編程序:


按一下F10執行第一條語句,箭頭指向下一條語句,變成這樣:


(和我們在外邊的調試是一樣的)這句 push ebp 就是將ebp中的值進行壓棧,而此時ebp存放的是系統分給__tmainCRTStartup()函數的空間的起始地址。因爲我們現在要調用main()函數了,所以當然要先把__tmainCRTStartup()函數的運行狀態保存下來,這樣main()函數才能返回的時候才能找得到!push是在棧頂進行的,所以,push之後,esp要向上移動:


剛剛說了,棧是由高地址向低地址擴展的,所以這個push操作應該是對esp進行一個減操作,具體見了多少,可以在內存裏查一查:

先看一下push之前esp的的值:

esp的當前值爲0x00ABFA30,代表它指向0x00ABFA30這個地址代表的內存。

再看一下push之後esp的值發生了什麼變化:

變成了0x00ABFA2C,差了4個字節,就是放進去的地址的大小。

然後繼續執行下一條語句: mov         ebp,esp   

即把esp的值賦給ebp,這樣,ebp也就指向了現在esp的位置,如下圖:


 接着又執行語句:sub         esp,0E4h  

即將esp的值減去E4h,所以esp向上移動了E4h個位置(相當於申請了這麼大的一塊空間),新申請的這塊空間就給main()用了。如下:




接下來緊接着三條push語句將後面要用到的寄存器中原來的值存儲起來,等我們借用完寄存器後再給人家pop回去,不管它,這裏esp再向上移動三次。

(ps:圖片太大,所以只截了當前要用到的)

緊接着的四條語句共同完成一個任務,就是將圖中最大長方形區域初始化爲0CCCCCCCh(你經常看到的:燙燙燙燙......)

 第一句:lea         edi,[ebp-0E4h]  
就是將ebp減去E4h的值賦給edi,這個E4h是不是很眼熟呢?它就是我們上一步分配給main()的空間的大小,即edi指向了3次push之前的esp的位置;

第二句:mov         ecx,39h 
把39h放在ecx中(充當了計數器)

第三句:mov         eax,0CCCCCCCCh 

把要初始化的數據寫入eax

最後一句:rep stos    dword ptr es:[edi]

循環的從低地址(ebp-0E4h)向高地址(ebp)寫0CCCCCCCCh,循環了39h次!

我們在執行之前轉到內存中看一下:

先查找ebp:



(我往下拖了一點,左下角的光標處的地址就是ebp當前值0z00ABFA2C)

四條語句執行後:


相應的位置已經被初始化爲0CCCCCCCh,其它部分是亂碼(此時ebp值爲0x00ABFA2C,它之上的一段空間是分配給main()的)

程序繼續往下執行:


mov         dword ptr [ebp-8],1  在ebp-8h的位置放一個1,
mov         dword ptr [ebp-14h], 2 在ebp-14h的位置放一個2

即分別創建了a,b兩個變量,如圖:


接着創建c:


此時我們的內存分配變成了這樣:


然後到了這裏,調用add()準備工作:


mov         eax,dword ptr [ebp-14h]    是把ebp-14h位置的值放入eax(此時ebp-14h的值是我們的變量b的值),然後:push        eax , 即eax壓棧;

同理,mov         ecx,dword ptr [ebp-8]   把ebp-8位置的值放入ecx,然後ecx壓棧。如下(傳遞形參給x和y):



程序到這:


在彙編裏我們用call調用一個函數(_add是一個標號,它代表了一個地址,是add()函數的首地址),而call在執行的同時,會把它下一條指令的地址(就是圖中的00D1450)push到main()的棧楨中去,以便add()執行完後返回的時候還可以找到程序當初執行到了哪裏,然後接着執行。

爲了證明這一點,我們先查看一下esp所指向內存的值:


然後F11跟進去到這裏:

再查看esp所指內存:

可以看到esp的位置發生了改變,此時內存中的值 50 14 0d 00 是不是很像剛剛的call語句下一條指令地址呢?對它就是00 0d 14 50 的小端字節序,這裏不再解釋小端字節序,只需理解它是內存中字節存儲的一種方式,有興趣的可以查看:http://blog.csdn.net/qq_33724710/article/details/51056542

棧楨分配圖變成了這樣:

接着F11執行剛剛的jmp語句:


歷盡千辛萬苦終於進入add()!現在貼出來的這幾句代碼就和我們剛剛進入main()函數的語句大同小異了。

  push        ebp  //ebp壓棧
  mov         ebp,esp  //ebp指向esp所指
  sub         esp,0CCh  //esp - 0CCh, 開闢了新的棧楨
  push        ebx  //3個push,照舊不管它
  push        esi  
  push        edi  
  lea         edi,[ebp-0CCh]  //初始化燙燙燙燙......  
  mov         ecx,33h  
  mov         eax,0CCCCCCCCh  
  rep stos    dword ptr es:[edi] 

然後到這裏:

給ebp-8處放了個0,就是創建z啦!

再接着到這裏:

eax,dword ptr [ebp+8]  //注意是加了8,取出的是我們之前傳遞進來的形參值1,放到eax
add         eax,dword ptr [ebp+0Ch]  //取epb+0Ch,取出的是我們之前傳遞進來的形參值2,加到eax
dword ptr [ebp-8],eax  //再把求和後的值eax賦給epb - 8的位置,就是z嘍! 


程序執行到這,準備返回main()了:


因爲z是個臨時變量,出了add()就會銷燬,要返回z的值,就要把它的值放進寄存器:

mov         eax,dword ptr [ebp-8]  //epb-8找到的就是z,賦給eax
 pop         edi  //連續三個pop,之前連續三個push我們沒管它,現在仍然不管它
 pop         esi  
 pop         ebx  
3次pop後,esp高地址處移動了3個單位:

雖然esp上邊的空間還在,但是已經不屬於當前的棧楨了,相當於釋放掉了!

然後:

  mov         esp,ebp  //esp指向當前ebp
  pop         ebp  //main()起始地址賦給給ebp,esp往高地址處移動一次

所以變成這樣:


最後執行ret,程序回到這裏:


看見了沒,ret指令自動取出了call的下一條語句地址(ret自動執行了pop,esp又往高地址處移動了一次)賦給了PC(PC總是指向下一條要執行的語句)。

接着的add         esp,8  使esp繼續往高地址方向移動,並跳過1,2兩個參數,如下:

mov         dword ptr [ebp-20h],eax  //還記得eax嗎,當初我們把求和的結果,即 z 的值賦給了它,ebp-20h依然是當初的c

現在,我們要的結果已經賦給 c 了!


  xor         eax,eax  //eax沒用了,異或eax,清零

  pop         edi  //又是連續3個pop
  pop         esi  
  pop         ebx  
  add         esp,0E4h  //oE4h,當出開闢的main()棧楨的大小,現在釋放掉
  cmp         ebp,esp  //不管它
  call        000D113B  //不管它
  mov         esp,ebp  //釋放main()棧楨
  pop         ebp  //ebp指向__tmainCRTStartup()起始地址,esp下移
  ret  //返回到__tmainCRTStartup()

__tmainCRTStartup()和mainCRTStart()裏邊的過程就不在分析了!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章