2.14 堆棧平衡
為什麼學習本節內容?因為如果不考慮堆棧平衡,會造成堆棧溢出,程序崩潰,這是我們不願意看到的。
本節必須掌握的知識點:
堆棧是什麼
堆棧的特點
堆棧平衡
在上節中我們有一個疑問沒有解答,在解答這個疑問之前,我們先整明白堆棧是什麼?
這節相當於總結堆棧相關知識了,如果大家從彙編一開始一步一步跟著做實驗應該對堆棧有些瞭解。這裡大概總結一下,希望大家在看下面的內容時,自己能總結出對堆棧的認識。
2.14.1【堆棧是什麼?】
1、初始堆棧空間是操作系統給應用程序分配的內存空間;
2、程序運行時用來存儲臨時數據的地方,比如參數、返回值;
3、寄存器ESP是棧頂指針,ESP指向哪個內存地址,哪個內存地址就是堆棧棧頂,ESP保存的數據,正是堆棧已使用地址的棧頂。
我們論證一下總結的三點是否正確。
論證第一點:初始堆棧空間是操作系統給應用程序分配的內存空間。
借用DTDebug.exe打開飛鴿軟件時,我們就能看到堆棧窗口已經有內存地址,我們並沒有對調試和被調試的軟件做什麼操作,且堆棧中有的內存已經被用了,如圖2-14-1所示。
![2.14 堆棧平衡](http://p2.ttnews.xyz/loading.gif)
圖2-14-1堆棧窗口中,內存地址0x0019FFFC到內存地址0x0019FFF0之間的內存都已經被佔用了,那操作系統給我們分配了多大的內存空間範圍哪?我們通過查看FS位,FS位存儲的數據是0x00365000,在命令行輸入 DD 0x00365000,如圖2-14-2所示。
![2.14 堆棧平衡](http://p2.ttnews.xyz/loading.gif)
看圖2-14-2內存窗口中,操作系統給我們分配的內存空間範圍0x0019D000~0x001A0000。
論證第二點:程序運行時用來存儲臨時數據的地方,比如參數。
我們往堆棧中存儲數據,輸入以下指令,例如:
PUSH 1
PUSH 2
PUSH 3
……
寫入後按F8執行,看到堆棧中已經有了我們保存的臨時數據。如圖2-14-3所示。
當前已經把數據保存到堆棧中了,只能反映出堆棧是用來存儲數據的,並沒有體現存儲的是臨時數據。彆著急,先記錄當前ESP寄存器存儲的數據,ESP存儲的數據為0x0019FFE4,重點來了,把以下代碼輸入堆棧窗口中,如圖2-14-4所示。
POP EAX
PUSH 4
按F8執行POP EAX,如圖2-14-5所示。
看圖2-14-5中,ESP存儲的數據發生了變化,由0x0019FFE4變為了0x0019FFE8,把之前存儲的數據放入了EAX中。但是堆棧中內存地址0x0019FFE4現在存儲的數據還是3,並沒有發生變化,我們接著按F8執行PUSH 4,如圖2-14-6所示。
看圖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所示:
按F8執行並觀察EAX、ECX、EDX存儲數據的變化。
圖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指令就是調用一個函數,那麼問題來了,函數之間能不能使用同一塊內存?答案是可以的。
這些函數執行到RETN指令的時候,返回值是從哪取出來的?都是從[ESP]中取的,但是這些函數返回時都不是同一個地址,因為每一個函數都在堆棧中有一塊屬於自己的內存空間,用來存放臨時數據,由於堆棧大小空間是有限的,當一個函數執行完,我們要釋放它用過的內存空間,如果不釋放會導致堆棧溢出。
如何解決堆棧使用過程中不斷存儲數據導致堆棧溢出?
解決方案:函數調用時為臨時數據分配堆棧空間,函數執行完畢後,釋放這塊空間。
2.14.3【堆棧平衡】
介紹到這裡,我們現在來解決上一節留下來的疑問“我們的函數執行完了,可是我們的數據還保存在堆棧中,該怎麼解決呢”如圖2-14-10所示。
這裡就需要使用堆棧平衡的知識了。為什麼需要使用堆棧平衡的知識哪?我們首先搞清楚什麼是堆棧平衡?堆棧平衡就是函數調用時為臨時數據分配堆棧空間,函數執行完畢後,釋放這塊空間。
在圖2-14-10中函數執行過程中的臨時數據有:參數,返回值。當函數執行完時要釋放掉這些存在堆棧中的數據,一般函數運行中返回值是通過RETN指令釋放掉的空間,而參數我們該怎麼釋放哪?
有兩種解決方案:
1、外平棧
2、內平棧
先介紹外平棧,我們看圖2-14-10中,我們向堆棧窗口中壓入了5個參數,思考一下我們該怎麼用指令實現使ESP存儲的數據變為0x0019FFF0。
可以在函數執行完返回到CALL指令的下一行地址裡, 輸入ADD ESP,0x14
如圖2-14-11所示:
按F8執行並觀察堆棧變化,如圖2-14-12所示。
看圖2-14-12中,按F8執行後看到了ESP存儲的數據為0x0019FFF0。恢復到參數沒有壓入堆棧時的內存地址,使堆棧平衡。
介紹內平棧,所謂的內平棧就是在函數沒有執行完,完成堆棧平衡,思考一下該怎麼實現堆棧平衡。看圖2-14-13所示。函數執行到RETN指令時,RETN指令是將CALL指令壓入堆棧的數據彈出堆棧。我們就可以利用RETN指令將堆棧平衡,記錄當前ESP存儲的數據為0x0019FFD8由於圖2-14-13堆棧中壓入了5個數據,輸入以下指令
RETN 0x14
按F8執行RETN指令觀察ESP存儲的數據變化,如圖2-14-14所示。
看圖2-14-14中,按F8執行完,ESP存儲的數據變成0x0019FFF0。
以上是內平棧的操作,總結:在函數沒有執行完,使用RETN指令操作的平衡堆棧叫內平棧。
那麼問題來了什麼情況下需要考慮堆棧平衡呢?
首先考慮一下,怎麼才能使堆棧不平衡哪?無非就是向堆棧中傳遞參數。
參數傳遞的方式有:寄存器、堆棧、寄存器加內存這三種,首先排除寄存器傳參,因為它沒有使堆棧產生變化;而用堆棧傳參的時候,我們要注意向堆棧裡傳遞了多少參數,使用完這些參數後,我們要考慮堆棧平衡;寄存器加內存設計到操作系統內核的知識了,在這裡就不介紹了。
下節介紹ESP尋址。
練習:
1、用匯編編寫一個函數,功能是實現對任意10個整數的加法運算(不考慮溢出),
要求:
1、前2個參數使用寄存器進程參數傳遞;
2、後8個參數使用堆棧進行傳遞。
3、用2種方式實現堆棧平衡。
閱讀更多 愛達人編程達人 的文章