函數的調用過程(棧幀)

一、什麼是棧幀?

棧幀也叫過程活動記錄,是編譯器用來實現函數調用過程的一種數據結構。C語言中,每一個棧幀對應着一個未運行完的函數。從邏輯上講,棧幀就是一個函數執行的環境:函數調用框架、函數參數、函數的局部變量、函數執行完後返回到哪裏等等。棧是從高地址向低地址延伸的。每一個函數的每次調用,都有它本身獨立的一個棧幀,這個棧幀中維持着所須要的各類信息。寄存器ebp指向當前的棧幀的底部(高地址),寄存器esp指向當前的棧幀的頂部(低地址)。算法

二、Add()函數的調用過程

咱們以Add()函數爲例深刻的研究一下函數的調用過程。
先看一段簡單的代碼:
 1 #include <stdio. h>
 2 int Add(int x, int y)
 3 {
 4 int z = 0;
 5 z = x + y;
 6 return z;
 7 }
 8 int main()
 9 {
10 int a = 10;
11 int b = 20;
12 int ret = Add(a, b) ;
13 printf("ret = %d\n", ret) ;
14 return 015 }

當講程序調試的時候, 查看【調用堆棧】(按F10進入調試-窗口-調用堆棧,或按快捷鍵ctrl+alt+C) ,用VS2015調試 以下圖:
數組

若是用版本更老的,或其餘如VC6.0等編輯器則能夠看到更多信息,VS2008調試如圖:數據結構

咱們發現其實main函數在 __tmai nCRTStartup 函數中調用的,而 __tmai nCRTStartup 函數是在 mai nCRTStartup 被調用的。咱們知道每一次函數調用都是一個過程。這個過程咱們一般稱之爲: 函數的調用過程。這個過程要爲函數開闢棧空間, 用於本次函數的調用中臨時變量的保存、 現場保護。 這塊棧空間咱們稱之爲函數棧幀。
而棧幀的維護咱們必須瞭解ebp和esp兩個寄存器。 在函數調用的過程當中這兩個寄存器存放了維護這個棧的棧底和棧頂指針。好比:調用main函數, 咱們爲main函數分配棧幀空間, 那麼棧幀維護以下:
ebp存放了指向函數棧幀棧底的地址。esp存放了指向函數棧幀棧頂的地址。
注意:ebp指向當前位於系統棧最上邊一個棧幀的底部,而不是系統棧的底部。嚴格說來,「棧幀底部」和「棧底」是不一樣的概念;ESP所指的棧幀頂部和系統棧的頂部是同一個位置。框架

1 . 從main函數的地方開始, 要展開main函數的調用就得爲main函數建立棧幀, 那咱們先來看main函數棧幀的建立。轉到反彙編能夠更清晰的看到過程:編輯器

過程分析:

a.首先mainCRTStartup(),__mainCRTStartup()函數的調用,調main()函數;函數

b.將ebp壓棧處理,保存指向棧底的ebp的地址(方便函數返回以後的現場恢復),此時esp指向新的棧頂位置;spa

c.將esp的值賦給ebp,產生新的ebp;指針

d.給esp減去一個16進制數0E4H(爲main函數預開闢空間);調試

e.push ebx、esi、edi;code

f.lea指令,加載有效地址;

g.初始化預開闢的空間爲0xcccccccc;

h.建立變量a與b。

2. 接下來是Add函數的調用。

參數傳遞過程:

 過程分析:

a.將b存入寄存器eax,再將將eax壓棧;(傳參過程,從左向右傳遞)

b.將a存入寄存器ecx,再將將ecx壓棧;

c.call指令的調用,先要壓棧call指令下一條指令的 地址,而後跳轉(push+jmp)到Add()函數的地方(__cdecl調用約定)。
執行call指令的時候按F11 , 來到了這裏。
再按F11 就進入Add函數的執行代碼處。Add函數棧幀的建立:

過程分析:

a.首先將main()函數ebp壓棧處理,保存指向main()函數棧幀底部的ebp的地址(方便函數返回以後的現場恢復),此時esp指向新的棧頂位置;

b.將esp的值賦給ebp,產生新的ebp,即Add()函數棧幀的ebp;

c.給esp減去一個16進制數0E4H(爲Add()函數預開闢空間);

d.push ebx、esi、edi;

e.lea指令,加載有效地址;

f.初始化預開闢的空間爲0xcccccccc;

g.建立變量z;

h.獲取形參的a和b再相加,將結果存儲到z中;

i.將結果存儲到eax寄存器,經過寄存器帶回函數的返回值。
剩下的就是是函數返回部分:

過程分析:

a.pop3次,edi、esi、ebx依次出棧,esp 會向下移動;

b.將ebp賦給esp,使esp指向ebp指向的地方

c.ebp 出棧,將出棧的內容給ebp(即main()函數ebp),回到main()函數的棧幀;

d.ret 指令,出棧一次,並將出棧的內容當作地址,並跳轉到該地址處(pop+jmp)。

注: 棧幀這部份內容在不一樣的編譯器上實現存在差別, 可是思想都是一致的。

棧幀的通常總結:

1. 堆棧是C語言程序運行時必須的一個記錄調用路徑和參數的空間:
➢ 函數調用框架;
➢ 傳遞參數;
➢ 保存返回地址;
➢ 提供局部變量空間;
➢ 等等。
以x86體系結構爲例
2. 堆棧寄存器和堆棧操做
 堆棧相關的寄存器
➢ esp,堆棧指針(stack pointer)
➢ ebp,基址指針(base pointer)
堆棧操做
➢ push 棧頂地址減小4個字節(32位)
➢ pop 棧頂地址增長4個字節
❖ ebp在C語言中用做記錄當前函數調用基址
3. 利用堆棧實現函數調用和返回
❖其餘關鍵寄存器
➢ cs : eip:老是指向下一條的指令地址
● 順序執行:老是指向地址連續的下一條指令
● 跳轉/分支:執行這樣的指令的時候, cs : eip的值會根據程序須要被修改
● call:將當前cs : eip的值壓入棧頂, cs : eip指向被調用函數的入口地址
● ret:從棧頂彈出原來保存在這裏的cs : eip的值,放在cs : eip中
● 發生中斷時???
4. 函數堆棧框架的造成

❖call xxx
➢執行call以前;
➢執行call時,cs:eip原來的值指向call下一條指令,該值被保存到棧頂,而後cs:eip的值指向xxx的入口地址
❖進入xxx
➢第一條指令:pushl %ebp
➢第二條指令:movl %esp,%ebp
➢函數體中的常規操做,壓棧,出棧等
❖退出xxx
movl %ebp,%esp
popl %ebp
ret

5. 堆和棧的關係
咱們平時說的堆棧實際上是指棧,而實際上堆和棧是兩種不一樣的內存分配。簡單羅列以下各方面的異同點。
1).堆須要用戶在程序中顯式申請,棧不用,由系統自動完成。申請/釋放堆內存的API,在C中是malloc/free,在C++中是new/delete。申請與釋放必定要配對使用,不然會形成內存泄漏(memory leak),長此以往系統就無內存可用了,出現OOM(Out Of Memory)錯誤。通常在return/exit或break/continue等語句時容易忘記釋放內存,因此檢查內存泄漏的代碼時要關注這些語句,看它們前面是否有必要的釋放語句free/delete。
2).堆的空間比較大,棧比較小。因此申請大的內存通常在堆中申請;棧上不要有較大的內存使用,好比大的靜態數組;並且除非算法必要,不然通常不要使用較深的迭代函數調用,那樣棧消耗內存會隨着迭代次數的增長飛漲。
3).關於生命週期。棧較短,隨着函數退出或返回,本函數的棧就完成了使用;堆就要看何時釋放,生命週期就何時結束。
咱們發現解析Coredump仍是跟棧的關係相對緊密,跟堆的關係是有一種產
生Coredump的緣由是訪問堆內存出錯。

爲何研究棧幀?看一個題目 :
在VC6.0環境中, 下面代碼的結果是什麼?

 1 #include <stdi o. h>
 2 void fun()
 3 {
 4 int tmp = 10;
 5 int *p = (int *) (*(&tmp+1) ) ;
 6 *(p-1) = 20;
 7 }
 8 int main()
 9 {
10 int a =0;
11 fun() ;
12 printf("a = %d\n", a) ;
13 return 0;
14 }

事實上在不一樣平臺下這段代碼有不一樣的輸出,可自行驗證。

相關文章
相關標籤/搜索