楊力祥老師在C++課後給同學留了一道思考題,即探討C++函數調用時其內存的結構究竟是什麼樣的。在參考《程序員的自我修養》的過程中,對於書上的描述有些疑惑,因此自己在VS2008的環境下,對程序1進行了反匯編,並隨著單步調試的進行察看了內存的變化,發現書上給出的圖和描述存在一些小錯誤。在此將實際的過程記錄下來。
//程序1
#include <iostream>
using namespacestd;
int foo(inti){
int a= 1, b= 2;
b = a;
return i;
}
int main(){
int a =foo(0xABCDEF);
return 0;
}
首先要說明的是,程序運行時的實現是根據編譯器的不同而不同的,因此需要強調,本文中討論的所有過程都是在VS2008的編譯環境下進行的,且考察的是debug版本的反匯編。
圖1 運行時的棧結構
圖1 是函數調用時棧的結構,下面來具體說一說匯編指令每一步都做了什麼。
要事先說明的是EBP指針和ESP指針,前者在一個函數的運行過程中(沒有調用其他函數時)是一個固定值,它指向的位置如圖1所示;後者是棧頂指針。
序
操作方
對應匯編
內存操作
1
main
函數
push 0ABCDEFh
函數參數壓棧
ESP指向位置1
2
main
函數
call foo (11C1028h)
在調用的時候,會自動將調用函數之後下一條要執行的指令地址(也就是所謂的返回地址)壓棧
ESP指向位置2
3
foo
函數
push ebp
mov ebp,esp
將舊的EBP值壓棧;
ESP指向位置3
將當前ESP值賦給EBP,所以EBP也指向位置3
4
foo
函數
sub esp,0D8h
在棧上給局部變量預留空間
ESP指向位置4
5
foo
函數
push ebx
push esi
push edi
寄存器壓棧保存
ESP指向位置5
6
foo
函數
lea edi,[ebp-0D8h]
mov ecx,36h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
將位置4的地址值賦給EDI寄存器;
將位置4到位置3之間的內存全部賦值為0CCCCCCCCh(由低到高)
此時ESP指向位置5
7
foo
函數
mov dword ptr [a],1
mov dword ptr [b],2
通過指針訪問,將局部變量區域中的a和b賦值;
注意:a和b並不是連續存放在棧中的,也沒有將局部變量的空間占滿。
ESP位置不變
8
foo
函數
mov eax,dword ptr [a]
mov dword ptr [b],eax www.2cto.com
通過EAX寄存器來完成局部變量的賦值操作;依然是指針訪問。
ESP位置不變
9
foo
函數
mov eax,dword ptr [i]
8,9,10三步實現foo函數返回;
通過EAX寄存器來傳遞返回值
ESP依然指向位置5,即最後一個被壓棧的寄存器
10
foo
函數
pop edi
pop esi
pop ebx
將此前壓棧的寄存器出棧
ESP指針指向位置4
11
foo
函數
mov esp,ebp
pop ebp
將EBP值賦給ESP指針,意味著釋放掉了局部變量的內存空間
ESP指向位置3
將此前壓棧的舊EBP值出棧,賦給EBP
ESP指向位置2
12
foo
函數
ret
函數返回,會根據此前壓棧的返回地址跳轉到下一條指令的位置
ESP指向位置1
13
main
函數
add esp,4
釋放此前壓棧的foo函數參數,至此,foo函數徹底結束了它的生命
ESP指向位置0
14
main
函數
mov dword ptr [a],eax
通過EAX傳遞返回值,將返回值賦給變量a;
main函數中有關調用foo函數的內容至此結束。
需要說明的是,在函數完成了現場保護之類的初始化工作之後,ESP會始終指向當前函數的棧空間頂,此時,若當前函數又調用了另一個函數,則會將此時的EBP視為舊EBP壓棧,而與新調用的函數有關的內容則會從當前ESP所指向位置開始壓棧。