棧與棧幀的調試
棧,通常用于存儲局部變量、傳遞函數參數、保存函數返回地址等。
棧內存在進程中的作用
1、暫時保存函數內的局部變量
2、調用函數時傳遞參數
3、保存函數返回后的地址
棧的特征
一個進程中,棧頂指針(ESP)初始狀態指向棧底端、執行push命令將數據壓入棧時,棧頂指針就會上移到棧頂端。執行pop命令從棧中彈出數據時,若棧為空,則棧頂指針就會重新移動到棧底端。
棧是由下往上擴展的,棧頂頂針在初始狀態下指向棧底,這就是棧的特征。
棧操作示例
1、od打開stack.exe文件,從中看到寄存器窗口中棧頂指針ESP的值為0012FFC4,棧窗口中最上面的地址(即棧頂地址)也為0012FFC4,右邊為0012FFC4對應的值。

2、在代碼窗口按下F7(Step into),自己執行地址00401000處的PUSH 100指令。
可以看到ESP的值減少了四個字節,變成了0012FFC0,并且棧窗口中的棧頂指定也指向了0012FFC0地址,且值也變成100。


再次執行pop EAX,ESP的值增加4個字節,變為0012FFC4。OD狀態變成最開始的狀態。

棧幀在程序中用于聲明局部變量、調用函數。理解棧幀主要用來掌握保存在其中的函數參數與局部變量。
簡而言之,棧幀就是利用EBP(棧幀指針,注意不是ESP)寄存器訪問棧內局部變量、參數、函數返回地址等的手段。ESP寄存器承擔著棧頂指針的作用,而EBP寄存器則負責行駛棧幀指針的職能。
在程序運行中,ESP寄存器的值隨時會變化,訪問棧中函數的局部變量、參數時,若以ESP的值為基準編寫程序會十分困難,并且也很難使CPU引用到準確的地址。
所以,調用某函數時,先要把用作基準點(函數起始地址)的ESP值保存到EBP,并維持在函數內部。這樣,無論ESP的值如何變化,以EBP的值為基準能夠安全訪問到相關函數的局部變量、參數、返回地址。
棧幀對應的匯編代碼:

示例:stackframe.cpp源碼

在main()函數的起始地址(40120)處下一個斷點,然后F9運行程序,程序運行到斷點處停止。

在開始調試前,記錄一下棧的狀態。如下,此時ESP的值為0012FF7C,EBP的值為0012FFC0。
注意,地址00401250保存在ESP(0012FF7C)中,它是main()函數執行完畢后要返回的地址。

main()函數一開始運行就生成與其對應的函數。
PUSH是一條壓棧指令,下面這一句的意思是將EBP的值壓入棧。main()函數中,EBP為棧幀指針,用來把EBP之前的值備份到棧中(main()函數執行完畢后,在返回之前,該值會再次恢復)。同時執行,這一條PUSH指令后,ESP的值變成12 FF78(0012FF7C-4)

同時執行這一條PUSH指令后,ESP的值變成12 FF78(0012FF7C-4)。

MOV是一條數據傳輸指令,這條語句的意思是將ESP的值傳送到EBP。也就是說,從這條指令開始,EBP就有了與現在ESP相同的值,并且直到main()函數執行完畢,EBP的值始終保持不變。
換言之,可以通過EBP安全的訪問到存儲在棧中的函數參數與局部變量。執行完PUSH與MOV兩條指令,棧幀就生成了,即EBP被設置好了。


在OD的棧窗口,右擊依次選擇Address-Relative to EBP。

現在的棧內情況被轉換成相對于EBP的偏移后,能更直觀地觀察到棧內情況。如下,在棧窗口中可以看到EBP的位置。

2、設置局部變量
開始分析源文件中的變量聲明與賦值語句。下面的語句用于在棧中為局部變量(a,b)分配空間

執行該指令之后,ESP由原來的12 FF78變成12 FF70。減去這8個字節,是為局部變量(a和b)開辟空間,以便將它們保存在棧中。由于局部變量a與b都是long型(長整型),它們分別占據4個字節,因此需要開辟8個字節的空間。
為函數變量開辟好棧空間后,在main()函數內部,無論ESP的值如何變化,變量a與b的棧空間都不會收到損壞。由于EBP的值在函數內部是固定不變的,所以可以以它為基準來訪問函數的局部變量了。

接下來看如下代碼,dword ptr ss:[ebp-0x4]可以理解為c語言中的指針。

匯編語言與C語言的指針語句格式:

提示:DWORD PTR SS:[EBP-4]語句中,SS是Stack Segment的縮寫,表示棧段。由于Windows中使用的段內存模型,使用時需要指出相關內存屬于哪一個區段。起始,32位的Windows OS中,SS、DS、ES的值皆為0,所以采用這種方式附上區段并沒有什么意義。
因為EBP與ESP是指向棧的寄存器,所以添加上了SS寄存器。DWORD PTR SS:等字符串可以通過OD的相關選項來隱藏。
再次分析一下剛剛的兩條MOV指令,它們的含義是把數據1與數據2分別保存在[EBP-4]與[EBP-8]中,即[EBP-4]代表局部變量a,[EBP-8]代表局部變量b。執行完兩條指令后,棧內情況如下圖,可以看到其對應的值分別為1,2。

3、add()函數參數傳遞與調用
代碼如下:

請看下面五行匯編代碼,它描述了add()函數的整個過程。
地址0040103C處為"CALL 40100" 命令,用于調用40100處的函數,而40100處的函數為add()函數。

函數add()接收a、b兩個長整型參數,所以在調用add()之前需要把兩個參數壓入棧,地址401034—40103B之間的指令就是這個作用。這一過程中需要注意的是,參數入棧的順利與C語言源碼中的參數順序相反(這種現象被稱為函數參數的逆向存儲)。
換言之,變量B首先入棧,接著變量a再入棧。執行完401034—40103B的指令后,棧內情況如下:

執行CALL命令進入被調用函數之前,CPU會把函數的返回值壓入棧,用做函數執行完畢后的返回地址。
從上圖可以看出調用add()函數之后,下一條命令的地址為401041。函數執行完畢后就會跳轉到這個地址。這個地址被稱為函數的返回地址。執行CALL命令后的棧窗口如下:

接下來執行add()函數的匯編指令。指令如下:

指令先把EBP值(main()函數的基址指針)保存到棧中,再把當前的ESP的值存儲到EBP當中。執行完,棧窗口中的情況如下:

4、設置add()函數的局部變量(x,y)
源代碼如下:
聲明了兩個長整型的局部變量(x,y),并用兩個形式參數(a,b)分別為它們賦初始值。

執行完下面的指令后,[EBP+8]與[EBP+C]分別執行參數a與b,而[EBP-8]與[EBP-4]分別指向add()函數的兩個局部變量x,y。棧窗口如下:

5、ADD運算
源碼:

匯編指令:

小知識:
EAX是一種通用寄存器,在算術運算中存儲輸入輸出數據,為函數提供返回值。若向EAX輸入某個值,該值就會原封不動地返回。執行過程中,棧內狀態不變。

6、刪除函數add()的棧幀以及函數執行完成返回
把EBP值賦給ESP,與前文的MOV EBP,ESP指令對應。把存儲到EBP的值恢復到ESP中。
提示:
執行完MOV ESP,EBP之后,地址401003處的指令不在有效。POP EBP用于恢復函數add()開始執行時備份到棧中的EBP值,可以看到ESP的值為0012FF64,對應的值為401041是執行call 401000命令時cpu存儲到棧中的返回地址。
EBP恢復為0012FF78,是當初main()函數的EBP值。

執行下面的語句后,存儲在棧中的返回地址被調用。

棧中地址如下:此時棧中的地址與返回到調用add()函數之前的狀態。


7、從棧中刪除函數add()的參數
執行如下命令,將ESP+8。

ESP加8的原因可以從上一張圖看出,地址12FF68與12FF6C處存儲的是傳遞給函數add()的參數a與b。函數add()執行后,不需要參數a與b了,所以ESP+8將它們從棧中清理掉。
提示:
1、調用add()函數之前先使用push指令把參數a,b壓入棧;
2、被調函數執行完畢后,函數的調用者(Caller)負責清理掉存儲在棧中的參數,這種方式稱為cdecl方式;
反之,被調用者(Caller)負責清理保存在棧中的參數,這種方式被稱為stdcall方式。
這些函數調用規則統稱為調用約定。
8、調用printf()函數
匯編指令如下:

地址401044處的 EAX 寄存器中存儲著函數 add()的返回值,它是執行加法運算后的結果值3。地址40104A處的 CALL 401067命令中調用的是401067地址處的函數,它是一個 C 標準庫函數 printf ,所有 C 標準庫函數都由 Visual C ++編寫而成(其中包含著數量龐大的函數,在此不詳細介紹)。
由于上面的 printf 函數有2個參數,大小為8個字節(32位寄存器+32位常量=64位=8字節),所以在40104F地址處使用 ADD 命令,將 ESP 加上8個字節,把函數的參數從棧中刪除。函數 printf()執行完畢并通過 ADD 命令刪除參數后。棧內情況如下:

執行如下指令來設置返回值:

常用于寄存器的初始化操作,執行下面兩條命令來對棧幀進行刪除以及函數終止。
執行后棧內地址如下圖:

執行完下面的命令,主函數執行完畢并返回,程序執行跳轉到返回地址(401250)。該地址執行visual C++的啟動函數區域。
00401056 retn