天天看點

函數的調用過程(棧幀)

棧幀也叫過程活動記錄,是編譯器用來實作函數調用過程的一種資料結構。c語言中,每個棧幀對應着一個未運作完的函數。從邏輯上講,棧幀就是一個函數執行的環境:函數調用架構、函數參數、函數的局部變量、函數執行完後傳回到哪裡等等。棧是從高位址向低位址延伸的。每個函數的每次調用,都有它自己獨立的一個棧幀,這個棧幀中維持着所需要的各種資訊。寄存器ebp指向目前的棧幀的底部(高位址),寄存器esp指向目前的棧幀的頂部(低位址)。

我們以add()函數為例深入的研究一下函數的調用過程。

先看一段簡單的代碼:

函數的調用過程(棧幀)
函數的調用過程(棧幀)

當講程式調試的時候, 檢視【調用堆棧】(按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指向新的棧頂位置;

c.将esp的值賦給ebp,産生新的ebp;

d.給esp減去一個16進制數0e4h(為main函數預開辟空間);

e.push ebx、esi、edi;

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

g.初始化預開辟的空間為0xcccccccc;

h.建立變量a與b。

函數的調用過程(棧幀)

</h4>

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環境中, 下面代碼的結果是什麼?

函數的調用過程(棧幀)
函數的調用過程(棧幀)

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

繼續閱讀