C++函式呼叫棧的變化分析
阿新 • • 發佈:2020-09-13
程式中棧的基礎知識
棧是向下生長的
向下生長指的是從記憶體的高地址-->低地址的方向拓展。
棧有棧底和棧頂,從上面可以知道棧頂的地址是比棧底的要低的。
對於X86體系的CPU而言,大概需要知道以下基礎知識:
- ebp暫存器:一般叫做基址指標或者幀指標;
- esp暫存器:一般叫做棧指標
- ebp在沒有改變之前始終指向棧底,ebp主要用於在堆疊中定址
- esp會隨著資料入棧和出棧變化,esp始終指向棧頂
函式呼叫的過程描述
若函式A呼叫函式B,那麼A函式一般叫做呼叫者,B函式一般為被呼叫者,函式呼叫過程可以做如下描述
- 現將函式A的堆疊基址ebp入棧,用於儲存之前任務資訊
- 然後將函式A的棧頂指標esp
- 緊接著在新的ebp基礎上開闢相應的空間當做被呼叫者B的棧空間,開闢空間一般用sub指令;
- 函式B返回後,從當前棧底ebp恢復為呼叫者A的棧頂esp,使得棧頂恢復成函式B被呼叫前的位置;
- 最後呼叫者A從恢復的棧頂彈出之前的ebp值(因為在函式呼叫前一步被壓入堆疊);這樣ebp和esp都變成了呼叫函式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"; }
函式呼叫過程彙編解析
-
在main函式呼叫Add函式之前,main函式的棧幀情況如下所示
-
當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
- 從呼叫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;為當前棧幀開闢相應的空間
- main函式進入Add函式的示意圖如下所示
當Add函式執行完之後,將執行ret 指令返回,並且esp指向Add函式棧幀底部(就是main 函式棧幀頂部), 緊接著就是從彈出儲存的ebp恢復現場,這樣就回到了呼叫Add函式之前的狀態。