溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

函數(shù)的調(diào)用過程(棧幀)

發(fā)布時(shí)間:2020-07-24 17:36:11 來源:網(wǎng)絡(luò) 閱讀:803 作者:吳金瑞 欄目:網(wǎng)絡(luò)安全

    

1、什么是棧幀?

棧幀也叫過程活動(dòng)記錄,是編譯器用來實(shí)現(xiàn)函數(shù)調(diào)用過程的一種數(shù)據(jù)結(jié)構(gòu)。C語言中,每個(gè)棧幀對(duì)應(yīng)著一個(gè)未運(yùn)行完的函數(shù)。從邏輯上講,棧幀就是一個(gè)函數(shù)執(zhí)行的環(huán)境:函數(shù)調(diào)用框架、函數(shù)參數(shù)、函數(shù)的局部變量、函數(shù)執(zhí)行完后返回到哪里等等。棧是從高地址向低地址延伸的。每個(gè)函數(shù)的每次調(diào)用,都有它自己獨(dú)立的一個(gè)棧幀,這個(gè)棧幀中維持著所需要的各種信息。寄存器ebp指向當(dāng)前的棧幀的底部(高地址),寄存器esp指向當(dāng)前的棧幀的頂部(低地址)。

2、Add()函數(shù)的調(diào)用過程

我們以Add()函數(shù)為例深入的研究一下函數(shù)的調(diào)用過程。
先看一段簡(jiǎn)單的代碼:

函數(shù)的調(diào)用過程(棧幀)

 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 0;15 }

函數(shù)的調(diào)用過程(棧幀)

當(dāng)講程序調(diào)試的時(shí)候, 查看【調(diào)用堆棧】(按F10進(jìn)入調(diào)試-窗口-調(diào)用堆棧,或按快捷鍵ctrl+alt+C) ,用VS2015調(diào)試 如下圖:
函數(shù)的調(diào)用過程(棧幀)

如果用版本更老的,或其他如VC6.0等編輯器則可以看到更多信息,VS2008調(diào)試如圖:函數(shù)的調(diào)用過程(棧幀)

我們發(fā)現(xiàn)其實(shí)main函數(shù)在 __tmai nCRTStartup 函數(shù)中調(diào)用的,而 __tmai nCRTStartup 函數(shù)是在 mai nCRTStartup 被調(diào)用的。我們知道每一次函數(shù)調(diào)用都是一個(gè)過程。這個(gè)過程我們通常稱之為: 函數(shù)的調(diào)用過程。這個(gè)過程要為函數(shù)開辟??臻g, 用于本次函數(shù)的調(diào)用中臨時(shí)變量的保存、 現(xiàn)場(chǎng)保護(hù)。 這塊??臻g我們稱之為函數(shù)棧幀。
而棧幀的維護(hù)我們必須了解ebp和esp兩個(gè)寄存器。 在函數(shù)調(diào)用的過程中這兩個(gè)寄存器存放了維護(hù)這個(gè)棧的棧底和棧頂指針。比如:調(diào)用main函數(shù), 我們?yōu)閙ain函數(shù)分配棧幀空間, 那么棧幀維護(hù)如下:
函數(shù)的調(diào)用過程(棧幀)ebp存放了指向函數(shù)棧幀棧底的地址。esp存放了指向函數(shù)棧幀棧頂?shù)牡刂贰?br />注意:ebp指向當(dāng)前位于系統(tǒng)棧最上邊一個(gè)棧幀的底部,而不是系統(tǒng)棧的底部。嚴(yán)格說來,“棧幀底部”和“棧底”是不同的概念;ESP所指的棧幀頂部和系統(tǒng)棧的頂部是同一個(gè)位置。

1 . 從main函數(shù)的地方開始, 要展開main函數(shù)的調(diào)用就得為main函數(shù)創(chuàng)建棧幀, 那我們先來看main函數(shù)棧幀的創(chuàng)建。轉(zhuǎn)到反匯編可以更清晰的看到過程:函數(shù)的調(diào)用過程(棧幀)

過程分析:

a.首先mainCRTStartup(),__mainCRTStartup()函數(shù)的調(diào)用,調(diào)main()函數(shù);

b.將ebp壓棧處理,保存指向棧底的ebp的地址(方便函數(shù)返回之后的現(xiàn)場(chǎng)恢復(fù)),此時(shí)esp指向新的棧頂位置;

c.將esp的值賦給ebp,產(chǎn)生新的ebp;

d.給esp減去一個(gè)16進(jìn)制數(shù)0E4H(為main函數(shù)預(yù)開辟空間);

e.push ebx、esi、edi;

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

g.初始化預(yù)開辟的空間為0xcccccccc;

h.創(chuàng)建變量a與b。

2. 接下來是Add函數(shù)的調(diào)用。

參數(shù)傳遞過程:函數(shù)的調(diào)用過程(棧幀)
 過程分析:

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

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

c.call指令的調(diào)用,先要壓棧call指令下一條指令的 地址,然后跳轉(zhuǎn)(push+jmp)到Add()函數(shù)的地方(__cdecl調(diào)用約定)。執(zhí)行call指令的時(shí)候按F11 , 來到了這里。
函數(shù)的調(diào)用過程(棧幀)再按F11 就進(jìn)入Add函數(shù)的執(zhí)行代碼處。Add函數(shù)棧幀的創(chuàng)建:函數(shù)的調(diào)用過程(棧幀)

過程分析:

a.首先將main()函數(shù)ebp壓棧處理,保存指向main()函數(shù)棧幀底部的ebp的地址(方便函數(shù)返回之后的現(xiàn)場(chǎng)恢復(fù)),此時(shí)esp指向新的棧頂位置;

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

c.給esp減去一個(gè)16進(jìn)制數(shù)0E4H(為Add()函數(shù)預(yù)開辟空間);

d.push ebx、esi、edi;

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

f.初始化預(yù)開辟的空間為0xcccccccc;

g.創(chuàng)建變量z;

h.獲取形參的a和b再相加,將結(jié)果存儲(chǔ)到z中;

i.將結(jié)果存儲(chǔ)到eax寄存器,通過寄存器帶回函數(shù)的返回值。
剩下的就是是函數(shù)返回部分:函數(shù)的調(diào)用過程(棧幀)

過程分析:

a.pop3次,edi、esi、ebx依次出棧,esp 會(huì)向下移動(dòng);

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

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

d.ret 指令,出棧一次,并將出棧的內(nèi)容當(dāng)做地址,并跳轉(zhuǎn)到該地址處(pop+jmp)。

函數(shù)的調(diào)用過程(棧幀)

注: 棧幀這部分內(nèi)容在不同的編譯器上實(shí)現(xiàn)存在差異, 但是思想都是一致的。

棧幀的一般總結(jié):

1. 堆棧是C語言程序運(yùn)行時(shí)必須的一個(gè)記錄調(diào)用路徑和參數(shù)的空間:
函數(shù)調(diào)用框架;
傳遞參數(shù);
保存返回地址;
提供局部變量空間;
等等。
以x86體系結(jié)構(gòu)為例
2. 堆棧寄存器和堆棧操作
 堆棧相關(guān)的寄存器
esp,堆棧指針(stack pointer)
ebp,基址指針(base pointer)
堆棧操作
push 棧頂?shù)刂窚p少4個(gè)字節(jié)(32位)
pop 棧頂?shù)刂吩黾?個(gè)字節(jié)
ebp在C語言中用作記錄當(dāng)前函數(shù)調(diào)用基址
3. 利用堆棧實(shí)現(xiàn)函數(shù)調(diào)用和返回
其他關(guān)鍵寄存器
cs : eip:總是指向下一條的指令地址
● 順序執(zhí)行:總是指向地址連續(xù)的下一條指令
● 跳轉(zhuǎn)/分支:執(zhí)行這樣的指令的時(shí)候, cs : eip的值會(huì)根據(jù)程序需要被修改
● call:將當(dāng)前cs : eip的值壓入棧頂, cs : eip指向被調(diào)用函數(shù)的入口地址
● ret:從棧頂彈出原來保存在這里的cs : eip的值,放在cs : eip中
● 發(fā)生中斷時(shí)???
4. 函數(shù)堆棧框架的形成

call xxx
執(zhí)行call之前;
執(zhí)行call時(shí),cs:eip原來的值指向call下一條指令,該值被保存到棧頂,然后cs:eip的值指向xxx的入口地址
進(jìn)入xxx
第一條指令:pushl %ebp
第二條指令:movl %esp,%ebp
函數(shù)體中的常規(guī)操作,壓棧,出棧等
退出xxx
movl %ebp,%esp
popl %ebp
ret

函數(shù)的調(diào)用過程(棧幀)

5. 堆和棧的關(guān)系
我們平時(shí)說的堆棧其實(shí)是指棧,而實(shí)際上堆和棧是兩種不同的內(nèi)存分配。簡(jiǎn)單羅列如下各方面的異同點(diǎn)。
1).堆需要用戶在程序中顯式申請(qǐng),棧不用,由系統(tǒng)自動(dòng)完成。申請(qǐng)/釋放堆內(nèi)存的API,在C中是malloc/free,在C++中是new/delete。申請(qǐng)與釋放一定要配對(duì)使用,否則會(huì)造成內(nèi)存泄漏(memory leak),久而久之系統(tǒng)就無內(nèi)存可用了,出現(xiàn)OOM(Out Of Memory)錯(cuò)誤。一般在return/exit或break/continue等語句時(shí)容易忘記釋放內(nèi)存,所以檢查內(nèi)存泄漏的代碼時(shí)要關(guān)注這些語句,看它們前面是否有必要的釋放語句free/delete。
2).堆的空間比較大,棧比較小。所以申請(qǐng)大的內(nèi)存一般在堆中申請(qǐng);棧上不要有較大的內(nèi)存使用,比如大的靜態(tài)數(shù)組;而且除非算法必要,否則一般不要使用較深的迭代函數(shù)調(diào)用,那樣棧消耗內(nèi)存會(huì)隨著迭代次數(shù)的增加飛漲。
3).關(guān)于生命周期。棧較短,隨著函數(shù)退出或返回,本函數(shù)的棧就完成了使用;堆就要看什么時(shí)候釋放,生命周期就什么時(shí)候結(jié)束。
函數(shù)的調(diào)用過程(棧幀)我們發(fā)現(xiàn)解析Coredump還是跟棧的關(guān)系相對(duì)緊密,跟堆的關(guān)系是有一種產(chǎn)
生Coredump的原因是訪問堆內(nèi)存出錯(cuò)。

為什么研究棧幀?看一個(gè)題目 :
在VC6.0環(huán)境中, 下面代碼的結(jié)果是什么?

函數(shù)的調(diào)用過程(棧幀)

 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 }

函數(shù)的調(diào)用過程(棧幀)

事實(shí)上在不同平臺(tái)下這段代碼有不同的輸出,可自行驗(yàn)證。

    

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI