目錄
一.函數棧幀的概念
二.理解函數棧幀能讓我們解決什么問題
三.相關寄存器和匯編指令知識點補充
四.函數棧幀的創建和銷毀
4.1.調用堆棧
4.2.函數棧幀的創建
4.3 函數棧幀的銷毀
?一.函數棧幀的概念
--在C語言中,函數棧幀是指在函數調用過程中,在內存棧中為該函數分配的一塊空間,用于存儲函數的局部變量,參數,返回地址等信息。
棧幀的結構:
- 參數區:用于存放調用函數時傳遞給被調用函數的參數。
- 返回地址:記錄函數調用結束后要返回的指令地址,以便函數執行完畢后能正確回到調用點繼續執行。
- 局部變量區:存儲函數內部定義的局部變量。
- ebp和esp相關區域:ebp指向當前棧幀的底部,esp指向當前棧幀的頂部,通過這兩個指針來維護函數棧幀。
二.理解函數棧幀能讓我們解決什么問題
--在前期的學習中,我們可能會產生很多困惑
比如:
- 局部變量是怎么創建的
- 為什么局部變量的值是隨機值
- 函數是怎么傳參的?傳參的順序是怎樣的?
- 形參和實參是什么關系?
- 函數調用是怎么做的?
- 函數調用結束后是怎么返回的?
當我們理解函數棧幀的創建和銷毀后,我們就可以更好的去解決這些問題,如同修練自己的內功,也方便在后期能搞懂更多的知識。
三.相關寄存器和匯編指令知識點補充
相關寄存器:
- eax:通用寄存器,保留臨時數據,常用于函數返回值
- ebx:通用寄存器,保留臨時數據
- eip:指令寄存器,用于存儲下一條要執行的指令的地址
- ebp:棧底寄存器
- esp:棧頂寄存器
相關匯編指令:
- push:將操作數壓入棧中,棧頂指針esp也會相應調整
- pop:從棧中彈出數據到指定的位置,棧頂指針esp也會相應調整
- mov:數據傳送指令,用于在寄存器之間,寄存器與內存之間傳送數據
- add:加法指令,用于將兩個操作數相加,結果存放于指定的寄存器中
- sub:減法指令,用于將兩個操作數相減,結果存放于指定的寄存器中
- call:過程調用,壓入返回地址或轉入調用函數
- lea:加載有效地址指令,將操作數的地址加載到指定的寄存器中
- ret:返回地址指令,回到調用位置
四.函數棧幀的創建和銷毀
4.1.調用堆棧
代碼如下:
#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;
}
這段代碼我們在vs2022上調試的話,調試進入add函數后,我們就可以觀察到函數的調用堆棧(右擊勾選,顯示外部代碼)?,如下圖所示
函數調用堆棧是可以反饋函數調用邏輯的,我們可以清晰的觀察到,是由invoke_main函數來調用main函數的?,在此之間的我們就不過多的去考慮了,我們接下來直接從main函數的棧幀創建開始。
4.2.函數棧幀的創建
--當函數每次被調用時,系統都會在棧上為該函數分配一塊棧幀空間。首先將調用函數的相關信息,如參數,返回地址等壓入棧中,然后調整ebp和esp,為局部變量分配空間
我們先將main函數轉到反匯編--調試到main函數第一行時,右鍵鼠標轉到反匯編,反匯編代碼如下
int main()
{
//函數棧幀的創建
005518D0 push ebp
005518D1 mov ebp,esp
005518D3 sub esp,0E4h
005518D9 push ebx
005518DA push esi
005518DB push edi
005518DC lea edi,[ebp-24h]
005518DF mov ecx,9
005518E4 mov eax,0CCCCCCCCh
005518E9 rep stos dword ptr es:[edi]
005518EB mov ecx,55C008h
005518F0 call 0055132F
005518F5 nop
//main函數中的主要代碼int a = 10;
005518F6 mov dword ptr [ebp-8],0Ah int b = 20;
005518FD mov dword ptr [ebp-14h],14h int c = 0;
00551904 mov dword ptr [ebp-20h],0 c = Add(a, b);
0055190B mov eax,dword ptr [ebp-14h]
0055190E push eax
0055190F mov ecx,dword ptr [ebp-8]
00551912 push ecx
00551913 call 005510B9
00551918 add esp,8
0055191B mov dword ptr [ebp-20h],eax
------------------------------------------------------------ printf("%d\n", c);
0055191E mov eax,dword ptr [ebp-20h]
00551921 push eax
00551922 push 557B30h
00551927 call 005510D7
0055192C add esp,8 return 0;
0055192F xor eax,eax
}
?我們可以將上面main函數的函數棧幀創建過程的主要部分單獨拆解出來看看,代碼如下
005518D0 push ebp
//把ebp寄存器中的值進行壓棧,到了esp-4的位置,此時的ebp中存放的是invoke_main函數棧幀的ebp
005518D1 mov ebp,esp
//將esp的值存放到ebp中,相當于ebp來到了invoke_main函數棧幀的esp位置,產生了main函數的ebp
005518D3 sub esp,0E4h
//將esp中的地址減去一個16進制數字0E4h,esp向上移動,產生了新的esp,也就是main函數的esp
//結合上面產生的ebp之后,ebp和esp之間就維護了一塊為main函數開辟的棧幀空間
005518D9 push ebx
//將寄存器ebx中的值壓棧,esp-4,esp向上移動
005518DA push esi
//將寄存器epi中的值壓棧,esp-4,esp繼續向上移動
005518DB push edi
//將寄存器edi中的值壓棧,esp-4,esp接著向上移動
005518DC lea edi,[ebp-24h]
005518DF mov ecx,9
005518E4 mov eax,0CCCCCCCCh
005518E9 rep stos dword ptr es:[edi]
//以上這四串代碼是在初始化main函數的棧幀空間
//1.先將ebp-24h的地址加載到edi中
//2.將9放入ecx中
//3.將0xCCCCCCCC放入eax中
//4.將從ebp-24h到ebp之間ecx個4個字節的數字初始化為0xCCCCCCCC
?接下來再來分析main函數中的主要代碼
int a = 10;
005518F6 mov dword ptr [ebp-8],0Ah
//將0Ah存儲到ebp-8這個地址中,ebp-8的位置其實就是變量aint b = 20;
005518FD mov dword ptr [ebp-14h],14h
//將14h存儲到ebp-14h這個地址中,ebp-14h的位置其實就是變量bint c = 0;
00551904 mov dword ptr [ebp-20h],0
//將0存儲到ebp-20h這個地址中,ebp-20h的位置其實就是變量c
以上就是局部變量在其所在函數的棧幀空間中創建和初始化的過程c = Add(a, b);
0055190B mov eax,dword ptr [ebp-14h]
// 先傳參b,將ebp-14h位置中b的值存儲到eax中
0055190E push eax
//將eax的值壓棧,esp-4,向上移動
0055190F mov ecx,dword ptr [ebp-8]
//再傳參a,將ebp-8位置中a的值存儲到ecx中
00551912 push ecx
//將ecx的值壓棧,esp-4,繼續向上移動 //跳轉調用函數
00551913 call 005510B9
//call指令會將call指令的下一條指令的地址進行壓棧操作
//這樣做可以讓函數調用結束后回到call的下一條指令地址后繼續執行
00551918 add esp,8
0055191B mov dword ptr [ebp-20h],eax
call指令會執行函數調用邏輯,這個時候我們會跳轉到Add函數中,我們再來觀察Add函數的反匯編代碼
int Add(int x, int y)
{
00551790 push ebp
00551791 mov ebp,esp
00551793 sub esp,0CCh
00551799 push ebx
0055179A push esi
0055179B push edi
0055179C lea edi,[ebp-0Ch]
0055179F mov ecx,3
005517A4 mov eax,0CCCCCCCCh
005517A9 rep stos dword ptr es:[edi]
005517AB mov ecx,55C008h
005517B0 call 0055132F
005517B5 nop int z = 0;
005517B6 mov dword ptr [ebp-8],0 z = x + y;
005517BD mov eax,dword ptr [ebp+8]
005517C0 add eax,dword ptr [ebp+0Ch]
005517C3 mov dword ptr [ebp-8],eax return z;
005517C6 mov eax,dword ptr [ebp-8]
}
005517C9 pop edi
005517CA pop esi
005517CB pop ebx
005517CC add esp,0CCh
005517D2 cmp ebp,esp
005517D4 call 00551253
005517D9 mov esp,ebp
005517DB pop ebp
005517DC ret
代碼執行到Add函數的時候就要開始創建Add函數的棧幀空間了,與前面main函數的棧幀空間創建過程差不多,這里就不詳細講述了,主要是計算求和的時候我們通過偏移訪問,訪問到了函數調用前壓棧進去的參數,這就是形參訪問,很好說明了形參就是實參的一份臨時拷貝,最后將求出的和通過eax寄存器中帶回。
z = x + y;
005517BD mov eax,dword ptr [ebp+8]
// 將ebp+8地址處的數字存儲到eax中
005517C0 add eax,dword ptr [ebp+0Ch]
// 將ebp+12地址處的數字加到eax寄存中
005517C3 mov dword ptr [ebp-8],eax
//將eax的結果保存到ebp-8的地址處,其實就是放到z中return z;
005517C6 mov eax,dword ptr [ebp-8]
//將ebp-8地址處的值放在eax中,其實就是把z的值存儲到eax寄存器中,通過eax寄存器帶回計算的結果
4.3 函數棧幀的銷毀
--函數執行完畢后,棧幀被銷毀,通過恢復ebp和esp的值,釋放棧幀空間,將控制權返回給調用函數,繼續執行調用函數中調用之后的代碼
當函數調用結束后,前面創建的函數棧幀要銷毀,我們來看看Add函數的這部分反匯編代碼吧
005517C9 pop edi //在棧頂彈出一個值,存放到edi中,esp+4,向下移
005517CA pop esi //在棧頂彈出一個值,存放到esi中,esp+4,向下移
005517CB pop ebx //在棧頂彈出一個值,存放到ebx中,esp+4,向下移
005517CC add esp,0CCh //esp+0cch,向下移
005517D2 cmp ebp,esp //esp的值給ebp,ebp來到了esp的位置
005517D4 call 00551253
005517D9 mov esp,ebp //再將ebp的值給了esp,回收了Add函數的棧幀空間
005517DB pop ebp
//彈出棧頂的值存放到ebp,棧頂此時的值恰好就是main函數的ebp,esp+4,恢復了main函數棧幀空間的維護
005517DC ret
//ret指令的執行,首先是從棧頂彈出一個值,此時棧頂的值就是call指令下一條指令的地址,此時esp+4,向下移動,然后直接跳轉到call指令下一條指令的地址處,繼續往下執行
回到了call指令下一條指令的地方
調用完后繼續回到main函數后繼續執行這兩串代碼,函數的返回值通過eax帶了出來,其中就是x+y的和z,也就是a+b的和c。
結語:本篇文章就到此結束了,對于函數棧幀的創建與銷毀,個人能力有限,歡迎大家進行補充,一起交流學習,感謝大家的支持!