在我們剛開始學習C語言的時候,我們可能還有很多困惑的地方,
比如:
區域變數是怎么創建的?
為什么區域變數的值是隨機值?
函式是怎么傳參的,傳參的順序是怎么樣的?
形參和實參是什么關系?
函式呼叫是怎么做的?
函式呼叫結束后是怎么回傳的?
當看完今天這篇文章之后,一切都將豁然開朗,
目錄
前言
預備知識
1.堆疊區的使用習慣
2.常見的幾個暫存器
3.常用的匯編指令
2.變數的創建以及函式傳參
3.Add函式堆疊幀的開辟
4.函式堆疊幀的銷毀
前言
本人今天使用編譯器的VS2013,我沒有使用VS2019的原因是:越高級的編譯器,越不容易去學習和觀察,同時在不同的編譯器下,函式呼叫程序中堆疊幀的創建是略有差異的,具體細節取決于編譯器的實作,
預備知識
在正式開始之前還需要先了解以下幾個小知識,以便于更好地去理解函式堆疊幀的創建與銷毀,
1.堆疊區的使用習慣
堆疊區的使用習慣是會先使用高地址處的空間,再使用低地址處的空間,每個函式的呼叫,都需要在堆疊區中開辟一個空間,而這個空間的開辟,都會按照高地址向低地址的方向執行,也就是說在呼叫函式的時候,會先在堆疊區的高地址處開辟空間,
2.常見的幾個暫存器
暫存器是CPU內部用來存放資料的一些小型存盤區域,用來暫時存放參與運算的資料和運算的結果,我們常見的暫存器有:eax,ebx,ecx,edx,ebp以及esp等等,我們今天的兩個主角便是ebp和esp,ebp,esp這兩個暫存器中存放的是地址,這兩個地址是用來維護函式堆疊幀的,ebp通常指向堆疊底,存盤堆疊底地址,因此又被稱為堆疊底指標,esp通常指向堆疊頂,存盤堆疊頂地址,因此又被稱為堆疊頂指標,
3.常用的匯編指令
1.add:加法指令,第一個是目標運算元,第二個是源運算元,格式為:目標運算元=目標運算元+源運算元;
2.sub:減法指令,格式同add一樣;
3.call:呼叫函式,一般函式的引數放在暫存器中;
4.ret:跳轉會呼叫函式的地方,對應于call,回傳到對應的call呼叫的下一條指令,若有回傳值,則放入eax中;
5.push:把一個32位的運算元壓入堆疊中,這個操作在32位機中會使得esp被減去4個位元組,esp通常是指向堆疊頂的(前面有說到),堆疊中頂部是地址小的區域,那么,壓入堆疊的資料越多,esp也就越來越小,
6.pop:與push相反,每當有一個資料push(出堆疊),esp每次加4給四節,
7.mov:資料傳送,第一個引數是目的運算元,第二個是源運算元,它的作用就是把第二個引數拷貝到第一個引數,
8.lea:取得第二個引數地址后放入到前面的暫存器(第一個引數)中,
下面我們通過這段代碼和對應的匯編語言來詳細的解釋函式堆疊幀的創建與銷毀
#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()
{
00E41410 push ebp
00E41411 mov ebp,esp
00E41413 sub esp,0E4h
00E41419 push ebx
00E4141A push esi
00E4141B push edi
00E4141C lea edi,[ebp+FFFFFF1Ch]
00E41422 mov ecx,39h
00E41427 mov eax,0CCCCCCCCh
00E4142C rep stos dword ptr es:[edi]
int a = 10;
00E4142E mov dword ptr [ebp-8],0Ah
int b = 20;
00E41435 mov dword ptr [ebp-14h],14h
int c = 0;
00E4143C mov dword ptr [ebp-20h],0
c = Add(a, b);
00E41443 mov eax,dword ptr [ebp-14h]
c = Add(a, b);
00E41446 push eax
00E41447 mov ecx,dword ptr [ebp-8]
00E4144A push ecx
00E4144B call 00E410E1
00E41450 add esp,8
00E41453 mov dword ptr [ebp-20h],eax
printf("%d\n", c);
00E41456 mov esi,esp
00E41458 mov eax,dword ptr [ebp-20h]
00E4145B push eax
00E4145C push 0E45858h
00E41461 call dword ptr ds:[00E49114h]
00E41467 add esp,8
00E4146A cmp esi,esp
00E4146C call 00E4113B
return 0;
00E41471 xor eax,eax
}
00E41473 pop edi
00E41474 pop esi
00E41475 pop ebx
00E41476 add esp,0E4h
00E4147C cmp ebp,esp
00E4147E call 00E4113B
00E41483 mov esp,ebp
00E41485 pop ebp
00E41486 ret
通過在VS2013上面對該代碼除錯與呼叫堆疊我們發現main是被呼叫的,那么是被誰呼叫的呢?下面就以這幾張圖片來回答(有圖有真相嘛)




知道了main函式被誰呼叫之后,我們再通過上面代碼與匯編指令來詳細講一下main函式-函式堆疊幀的創建與銷毀,
00E41410 push ebp
00E41411 mov ebp,esp
00E41413 sub esp,0E4h
00E41419 push ebx
00E4141A push esi
00E4141B push edi
00E4141C lea edi,[ebp+FFFFFF1Ch]
00E41422 mov ecx,39h
00E41427 mov eax,0CCCCCCCCh
00E4142C rep stos dword ptr es:[edi]

首先是push ebp意思就是將ebp這個引數壓進去,因為esp一般是指向堆疊頂的所以當ebp壓進去之后esp也會指向ebp的位置,這時esp會向上走,那么它的地址會變小(push一個引數,32位的,esp會減去4個位元組,16位則減去兩個),通過呼叫監視視窗我們也可以觀察到esp的的值確是在變小



第二步move ebp,esp 就是把esp的值給ebp,即把ebp的地址指向改為esp的地址,通過觀察監視視窗我們也可以看到


再然后就是sub esp,0E4h 意思就是將esp減去0E4h,這時esp就會往上走,此時ebp與esp之間維護的這塊空間就是為main函式開辟的堆疊幀,然后再push ebx,esi,edi則是把ebx,esi,edi壓堆疊,每壓一個引數esp的地址就會減去4個位元組,此時esp會指向edi的位置上,

通過呼叫監視視窗記憶體視窗我們可以發現ebx,esi,edi的三個值被壓進去了,



然后我們再來看看接下來的四步(此時在VS2013里面打開了顯示符號名)
00E4141C lea edi,[ebp-0E4h]
00E41422 mov ecx,39h
00E41427 mov eax,0CCCCCCCCh
00E4142C rep stos dword ptr es:[edi]
這段的作用是:(1)ebp-0E4h的地址放到edi里面去
(2)把39h放到ecx里面去
(3)把0CCCCCCCCh放到eax里面去
(4)從edi這個位置開始以下的39h個4位元組都變成eax也就是CCCCCCCC
簡而言之就是將ebp到edi-0E4h這段空間都初始化成CCCCCCCC,而CCCCCCCC就是燙對應的亂碼,這也就解釋了為什么區域變數沒有初始化的時候是隨機值的原因,



此時為main函式堆疊幀的開辟就已經準備好了,下面就要來執行正式有效的代碼啦,
2.變數的創建以及函式傳參
int a = 10;
00E4142E mov dword ptr [ebp-8],0Ah
int b = 20;
00E41435 mov dword ptr [ebp-14h],14h
int c = 0;
00E4143C mov dword ptr [ebp-20h],0
這段的作用是:(1)將0Ah(也就是10)放到ebp-8的位置上去
(2)將14h(也就是20)放到ebp-14h的位置上去
(3)將0放到ebp-20h的位置上去

這就是變數創建的程序,
接下來我們就要呼叫Add函式以及傳參了
c = Add(a, b);
00E41443 mov eax,dword ptr [ebp-14h]
00E41446 push eax
00E41447 mov ecx,dword ptr [ebp-8]
00E4144A push ecx
00E4144B call 00E410E1
00E41450 add esp,8
00E41453 mov dword ptr [ebp-20h],eax
printf("%d\n", c);
將ebp-14h(也就是20)放到eax里面去,然后將eax壓入堆疊中,將ebp-8(也是就10)放到ecx里面去,接著將ecx壓入堆疊中,其實這就是在傳參!!!但是我們可能還感受不到,我們接著看后面,call 執行之后就會將它下一條指令的地址給壓進去


3.Add函式堆疊幀的開辟
int Add(int x, int y)
{
00E413C0 push ebp
00E413C1 mov ebp,esp
00E413C3 sub esp,0CCh
00E413C9 push ebx
00E413CA push esi
00E413CB push edi
00E413CC lea edi,[ebp+FFFFFF34h]
00E413D2 mov ecx,33h
00E413D7 mov eax,0CCCCCCCCh
00E413DC rep stos dword ptr es:[edi]
接著我們再來看這段指令,是不是感覺有點熟悉呢?對,沒錯 這段指令就是在為Add函式開辟堆疊幀
這里就不再重復贅述了,這里我們直接看執行完后的圖是怎樣的,
int z = 0; 00E413DE mov dword ptr [ebp-8],0 z = x + y; 00E413E5 mov eax,dword ptr [ebp+8] 00E413E8 add eax,dword ptr [ebp+0Ch] 00E413EB mov dword ptr [ebp-8],eax return z; 00E413EE mov eax,dword ptr [ebp-8]這段指令的意思是把0的值放到ebp-8(z)的位置,再將ebp+8的值(也就是之前ecx=10這個值)放到eax里面,然后再將ebp+0ch的值(也就是之前eax=20這個值)加上eax現在的值,最后再將我們eax現在的值放到ebp-8(z)里面,此時我們z的值就是30了,
再然后把ebp-8(也就是z的值)放到ebx里面去,
到這我們就真正清楚了函式是如何傳參的以及傳參的順序,同時也明白了為什么說形參是實參的一份臨時拷貝,
4.函式堆疊幀的銷毀
return z;
00E413EE mov eax,dword ptr [ebp-8]
}
00E413F1 pop edi
}
00E413F2 pop esi
00E413F3 pop ebx
00E413F4 mov esp,ebp
00E413F6 pop ebp
00E413F7 ret
接下來分別對edi,esi,ebx pop也就是將他們彈出堆疊,再將ebp值賦給esp,然后再把ebp彈到原main函式的最下面的地方,esp指向00E41450,此時又回到了main函式里頭,
![]()
最后的ret指令,就是彈出堆疊頂(call指令下一條指令的地址),回到main含函式中call指令發出的地方,從而使得我們能夠回傳去,使得整個程式按流程繼續下去,
00E41450 add esp,8 00E41453 mov dword ptr [ebp-20h],eax printf("%d\n", c);緊接著esp往下加8個位元組,再將eax存放的z值(30)給ebp-20h(c).此時我們的形參就銷毀了,并且z值也回傳了,
到這我想你們對于我剛開始所問的幾個問題心里應該已經有了答案,
如果覺得對你有用的話可以點贊關注一波哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/293656.html
標籤:其他





