1. 程式人生 > 實用技巧 >C++函式呼叫棧的變化分析

C++函式呼叫棧的變化分析

程式中棧的基礎知識

棧是向下生長的

向下生長指的是從記憶體的高地址-->低地址的方向拓展。

棧有棧底和棧頂,從上面可以知道棧頂的地址是比棧底的要低的。

對於X86體系的CPU而言,大概需要知道以下基礎知識:

  1. ebp暫存器:一般叫做基址指標或者幀指標
  2. esp暫存器:一般叫做棧指標
  3. ebp在沒有改變之前始終指向棧底,ebp主要用於在堆疊中定址
  4. esp會隨著資料入棧和出棧變化,esp始終指向棧頂

函式呼叫的過程描述

若函式A呼叫函式B,那麼A函式一般叫做呼叫者,B函式一般為被呼叫者,函式呼叫過程可以做如下描述

  1. 現將函式A的堆疊基址ebp入棧,用於儲存之前任務資訊
  2. 然後將函式A的棧頂指標esp
    的值賦給ebp,用作新的基址(這裡就是函式B的棧底)
  3. 緊接著在新的ebp基礎上開闢相應的空間當做被呼叫者B的棧空間,開闢空間一般用sub指令;
  4. 函式B返回後,從當前棧底ebp恢復為呼叫者A的棧頂esp,使得棧頂恢復成函式B被呼叫前的位置;
  5. 最後呼叫者A從恢復的棧頂彈出之前的ebp值(因為在函式呼叫前一步被壓入堆疊);這樣ebpesp都變成了呼叫函式B前的位置;

示意圖如下所示

簡單例子

函式呼叫示例程式碼

一個簡單的函式呼叫例子

#include <iostream>

int __cdecl Add(int a, int b)
{
    return a + b;
}

int main()
{
    auto res = Add(2, 3);
    std::cout << "2 + 3 = " << res << std::endl;
    std::cout << "Hello World!\n";
}

函式呼叫過程彙編解析

  1. main函式呼叫Add函式之前,main函式的棧幀情況如下所示

  2. 當main函式呼叫Add函式的時候,彙編如下

    auto res = Add(2, 3);

00E12618  push        3  

00E1261A  push        2  

00E1261C  call        Add (0E111D6h)  

00E12621  add         esp,8  

00E12624  mov         dword ptr [res],eax  

  1. 從呼叫Add函式的組合語言中大概可以得出呼叫函式的大概模式就是如下:
push parameter_n
push parameter_...
push parameter_1

call funcName; 呼叫函式funcName, 加你個返回地址填入棧,並且跳轉到funcName

main函式呼叫Add函式的棧示意圖如下:

call        Add (0E111D6h) 進入Add函式之後,組合語言如下所示

int __cdecl Add(int a, int b)
{

00E12300  push        ebp  

00E12301  mov         ebp,esp  

00E12303  sub         esp,0C0h  

00E12309  push        ebx  

00E1230A  push        esi  

00E1230B  push        edi  

00E1230C  lea         edi,[ebp-0C0h]  

00E12312  mov         ecx,30h  

00E12317  mov         eax,0CCCCCCCCh  

00E1231C  rep stos    dword ptr es:[edi]  

00E1231E  mov         ecx,offset _44E0C52E_AnalyseFunc@cpp (0E1F026h)  

00E12323  call        @__CheckForDebuggerJustMyCode@4 (0E11280h)  

    return a + b;

00E12328  mov         eax,dword ptr [a]  

00E1232B  add         eax,dword ptr [b]  

}

00E1232E  pop         edi  

00E1232F  pop         esi  

00E12330  pop         ebx  

00E12331  add         esp,0C0h  

00E12337  cmp         ebp,esp  

00E12339  call        __RTC_CheckEsp (0E1128Ah)  

00E1233E  mov         esp,ebp  

00E12340  pop         ebp  

00E12341  ret  

在Add函式的組合語言中可以看到開始的前3句,這裡做如下解釋

00E12300  push        ebp; 進入新的函式,新函式也需要一個棧幀了,就必須將main函式的棧幀底部全部儲存起來,棧頂則是作為一個新函式的棧底

00E12301  mov         ebp,esp;上一個棧幀頂部就是這個棧幀的底部

00E12303  sub         esp,0C0h;為當前棧幀開闢相應的空間
  1. main函式進入Add函式的示意圖如下所示

當Add函式執行完之後,將執行ret 指令返回,並且esp指向Add函式棧幀底部(就是main 函式棧幀頂部), 緊接著就是從彈出儲存的ebp恢復現場,這樣就回到了呼叫Add函式之前的狀態。