1. 程式人生 > >函式呼叫過程中函式棧詳解

函式呼叫過程中函式棧詳解

當程序被載入到記憶體時,會被分成很多段

  1. 程式碼段:儲存程式文字,指令指標EIP就是指向程式碼段,可讀可執行不可寫,如果發生寫操作則會提示segmentation fault
  2. 資料段:儲存初始化的全域性變數和靜態變數,可讀可寫不可執行
  3. BSS:未初始化的全域性變數和靜態變數
  4. 堆(Heap):動態分配記憶體,向地址增大的方向增長,可讀可寫可執行
  5. 棧(Stack):存放區域性變數,函式引數,當前狀態,函式呼叫資訊等,向地址減小的方向增長,可讀可寫可執行
  6. 環境/引數段(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函式棧恢復到了呼叫之前的狀態