1. 程式人生 > >堆和棧詳解

堆和棧詳解

對堆疊在程式中的作用有更深入的瞭解。不同的語言有不同的函式呼叫規定,這些因素有引數的壓入規則和堆疊的平衡。windows API的呼叫規則和ANSI C的函式呼叫規則是不一樣的,前者由被調函式調整堆疊,後者由呼叫者調整堆疊。兩者通過“__stdcall”和“__cdecl”字首區分。先看下面這段程式碼:

#include <stdio.h>

void __stdcall func(int param1,int param2,int param3)

{

int var1=param1;

int var2=param2;

int var3=param3;

printf("0x%08x\n",?m1); //打印出各個變數的記憶體地址

printf("0x%08x\n",?m2);

printf("0x%08x\n\n",?m3);

printf("0x%08x\n",&var1);

printf("0x%08x\n",&var2);

printf("0x%08x\n\n",&var3);

return;

}

int main()

{

func(1,2,3);

return 0;

}

編譯後的執行結果是:

0x0012ff78

0x0012ff7c

0x0012ff80

0x0012ff68

0x0012ff6c

0x0012ff70

├———————┤<—函式執行時的棧頂(ESP)、低端記憶體區域

│ …… │

├———————┤

│ var 1 │

├———————┤

│ var 2 │

├———————┤

│ var 3 │

├———————┤

│ RET │

├———————┤<—“__cdecl”函式返回後的棧頂(ESP)

│ parameter 1 │

├———————┤

│ parameter 2 │

├———————┤

│ parameter 3 │

├———————┤<—“__stdcall”函式返回後的棧頂(ESP)

│ …… │

├———————┤<—棧底(基地址 EBP)、高階記憶體區域

上圖就是函式呼叫過程中堆疊的樣子了。首先,三個引數以從又到左的次序壓入堆疊,先壓“param3”,再壓“param2”,最後壓入“param1”;然後壓入函式的返回地址(RET),接著跳轉到函式地址接著執行(這裡要補充一點,介紹UNIX下的緩衝溢位原理的文章中都提到在壓入RET後,繼續壓入當前EBP,然後用當前ESP代替EBP。然而,有一篇介紹windows下函式呼叫的文章中說,在windows下的函式呼叫也有這一步驟,但根據我的實際除錯,並未發現這一步,這還可以從param3和var1之間只有4位元組的間隙這點看出來);第三步,將棧頂(ESP)減去一個數,為本地變數分配記憶體空間,上例中是減去12位元組(ESP=ESP-3*4,每個int變數佔用4個位元組);接著就初始化本地變數的記憶體空間。由於“__stdcall”呼叫由被調函式調整堆疊,所以在函式返回前要恢復堆疊,先回收本地變數佔用的記憶體(ESP=ESP+3*4),然後取出返回地址,填入EIP暫存器,回收先前壓入引數佔用的記憶體(ESP=ESP+3*4),繼續執行呼叫者的程式碼。參見下列彙編程式碼:

;--------------func 函式的彙編程式碼-------------------

:00401000 83EC0C sub esp, 0000000C //建立本地變數的記憶體空間

:00401003 8B442410 mov eax, dword ptr [esp+10]

:00401007 8B4C2414 mov ecx, dwo