2.14 堆棧平衡

2.14 堆棧平衡

為什麼學習本節內容?因為如果不考慮堆棧平衡,會造成堆棧溢出,程序崩潰,這是我們不願意看到的。

本節必須掌握的知識點:

堆棧是什麼

堆棧的特點

堆棧平衡

在上節中我們有一個疑問沒有解答,在解答這個疑問之前,我們先整明白堆棧是什麼?

這節相當於總結堆棧相關知識了,如果大家從彙編一開始一步一步跟著做實驗應該對堆棧有些瞭解。這裡大概總結一下,希望大家在看下面的內容時,自己能總結出對堆棧的認識。

2.14.1【堆棧是什麼?】

1、初始堆棧空間是操作系統給應用程序分配的內存空間;

2、程序運行時用來存儲臨時數據的地方,比如參數、返回值;

3、寄存器ESP是棧頂指針,ESP指向哪個內存地址,哪個內存地址就是堆棧棧頂,ESP保存的數據,正是堆棧已使用地址的棧頂。

我們論證一下總結的三點是否正確。

論證第一點:初始堆棧空間是操作系統給應用程序分配的內存空間。

借用DTDebug.exe打開飛鴿軟件時,我們就能看到堆棧窗口已經有內存地址,我們並沒有對調試和被調試的軟件做什麼操作,且堆棧中有的內存已經被用了,如圖2-14-1所示。

2.14 堆棧平衡

圖2-14-1堆棧窗口中,內存地址0x0019FFFC到內存地址0x0019FFF0之間的內存都已經被佔用了,那操作系統給我們分配了多大的內存空間範圍哪?我們通過查看FS位,FS位存儲的數據是0x00365000,在命令行輸入 DD 0x00365000,如圖2-14-2所示。

2.14 堆棧平衡

看圖2-14-2內存窗口中,操作系統給我們分配的內存空間範圍0x0019D000~0x001A0000。

論證第二點:程序運行時用來存儲臨時數據的地方,比如參數。

我們往堆棧中存儲數據,輸入以下指令,例如:

PUSH 1

PUSH 2

PUSH 3

……

寫入後按F8執行,看到堆棧中已經有了我們保存的臨時數據。如圖2-14-3所示。

2.14 堆棧平衡

當前已經把數據保存到堆棧中了,只能反映出堆棧是用來存儲數據的,並沒有體現存儲的是臨時數據。彆著急,先記錄當前ESP寄存器存儲的數據,ESP存儲的數據為0x0019FFE4,重點來了,把以下代碼輸入堆棧窗口中,如圖2-14-4所示。

POP EAX

PUSH 4

2.14 堆棧平衡

按F8執行POP EAX,如圖2-14-5所示。

2.14 堆棧平衡

看圖2-14-5中,ESP存儲的數據發生了變化,由0x0019FFE4變為了0x0019FFE8,把之前存儲的數據放入了EAX中。但是堆棧中內存地址0x0019FFE4現在存儲的數據還是3,並沒有發生變化,我們接著按F8執行PUSH 4,如圖2-14-6所示。

2.14 堆棧平衡

看圖2-14-6中,ESP存儲的數據發生了變化,由0x0019FFE8變為了0x0019FFE4,且堆棧中內存地址0x0019FFE4存儲的數據為4。

我們把沒執行

POP EAX

PUSH 4

之前的堆棧進行對比,可以說明堆棧中保存的是臨時數據,應用程序在運行時會出現大量向堆棧中讀取數據的操作,若全部保存在操作系統分配的堆棧是遠遠不夠的,所以堆棧中保存的是臨時數據。

論證第三點:寄存器ESP是棧頂指針,ESP指向哪個內存地址,哪個內存地址就是堆棧棧頂,ESP保存的數據,正是堆棧已使用地址的棧頂。

我們看圖2-14-6所示,當前ESP存儲的數據為0x0019FFE4,而堆棧中黑色定位光標中內存地址為0x0019FFE4,內存地址0x0019FFE4存儲的數據為4。我們可以根據ESP尋址的方式去提取堆棧中存儲的數據,把存入堆棧中的 4、2、1依次存儲到EAX、ECX、EDX中。

輸入以下指令:

MOV EAX,DWORD PTR SS:[ESP]

MOV ECX,DWORD PTR SS:[ESP+4]

MOV EDX,DWORD PTR SS:[ESP+8]

如圖2-14-7所示:

2.14 堆棧平衡

按F8執行並觀察EAX、ECX、EDX存儲數據的變化。

2.14 堆棧平衡

圖2-14-8中,已經成功將堆棧中存儲的數據提取出來。所以ESP指向哪個內存地址,哪個內存地址就是棧頂。

以上是對堆棧是什麼我們做了總結,也做了論證,希望大家能夠自己動手做實驗,自己能夠總結出對堆棧的理解。

2.14.2【堆棧的特點】

通過操作我們可以大概總結出堆棧的特點:

1、初始堆棧空間有限;

2、可讀可寫;

3、頻繁修改;

4、方便查找、方便讀寫(方便使用);

5、地址連續;

6、使用時,是從高地址到低地址(處理器(CPU)規定的)。

大家看到總結堆棧的特點有沒有這樣的疑問,為什麼堆棧使用時,是從高地址到低地址(CPU規定的)?

答:因為對堆棧的操作方式,由兩個指令PUSH和POP直接操作堆棧,而PUSH指令和POP指令是屬於處理器(CPU)的,操作系統為了迎合處理器只能是從高低址到低地址,所以堆棧使用時,是從高低址到低地址由處理器決定。

我們在DTDebug.exe軟件彙編窗口中往下拉,看圖2-14-9所示,可以看到有很多CALL指令,我們知道執行CALL指令就是調用一個函數,那麼問題來了,函數之間能不能使用同一塊內存?答案是可以的。

2.14 堆棧平衡

這些函數執行到RETN指令的時候,返回值是從哪取出來的?都是從[ESP]中取的,但是這些函數返回時都不是同一個地址,因為每一個函數都在堆棧中有一塊屬於自己的內存空間,用來存放臨時數據,由於堆棧大小空間是有限的,當一個函數執行完,我們要釋放它用過的內存空間,如果不釋放會導致堆棧溢出。

如何解決堆棧使用過程中不斷存儲數據導致堆棧溢出?

解決方案:函數調用時為臨時數據分配堆棧空間,函數執行完畢後,釋放這塊空間。

2.14.3【堆棧平衡】

介紹到這裡,我們現在來解決上一節留下來的疑問“我們的函數執行完了,可是我們的數據還保存在堆棧中,該怎麼解決呢”如圖2-14-10所示。

2.14 堆棧平衡

這裡就需要使用堆棧平衡的知識了。為什麼需要使用堆棧平衡的知識哪?我們首先搞清楚什麼是堆棧平衡?堆棧平衡就是函數調用時為臨時數據分配堆棧空間,函數執行完畢後,釋放這塊空間。

在圖2-14-10中函數執行過程中的臨時數據有:參數,返回值。當函數執行完時要釋放掉這些存在堆棧中的數據,一般函數運行中返回值是通過RETN指令釋放掉的空間,而參數我們該怎麼釋放哪?

有兩種解決方案:

1、外平棧

2、內平棧

先介紹外平棧,我們看圖2-14-10中,我們向堆棧窗口中壓入了5個參數,思考一下我們該怎麼用指令實現使ESP存儲的數據變為0x0019FFF0。

可以在函數執行完返回到CALL指令的下一行地址裡, 輸入ADD ESP,0x14

如圖2-14-11所示:

2.14 堆棧平衡

按F8執行並觀察堆棧變化,如圖2-14-12所示。

2.14 堆棧平衡

看圖2-14-12中,按F8執行後看到了ESP存儲的數據為0x0019FFF0。恢復到參數沒有壓入堆棧時的內存地址,使堆棧平衡。

介紹內平棧,所謂的內平棧就是在函數沒有執行完,完成堆棧平衡,思考一下該怎麼實現堆棧平衡。看圖2-14-13所示。函數執行到RETN指令時,RETN指令是將CALL指令壓入堆棧的數據彈出堆棧。我們就可以利用RETN指令將堆棧平衡,記錄當前ESP存儲的數據為0x0019FFD8由於圖2-14-13堆棧中壓入了5個數據,輸入以下指令

2.14 堆棧平衡

RETN 0x14

按F8執行RETN指令觀察ESP存儲的數據變化,如圖2-14-14所示。

2.14 堆棧平衡

看圖2-14-14中,按F8執行完,ESP存儲的數據變成0x0019FFF0。

以上是內平棧的操作,總結:在函數沒有執行完,使用RETN指令操作的平衡堆棧叫內平棧。

那麼問題來了什麼情況下需要考慮堆棧平衡呢?

首先考慮一下,怎麼才能使堆棧不平衡哪?無非就是向堆棧中傳遞參數。

參數傳遞的方式有:寄存器、堆棧、寄存器加內存這三種,首先排除寄存器傳參,因為它沒有使堆棧產生變化;而用堆棧傳參的時候,我們要注意向堆棧裡傳遞了多少參數,使用完這些參數後,我們要考慮堆棧平衡;寄存器加內存設計到操作系統內核的知識了,在這裡就不介紹了。

下節介紹ESP尋址。

練習:

1、用匯編編寫一個函數,功能是實現對任意10個整數的加法運算(不考慮溢出),

要求:

1、前2個參數使用寄存器進程參數傳遞;

2、後8個參數使用堆棧進行傳遞。

3、用2種方式實現堆棧平衡。


分享到:


相關文章: