函式呼叫過程中函式棧詳解
當程序被載入到記憶體時,會被分成很多段
- 程式碼段:儲存程式文字,指令指標EIP就是指向程式碼段,可讀可執行不可寫,如果發生寫操作則會提示segmentation fault
- 資料段:儲存初始化的全域性變數和靜態變數,可讀可寫不可執行
- BSS:未初始化的全域性變數和靜態變數
- 堆(Heap):動態分配記憶體,向地址增大的方向增長,可讀可寫可執行
- 棧(Stack):存放區域性變數,函式引數,當前狀態,函式呼叫資訊等,向地址減小的方向增長,可讀可寫可執行
- 環境/引數段(environment/argumentssection):用來儲存系統環境變數的一份複製檔案,程序在執行時可能需要。例如,執行中的程序,可以通過環境變數來訪問路徑、shell 名稱、主機名等資訊。該節是可寫的,因此在緩衝區溢位(buffer overflow)攻擊中都可以使用該段
暫存器
EAX:累加(Accumulator)暫存器,常用於函式返回值
EBX:基址(Base)暫存器,以它為基址訪問記憶體
ECX:計數器(Counter)暫存器,常用作字串和迴圈操作中的計數器
EDX:資料(Data)暫存器,常用於乘除法和I/O指標
ESI:源變址暫存器
DSI:目的變址暫存器
ESP:堆疊(Stack)指標暫存器,指向堆疊頂部
EBP:基址指標暫存器,指向當前堆疊底部
EIP:指令暫存器,指向下一條指令的地址
入棧push和出棧pop
push ebp就等於將ebp的值儲存到棧中,並且將當前esp下移
pop ebp就等於將ebp的值從棧中取出來,將ebp指向這個值
下面用一個例子來講函式呼叫過程中棧的變化
int sum(int _a,int _b)
{
int c=0;
c=_a+_b;
return c;
}
int main()
{
int a=10;
int b=20;
int ret=0;
ret=sum(a,b);
return 0;
}
main函式的棧在呼叫之前如圖:
Ok現在講一講ret=sum(a,b);的執行過程
Step 1:
函式引數從右至左入棧
Step 2:
ret=sum(a,b);
call @ILT+0(sum) (00401005) call指令實際上分兩步
push EIP 將下一條指令入棧儲存起來
esp-4 esp指標下移
Step 3:
push ebp 將main函式基指標入棧儲存
mov ebp esp 將esp的值存入ebp也就等於將ebp指向esp
sub esp 44H將esp下移動一段空間建立sum函式的棧棧幀
Step 4:
push ebx
push esi
push edi
lea edi,[ebp-44h] 從ebp-44h的地方開始拷貝
mov ecx,11h 拷貝11次
mov eax,0CCCCCCCCh 拷貝內容為0CCCCCCCCh
rep stos dword ptr [edi] 每次拷貝雙字
Step 5:
int c = 0;
mov dword ptr [ebp-4],0 將sum的區域性變數c放入[ebp-4]的空間內
Step 6:
執行函式操作
Step 7:
return c = 0;
mov eax,dword ptr [ebp-4] 將返回值(變數c所在的地址的內容)放入eax暫存器儲存住
Step 8:
pop edi //將之前入棧的值重新返回給edi暫存器
0040104C pop exi ////將之前入棧的值重新返回給exi暫存器
0040104D pop ebx ////將之前入棧的值重新返回給ebx暫存器
Step 9:
mov esp ebp //將ebp的值賦給esp,也就等於將esp指向ebp,銷燬sum函式棧幀
Step 10:
pop ebp //ebp出棧,將棧中儲存的main函式的基址賦值給ebp
Step 11:
ret //ret相當於pop eip 就是把之前儲存的函式返回地址(也就是main函式中下一條該執行的指令的地址)出棧
Step 12:
add esp,8 //此時若傳入sum函式的引數已經不需要了,我們將esp指標上移
此時函式整個呼叫過程就結束了,main函式棧恢復到了呼叫之前的狀態