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前的位置;

示意圖如下所示
funcstack

簡單例子

函數調用示例代碼

一個簡單的函數調用例子

#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函數的棧幀情況如下所示
    main stack

  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函數的棧示意圖如下:
use add stack

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 stack

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM