C語言中關于棧幀:由于在函數調用時往往會形成棧幀結構,為此我們經常有以下幾個疑問:
1.隻要給函數傳遞參數就會形成臨時變量,這些臨時變量會存在棧上,具體怎樣存的?
2.函數内部定義的變量叫局部變量(自動變量),這些變量調用時建立,調用完成後自動釋放,為什麼?
3.函數調用完成之後應該傳回到原來調用的地方,那之前要做什麼?
4.函數傳回時的臨時變量存在哪裡?
關于了解棧幀,我們需要知道一些預備知識:
1.寄存器:具有存儲功能的離cpu最近的存儲單元,速度最快
(1)ebp:基址寄存器(棧底)寄存器
(2)esp:棧頂寄存器
(3)pc指針(程式計數器):指向目前正在執行指令的下一條指令
(4)eax,ebx...通用寄存器
2.cpu的任務:(1)取指令(從記憶體取到cpu内部) (2)分析指令(根據指令集、操作數) (3)執行指令
3.指令集:精簡指令集、複雜指令集
4.任何一個函數都有屬于自己的ebp(棧底)、esp(棧頂);但是記憶體中的ebp、esp隻有一份;為了保證一個函數不會覆寫另一個函數的ebp、esp,調用時必須對原來函數
的ebp(棧底)、esp(棧頂)進行儲存;因為ebp(棧底)、esp(棧頂)永遠指向目前函數最新的棧底、棧頂;棧是從(位址減小)的方向生長
6.函數調用時,相關寄存器内容必須儲存,目的是調用完之後恢複(程式切換也要儲存,隻是os的問題)
7.call指令的任務:
(1)跳轉到被調用函數的入口處(通過調整pc指針完成)
(2)儲存目前正在執行指令的下一條指令的位址到棧中
8.push:把資料壓到棧頂;pop把棧頂資料拉出來放在指定寄存器裡
9.形參執行個體化時從右往左順序進行的
10.将資料壓入棧頂時,先移動指針再放資料,保證棧頂指針永遠指向一個有效的資料
11.調用函數時,先形成實參的臨時變量,再執行call指令
12.第一個參數與函數傳回位址相鄰;
13.先将棧底位址儲存,再将棧頂内容指派給棧底,(棧底指向現在的棧頂)
為了講解清楚棧幀,我們以下面一個小小的程式為例:
#include <stdio.h>
#include <windows.h>
int fun(int x,int y)
{
int c=0xcccccccc;
}
int main()
{
int a=0xaaaaaaaa;
int b=0xbbbbbbbb;
int ret=fun(a, b);
printf("you should running here\n");
system("pause");
return 0;
}
具體執行過程,我們以指令在計算機内部過程來剖析:
開始,寄存器ebp、esp分别指向main函數的棧底、棧頂(main函數是入口),pc指針指向main函數的代碼區;
(1)定義變量a,b;反彙編:dword ptr [a],0AAAAAAAAh ,dword ptr [b],0BBBBBBBBh
(2)開始調用:但是調用之前要形參執行個體化形成臨時變量:(注意形參從右往左進行執行個體化)
mov eax,dword ptr [b] :把變量b存到通用寄存器eax中
push eax :把eax中(b)壓入堆棧中
mov ecx,dword ptr [a] :把變量b存到通用寄存器ecx中
push ecx :把ecx中(b)壓入堆棧中
使用call指令開始調用函數:call @ILT+295(_fun) (0F9112Ch) ;注意call指令完成兩個任務:(1)跳轉到被調用函數的入口處(通過調整pc指針完成)
(3)儲存目前正在執行指令的下一條指令的位址到棧中
add esp,8 :儲存目前正在執行指令的下一條指令的位址到棧中
mov dword ptr [ret],eax :跳轉到被調用函數的入口處
(4)這時已經來到fun函數:但是現在系統的ebp、esp還指向原來main函數的棧底和棧頂,顯然我們應該馬上修改二者的值:
push ebp :把ebp(main函數的棧底内容)中的内容壓入棧頂
mov ebp,esp :把esp内容放到ebp中,是以上面先儲存ebp内容;此時顯示的棧底指向棧頂
sub esp,0CCh :棧頂向下移動
現在的棧底、棧頂所組成的部分為被調用函數的棧幀結構
(4)恢複棧幀結構:
mov esp,ebp :把ebp裡内容複原給esp,恢複棧底
pop ebp :然後把棧頂内容傳回給ebp,恢複棧頂
ret :恢複pc指針的指向(指向main函數的函數傳回位址)
add esp,8 :棧頂向上移動,釋放實參執行個體化的臨時變量
以上的執行過程用簡單的圖示描述如下: