Win32 环境下的堆栈
原文已經找不到,作者應該是:http://blog.csdn.net/slimak?? 但是沒有找到此文,其中丟了2幅圖
簡介
在Win32環境下利用調試器調試應用程序的時候經常要和堆棧(Stack)打交道,尤其是在需要手工遍歷堆棧(Manually Walking Stack)的時候我們需要對堆棧的工作過程有一個比較清晰的了解.接下來的這些文字將通過一個例子程序詳細的講解堆棧的工作過程.
關鍵字
調試堆棧 Stack Stack-Frame
目錄
1.堆棧是什么?
2.堆棧里面放的都是什么信息?
3.堆棧是在什么時候被建立起來的?它的默認大小是多少?
4.默認才1M??那要是我的程序使用超過了1M的堆棧怎么辦?
5.什么叫Stack Frame?
6.在一次函數調用中,堆棧是如何工作的?
7.老大,結合一個例子講講吧?
1.堆棧是什么?
從內存管理角度看,堆棧是就是一塊連續的內存空間,對它的操作采用先入后出的規則,他的生長方向與內存的生長方向正好相反,也就是說它是從高地址向低地址生長.
從Win32程序內部的角度看,每一個線程有自己的堆棧,它主要用來給線程提供一個暫時存放數據的區域,程序使用POP/PUSH指令來對堆棧進行操作.
2.堆棧里面放的都是什么信息?
堆棧中存放的信息包括:
當前正在執行的函數的局部變量;
函數返回地址;
該函數的上層函數傳給該函數的參數;
EBP的值;
一些通用寄存器(EDI,ESI…)的值。
?
注意這里提到的正在執行的函數,比如有下面的一段C代碼:
void B()
{
printf(“B\n”);
}
void A()
{
B();
}
那么當程序執行到B函數的printf函數的時候我們說正在執行的函數包括A和B而不僅僅是B函數,這一點需要注意.
3.堆棧是在什么時候被建立起來了?它的默認大小是多少?
堆棧是在我們的main主函數被系統調用之前被建立起來的,對于非主線程它是在線程被建立之前創建的,
它的默認大小是1M,
如果需要修改堆棧的大小的話可以在VC6++中通過使用/STACK編譯項實現:
#pragmacomment(linker,“/STACK:2048,1024″)//預約(Reserve)2M,提交(Commit)1M
關于預約(Reserve)和提交(Commit)的概念請參看”Programming Applications for Microsoft Windows“( Jeffrey Richter,Chapter 15Using Virtual Memory in Your Own Applications)
4.默認才1M??那要是我的程序使用超過了1M的堆棧怎么辦?
系統通過使用異常捕獲(Exception Handling)機制來捕獲應用程序企圖去訪問超過該程序提交(Commit)的堆棧范圍這種異常,假如你程序預約了2M并且提交了1M大小的堆棧,那么當你的程序企圖訪問超過1M的范圍的時候會產生一個異常并且被系統捕獲,系統會幫你繼續從另外1M預約的內存中提交內存來滿足你的需求,如果你要求提交的大小甚至超過了2M(你一開始預約的大小)在 NT系統下(98除外)系統也會嘗試去分配(allocate)內存來滿足你,但是系統并不保證分配會成功
5.什么叫Stack Frame?
Stack Frame這個詞你可以在各種各樣的匯編書籍中看到,到底它表示什么意思呢?也許你看完文章的后半部分就會明白,在此我們先給它一個定義,你看完整篇文章在回過頭來回味一下就會知道它的確切含義了,Stack Frame是堆棧中的一塊區域,它保存著一個函數的返回地址,和該函數內部使用的局部數據(Local Data),它是由函數入口處的SUB ESP,48h之類的語句來建立的.
6.在一次函數調用中,堆棧是如何工作的?
假設我們的主角叫A函數…
a.首先上級函數傳給A函數的參數被壓入堆棧中(至于是誰來做這個壓棧操作取決于A函數的調用方式:是__stdcall, __cdecl還是其他);
b.然后是返回地址(A函數執行完后接下來程序繼續執行的地址)入棧;
c.接下來是當前的EBP;
d.如果A函數有局部變量,就在堆棧中開辟相應的空間以構造那些變量變量(A函數執行結束,這些局部變量的內容將被忽略/遺棄,但是不被清除,比如A函數中有一個變量int m存在于地址0×0012FFCC處,函數結束時9依然存在于0×0012FFCC處沒有被清除,但是此時它已經沒有任何意義了,
e.在函數返回的時候,彈出EBP,恢復堆棧到函數調用前的地址,彈出返回地址到EIP以繼續執行程序。
7.結合一個例子
下面就是我們要拿來做模特的代碼,程序很簡單,wWinMain調用AFunc,AFunc再調用BFunc,下面的講解過程中我們要觀摩這個程序的匯編代碼形式,可以通過在VC6++該工程的Debug模式中按F5然后Ctrl+Tab做到,我想這對于Win32程序員應該不是難事.
int BFunc(int i,int j)
{
int m = 1;
int n = 2;
m = i;
n = j;
return m;
}
int AFunc(int i,int j)
{
int m = 3;
int n = 4;
m = i;
n = j;
BFunc(m,n);
return 8;
}
int APIENTRY wWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
AFunc(5,6);
return 0;
}
步驟1.我們從wWinMain調用AFunc函數開始
wWinMain調用AFunc的時候,先把參數壓棧(至于為什么壓棧順序是6,5而不是5,6請參看附錄.注解1)參數壓棧結束后此時ESP = 0×0012FEDC,EBP = 0×0012FF30,
這是進入AFunc函數之前的堆棧形勢圖:
圖 1
步驟2.記住進入AFcun函數之前的ESP,EBP的值,然后我們進入AFunc…
為方便大家觀摩,先把AFunc函數的全貌貼出來
29: int AFunc(int i,int j)
30: {
004010D0push ebp ;先把EBP入棧保存
004010D1mov ebp,esp ;再把此時的ESP賦給EBP,這樣EBP就可以拿來訪問本函數的局部變量
004010D3sub esp,48h ;為AFunc函數在堆棧重開辟一塊空間,一般來說開辟的空間大小是40+
;函數內所有局部變量的大小;
004010D6push ebx ;通用寄存器入棧,算保存現場吧
004010D7push esi
004010D8push edi
004010D9lea edi,[ebp-48h]
004010DCmov ecx,12h
004010E1mov eax,0CCCCCCCCh
004010E6rep stos dword ptr [edi]
31: int m = 3;
004010E8mov dword ptr [ebp-4],3 ;為什么局部變量m位于ebp-3處?
32: int n = 4;
004010EFmov dword ptr [ebp-8],4;為什么局部變量n位于ebp-8處?
33:
34: m = i;
004010F6mov eax,dword ptr [ebp+8] ;ebp+8處存的是什么?
004010F9mov dword ptr [ebp-4],eax
35: n = j;
004010FCmov ecx,dword ptr [ebp+0Ch] ;ebp+0ch處存的是什么?
004010FFmov dword ptr [ebp-8],ecx
36:
37: BFunc(m,n);
00401102mov edx,dword ptr [ebp-8] ;AFunc調用BFunc之前先把傳給BFunc的參數入棧
00401105push edx
00401106mov eax,dword ptr [ebp-4]
00401109push eax
0040110Acall @ILT+25(BFunc) (0040101e)
0040110Fadd esp,8 ;這個出棧操作為什么?
38:
39:return 8;
00401112mov eax,8
40: }
00401117pop edi ;恢復現場
00401118pop esi
00401119pop ebx
0040111Aadd esp,48h ;收回函數一開始在棧中開辟的空間
;對應于一開始的sub esp,48h
0040111Dcmp ebp,esp
0040111Fcall __chkesp (00401220)
00401124mov esp,ebp
00401126pop ebp;恢復調用前的EBP
00401127ret
下面我們要花些篇幅詳細的解釋AFunc函數執行過程堆棧(主要是ESP,EBP)的變化情況:
29: int AFunc(int i,int j)
30: {
004010D0 push ebp
004010D1 mov ebp,esp
004010D3 sub esp,48h
004010D6 push ebx
004010D7 push esi
004010D8 push edi
;
; 上面幾行代碼叫做prolog,可以理解成”序曲,開始部分”,與之對應的叫epilog(結束曲,結束部分)對于
; prolog需要逐行解釋一下:
;
; 004010D0 PUSH EBP
; 將進入AFunc函數之前的EBP的值入棧保存,這時候的EBP相當于是AFunc上級函數
; 的一個現場信息,所以需要保存起來,以便于AFunc返回后上級函數可以恢復EBP使其指向其調用
; AFunc之前的堆棧位置(當然,這還需要靠恢復ESP來協助達到這一目的),該語句執行完之后堆棧將
; 變成下面這個樣子
;
圖 2
; 在這里要解釋一下什么時候”AFunc結束之后的返回地址”入棧了?導致它入棧的語句就是
; CALL @ILT+20(AFunc) (00401019)
; 也就是說是CALL指令干的
;
; 004010D1 MOV EBP,ESP
; ESP賦給EBP,這樣EBP就可以拿來訪問本函數的局部變量
圖 3
; 004010D3 SUB ESP,48h AFunc函數中有兩個int型的變量所以開辟的空間大小是
; 40+2*sizeof(int),我暫時還沒有找到正式文檔中對于此大小
; 計算的公式.注意:ESP-48h后開辟的新的堆棧中的這塊空間就是
; 大名鼎鼎的Stack Frame.
; 004010D6 PUSH EBX 我們知道通用寄存器有時候在程序運算的時候可以用來存放
; 臨時結果,如果此結果有必要的話也是需要作為現場信息被保存在
; 堆棧中的.
; 004010D7 PUSH ESI
; 004010D8 PUSH EDI
圖 4
; 從上面的圖解我們很容易看出在進入AFunc函數執行完prolog之后ESP和EBP指示出了堆棧中
; 存放的當前執行函數的信息(綠色部分,其上級函數的堆棧信息由亮綠色表示,呵呵,我可能有一點色
; 弱所以那到底是不是亮綠色我也不是很確定,夜深人靜也沒人可問…)
004010D9 lea edi,[ebp-48h]
004010DC mov ecx,12h
004010E1 mov eax,0CCCCCCCCh
004010E6 rep stos dword ptr [edi]
31: int m = 3;
004010E8 mov dword ptr [ebp-4],3;函數的局部變量放置在EBP的負偏移處(Negative
; Offset)也就是向低地址方向(當然,當然,這是針對該函數使用
; 了標準的Stack Frame,如果代碼被編譯器作了優化了那么你
; 很可能就要遇到FPO這個概念,這可能需要另外寫一篇文章
; 來解釋,所以這里假設我們的函數使用的是標準的Stack
; Frame)
32: int n = 4;
004010EF mov dword ptr [ebp-8],4;同上
圖 5
33:
34: m = i;
004010F6 mov eax,dword ptr [ebp+8];從上圖中很容易看出來dword ptr [ebp+8]里面放的是
; 上級函數傳給AFunc的第一個參數,這里用ebp+8來訪問
; 參數說明上級傳給下級函數的參數是放在下級函數
; 的EBP的正向偏移位置處(Positive Offset)
004010F9 mov dword ptr [ebp-4],eax;將參數的值賦給局部變量
35: n = j;
004010FC mov ecx,dword ptr [ebp+0Ch];同上
004010FF mov dword ptr [ebp-8],ecx;同上
圖 6
步驟3.現在AFcun函數要調用BFunc了…
這是調用前的準備工作:
a.參數被壓棧;
b.CALL指令導致返回地址0040110F入棧;
37: BFunc(m,n);
00401102 mov edx,dword ptr [ebp-8]
00401105 push edx
00401106 mov eax,dword ptr [ebp-4]
00401109 push eax
0040110A call @ILT+25(BFunc) (0040101e)
0040110F add esp,8
圖 7
; 這和一開始wWinMain調用AFunc是差不多的過程
38:
39: return 8;
00401112 mov eax,8
40: }
00401117 pop edi
00401118 pop esi
00401119 pop ebx
0040111A add esp,48h
0040111D cmp ebp,esp
0040111F call __chkesp (00401220)
00401124 mov esp,ebp
00401126 pop ebp
00401127 ret
步驟4.進入BFcun函數之后堆棧的變化…
老規矩,我們先通篇看看BFunc在VC6++中的匯編代碼:
18: int BFunc(int i,int j)
19: {
00401090 push ebp
00401091 mov ebp,esp
00401093 sub esp,48h
00401096 push ebx
00401097 push esi
00401098 push edi
00401099 lea edi,[ebp-48h]
0040109C mov ecx,12h
004010A1 mov eax,0CCCCCCCCh
004010A6 rep stos dword ptr [edi]
20: int m = 1;
004010A8 mov dword ptr [ebp-4],1
21: int n = 2;
004010AF mov dword ptr [ebp-8],2
22:
23: m = i;
004010B6 mov eax,dword ptr [ebp+8]
004010B9 mov dword ptr [ebp-4],eax
24: n = j;
004010BC mov ecx,dword ptr [ebp+0Ch]
004010BF mov dword ptr [ebp-8],ecx
25:
26: return m;
004010C2 mov eax,dword ptr [ebp-4]
27: }
004010C5 pop edi
004010C6 pop esi
004010C7 pop ebx
004010C8 mov esp,ebp
004010CA pop ebp
004010CB ret
; 先看看BFunc的prolog:
18: int BFunc(int i,int j)
19: {
00401090 push ebp
00401091 mov ebp,esp
00401093 sub esp,48h
00401096 push ebx
00401097 push esi
00401098 push edi
圖 8
; 這個時候BFunc的堆棧信息也搭建好了(灰色部分)
20: int m = 1;
004010A8 mov dword ptr [ebp-4],1;沒什么新意的操作,和AFunc中發生的一模一樣
21: int n = 2;
004010AF mov dword ptr [ebp-8],2;沒新意
圖 9
22:
23: m = i;
004010B6 mov eax,dword ptr [ebp+8];沒新意
004010B9 mov dword ptr [ebp-4],eax
24: n = j;
004010BC mov ecx,dword ptr [ebp+0Ch];沒新意
004010BF mov dword ptr [ebp-8],ecx
25:
26: return m;
004010C2 mov eax,dword ptr [ebp-4];函數的返回值是放在EAX里面返回的,如果說一個個函
; 數之間是行星的話EAX就是神5那載著楊天人的返回
; 艙了.
27: }
; 我們把重點放在BFunc函數返回時執行的這些指令上(epilog)
004010C5 pop edi
004010C6 pop esi
004010C7 pop ebx
004010C8 mov esp,ebp
004010CA pop ebp
004010CB ret
圖 10
圖 11
; 此時你會發現圖11與圖 7時的堆棧情況完全(ESP,EBP的值相同)一樣,也就是說調用完BFunc函數后
; 堆棧恢復到了調用前的狀態.
0040110F add esp,8;注意BFunc執行完返回AFunc后AFunc將通過改變ESP將先前傳給BFunc
; 的參數出棧,但不清空.
圖12
就此AFunc調用BFunc函數結束了,接下來堆棧繼續重演著:父函數調用子函數,子函數執行結束后返回.然后父函數又作為別人的子函數,執行結束,返回…..
附錄
注解1
因為默認C/C++函數的調用約定是__cdecl,這種調用約定參數是從右到左壓棧的,Windows提供的函數大部分是__stdcall的調用約定,符合該約定的函數在傳參數的時候也是從右到左壓棧.
參考書目
[1] Jeffrey Richter,”Programming Applications for Microsoft Windows4rd”.( Microsoft Press,1999)
[2] Intel Architecture Software Developer Manual
[3] Randy Kath. “The Win32 Debugging API.” MSDN
總結
以上是生活随笔為你收集整理的Win32 环境下的堆栈的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 哈希函数原理及实现
- 下一篇: Linux环境下的堆栈--调试C程序