汕頭網(wǎng)絡(luò)公司網(wǎng)站建設(shè)朝陽網(wǎng)站seo
一、前言:
環(huán)境:X86+Vs2013
我們C語言學(xué)習(xí)過程中是否遇到過如下問題或者疑惑:
1、局部變量是如何創(chuàng)建的?
2、為什么局部變量的值是隨機值?
3、函數(shù)是怎么傳參的?傳參的順序是怎樣的?
4、形參和實參是什么關(guān)系?
5、函數(shù)調(diào)用是怎么做的?
6、函數(shù)調(diào)用完后怎么返回的?
看完這篇關(guān)于函數(shù)棧幀的博客,我相信你對這些問題會有一些進一步的理解,希望能幫助你解決一些學(xué)習(xí)中的困惑。
二、預(yù)備知識了解
2.1、寄存器的種類和作用
eax | 累加寄存器,相對于其他寄存器,在運算方面比較常用。 |
ebx | 基地址寄存器,在內(nèi)存尋址時存放基地址。 |
ecx | 計數(shù)寄存器,用于循環(huán)操作,比如重復(fù)的字符存儲操作,或者數(shù)字統(tǒng)計。 |
edx | 作為EAX的溢出寄存器,總是被用來放整數(shù)除法產(chǎn)生的余數(shù)。 |
esi | 變址寄存器,主要用于存放存儲單元在段內(nèi)的偏移量。通常在內(nèi)存操作指令中作為“源地址指針”使用 |
edi | 目的變址寄存器,主要用于存放存儲單元在段內(nèi)的偏移量。 |
eip | 控制寄存器,存儲CPU下次所執(zhí)行的指令地址(存放指令偏移地址)。 |
esp | 棧頂指針,堆棧的頂部是地址小的區(qū)域,壓入堆棧的數(shù)據(jù)越多,esp也就越來越小。在32位平臺上,esp每次減少4字節(jié)。棧指針寄存器(extended stack pointer),其內(nèi)存放著一個指針,該指針永遠指向系統(tǒng)棧最上面一個棧幀的棧頂。是CPU機制決定的,push、pop指令會自動調(diào)整esp的值。 |
ebp | 基址指針,指棧的棧底指針?;分羔樇拇嫫?extended base pointer),一般與esp配合使用,可以存取某時刻的esp,這個時刻就是進入一個函數(shù)內(nèi)后,CPU會將esp的值賦給ebp,此時就可以通過ebp對棧進行操作,比如獲取函數(shù)參數(shù),局部變量等。其內(nèi)存放著一個指針,該指針永遠指向系統(tǒng)棧最上面一個棧幀的底部。 |
2.2、匯編指令
1、push:在棧的頂端開辟地址并存放變量,然后減少esp的值,32位機器上esp每次減少4個字節(jié),64位機器上esp每次減少8字節(jié)。
2、pop:在棧頂端去掉一個地址,然后增加esp的值,2位機器上esp每次增加4個字節(jié),64位機器上esp每次增加8字節(jié)。
3、mov:用于將一個數(shù)據(jù)的源地址傳送到目標地址,原操作地址不變。將esp值賦給ebp。
4、sub:從寄存器上減去<shifter_operand>表示的數(shù)值,并將結(jié)果保存到寄存器上。
5、lea(load effective address):將一個內(nèi)存地址直接付給目標的操作數(shù)。
6、rep(repeat):引發(fā)字符串指令被重復(fù)使用。
7、stos(store string):將exc的值拷貝到es:[edi]指向的地址。
8、call:將程序下一條指令的位置的IP壓入堆棧,并調(diào)用的子程序。
9、jmp:跳轉(zhuǎn)指令。
10、add:將兩個數(shù)相加,結(jié)果寫入第一個數(shù)中。
11、ret:終止函數(shù)執(zhí)行,當(dāng)前棧幀所開辟的空間收回。
2.3、例子
為了能夠看清楚全部細節(jié),我們把函數(shù)寫的盡量細一點。
#include <stdio.h>int Add(int x, int y)
{int z = 0;z = x + y;return z;
}
int main()
{int a = 10;int b = 20;int c = 0;c = Add(a, b);printf("%d\n", c);return 0;
}
匯編碼
int main() {
002718A0 push ebp
002718A1 mov ebp,esp
002718A3 sub esp,0E4h
002718A9 push ebx
002718AA push esi
002718AB push edi
002718AC lea edi,[ebp-24h]
002718AF mov ecx,9
002718B4 mov eax,0CCCCCCCCh
002718B9 rep stos dword ptr es:[edi]
002718BB mov ecx,27C003h
002718C0 call 0027131B int a = 10;
002718C5 mov dword ptr [ebp-8],0Ah int b = 20;
002718CC mov dword ptr [ebp-14h],14h int c = 0;
002718D3 mov dword ptr [ebp-20h],0 c = Add(a, b);
002718DA mov eax,dword ptr [ebp-14h]
002718DD push eax
002718DE mov ecx,dword ptr [ebp-8]
002718E1 push ecx
002718E2 call 002710B4
002718E7 add esp,8
002718EA mov dword ptr [ebp-20h],eax printf("%d", c);
002718ED mov eax,dword ptr [ebp-20h]
002718F0 push eax
002718F1 push 277B30h
002718F6 call 002710D2
002718FB add esp,8 return 0;
002718FE xor eax,eax
}
00271900 pop edi
00271901 pop esi
00271902 pop ebx
00271903 add esp,0E4h
00271909 cmp ebp,esp
0027190B call 00271244
00271910 mov esp,ebp
00271912 pop ebp
00271913 ret
2.4、內(nèi)存模型
在棧區(qū)創(chuàng)建函數(shù)棧幀
三、棧幀的創(chuàng)建
按下F10,在視圖中打開調(diào)用堆棧窗口,我們發(fā)現(xiàn)
main()
函數(shù)被調(diào)用了。那么main()函數(shù)被誰調(diào)用調(diào)用了呢?
當(dāng)我們調(diào)試到 return 0 之后;再按F10,我們發(fā)現(xiàn)程序跳轉(zhuǎn)到了調(diào)用
main()
函數(shù)的函數(shù)內(nèi),
原來
main()
函數(shù)是被__tmainCRTStartup
函數(shù)調(diào)用的,而?__tmainCRTStartup
又是被mainCRTStartup
調(diào)用的。
3.1、為main函數(shù)開辟棧幀
?3.2、在main函數(shù)中創(chuàng)建變量
3.3、調(diào)用add函數(shù)前做準備
函數(shù)傳參從右向左
?3.4、為add函數(shù)開辟棧幀
?3.5、Add()函數(shù)中創(chuàng)建變量并運算
形參是實參的一份臨時拷貝
四、函數(shù)棧幀的銷毀
4.1、Add()棧幀的銷毀
過程一:pop ? ?edi / esi / ebx
過程二:mov? ? esp, ebp?】
過程三:pop ebp】
過程四:ret】
過程五:mov ? ? dword ptr [ebp-20h],eax】
4.2、返回main()函數(shù)棧幀
可以看到這里返回到了(3.3調(diào)用Add()函數(shù)前的準備),最后指令call
的下一條指令。
之后的過程還很復(fù)雜,這里就不詳細展示了。
五、總結(jié):
對此我們對剛開始的問題是不是有了一點柳暗花明的感覺。
在不同的編譯器下,函數(shù)調(diào)用過程中棧幀的創(chuàng)建是略有差異的,具體細節(jié)取決于編譯器的實現(xiàn)。
友情提示:
不要使用太高級的編譯器,越高級的編譯器,越不容易學(xué)習(xí)和觀察。
這篇博客有很多不足的地方,希望大家及時指出來!!!