堆疊就是一塊記憶體,作業系統啟動時已經分配好的供程式使用的。在這裡說明一下,這裡介紹的堆疊與資料結構中的堆疊無關。為什麼要有堆疊?堆疊是程式執行過程中必須使用的一塊記憶體,任何一個程式需要用到的關鍵資料、臨時資料等,都會在堆疊中有一定的體現,所以我們可以說堆疊是程式的心臟。堆疊不平衡程式就會報錯,沒辦法執行下一個函式,比如有時會報錯C0000005錯誤。
我們知道每個應用程式都有獨立的4GB記憶體空間,是不是當前執行的應用程式擁有的4GB記憶體空間都可以使用哪?當然不是,雖然說名義上有4GB記憶體空間,但是在你真正要用某一塊記憶體時,一定要向作業系統申請,換句話說你要告訴作業系統,我們把這個過程稱為記憶體申請。
不同的語言,在申請記憶體時寫法是不同的,但它們的本質是相同的,都是申請記憶體。至於怎麼申請,怎麼實現,這就和C語言相關了,這裡暫不介紹了。
示例:我們回顧一下DTDebug.exe介面,首先開啟DTDebug.exe軟體,將飛鴿軟體拖到DTDebug.exe軟體中,如圖2-10-1所示,DTDebug.exe介面包括彙編視窗、暫存器視窗、記憶體視窗、堆疊視窗,本節主要介紹堆疊視窗和暫存器視窗中的兩個暫存器,這兩個暫存器分別為ESP、EBP。
看圖2-10-1堆疊視窗中的記憶體,都是已經向作業系統申請過的。那麼我們怎麼知道申請了多少堆疊哪?看圖2-10-1暫存器視窗中有FS位,它對應的資料是0x00272000,在DTDebug.exe軟體中找到命令視窗,輸入dd 0x00272000,如圖2-10-2所示:
在記憶體視窗中,記憶體地址0x00272004對應的資料是0x001A0000 (Top of thread"s stack),記憶體地址0x00272008對應的資料是0x0019D000 (Bottom of thread"s stack)。這就是作業系統為我們分配的堆疊地址,從0x0019D000到0x001A0000,堆疊視窗中的記憶體地址就在這個範圍的。不同的系統分配的大小會有差異。由於堆疊是作業系統專門給程式用的,程式在執行過程中必須使用堆疊,所以它又可以看做是一塊特殊的記憶體。
堆疊視窗中的記憶體地址是從高記憶體地址到低記憶體地址用的,也就是從記憶體地址0x001A0000開始使用到記憶體地址0x0019D000結束,如果用完了,就會報錯,提示堆疊溢位。那我們怎麼知道當前程式堆疊使用到哪裡了?比如圖2-10-3彙編視窗中,當前程式停在了0x77068E34,我們看暫存器視窗中,ESP暫存器儲存的資料為0x0019FFF0,則0x0019FFF0為當前程式堆疊使用到的地方。那麼從記憶體地址0x001A0000到記憶體地址0x0019FFF0都已經被使用過了如圖2-10-4所示:
為什麼是ESP暫存器哪?為什麼不是別的暫存器?在之前我們介紹過,32位彙編中8個通用暫存器都有各自的用途,ESP為棧指標,用於指向棧的棧頂,也就是說當前使用到的儲存資料的記憶體地址。有指向棧頂的指標,肯定有指向棧底的指標,那就是EBP暫存器,EBP為幀指標,指向當前活動記錄的底部。
我們知道了堆疊的知識,那我們該怎麼使用堆疊哪?接下來介紹堆疊的使用
2.10.2【堆疊的使用】
我們當前程式執行到某個階段時,中間會產生大量的資料,而這些資料將會暫時儲存在堆疊中。如果我們現在要使用堆疊,假如有臨時資料0x00000001和臨時資料0x00000002,我們怎麼能讓這些臨時資料暫時儲存在堆疊中哪?如圖2-10-4暫存器視窗中ESP儲存的當前資料為0x0019FFF0,說明堆疊已經使用到了記憶體地址為0x0019FFF0,臨時資料只能儲存在記憶體地址0x0019FFEC往上的地方。知道儲存在哪了,接下來就是寫彙編指令將臨時資料儲存在堆疊中了。
我們可以用已經學過的指令:
第一步:輸入指令,如圖2-10-5所示:
MOV DWORD PTR DS:[0x0019FFEC],0x00000001
MOV DWORD PTR DS:[0x0019FFE8],0x00000002
第二步:按F8執行,並觀察如圖2-10-6、2-10-7所示。
兩次按F8執行後,臨時資料0x00000001和臨時資料0x00000002都已經暫時儲存在了堆疊中。但ESP暫存器儲存的資料並沒有發生變化,這時我們要告訴堆疊當前已經使用到記憶體地址為0x0019FFE8的地方,若不告訴堆疊,則下一次再儲存資料時可能會覆蓋掉之前儲存的臨時資料,那麼該如何告訴堆疊哪?
第一步:輸入指令,如圖2-10-8所示:
SUB ESP,4
因為向堆疊中寫入了2次資料且堆疊中的資料是從高地址到低地址,所以將esp的值減少8個位元組,也可寫成SUB ESP,8。
第二步:按F8執行並觀察ESP暫存器儲存的資料變化及堆疊視窗的變化,如圖2-10-9、2-10-10所示:
圖2-10-9堆疊視窗中的黑色定位游標顯示在了記憶體地址0x0019FFEC中,而ESP儲存的資料為0x0019FFEC,說明堆疊已經記錄了資料0x00000001。我們接著按F8觀察。
圖2-10-10堆疊視窗中的黑色定位游標顯示在了記憶體地址0x0019FFE8中,而ESP儲存的資料為0x0019FFE8,說明堆疊已經記錄了資料0x00000002。
我們以後再使用資料時,可以先提升棧頂,再存入資料。比如再向堆疊中存入臨時資料0x33333333,我們先提升棧頂, 在輸入資料。
第一步:先提升棧頂,如圖2-10-11,ESP儲存的資料為0x0019FFE8。
第二步:按F8執行並觀察資料變化如圖2-10-12所示。
圖2-10-12堆疊視窗中的黑色定位游標顯示在了記憶體地址0x0019FFE4中,而ESP儲存的資料為0x0019FFE4,說明堆疊已經提升了。
第三步:向堆疊中寫入資料,如圖2-10-13所示,當前ESP儲存的資料為0x0019FFE4,記憶體地址0x0019FFE4儲存的資料為0x00000000。
MOV DWORD PTR SS:[ESP],0x33333333
第四步:按F8執行並觀察資料變化,如圖2-10-14。
圖2-10-14暫存器視窗中ESP儲存的資料為0x0019FFE4,記憶體地址0x0019FFE4儲存的資料為0x33333333。
假如我們儲存的臨時資料0x00000001、0x00000002和0x33333333,使用後不需要再用,那我們該如何釋放被臨時資料佔用的記憶體哪?由於我們使用的記憶體地址總是棧頂指標ESP的相對位置,所以我們只要修改ESP的值,就可以將這一塊記憶體釋放出來供下次使用,所以用完這3個臨時資料後,我們恢復原來的堆疊。
第一步:由於是存放了3個臨時資料,輸入指令,如圖2-10-15所示。
ADD ESP,4
或者ADD ESP,0xC
圖2-10-15中,ESP儲存的資料為0x0019FFE4,當前棧頂為記憶體地址0x0019FFE4。
第二步:按F8單步執行並觀察堆疊視窗中黑色游標區域棧頂的變動,如圖2-10-16所示。
圖2-10-16中,堆疊視窗中的黑色定位游標區域為記憶體地址0x0019FFF0,暫存器視窗中ESP儲存的資料為0x0019FFF0,所以成功恢復到一開始沒有儲存臨時資料的記憶體地址。
大家肯定看到圖2-10-16中我們儲存的臨時資料還存在,這裡我們已經恢復了棧頂,至於之前儲存的臨時資料對我們來說毫無影響了,下次儲存臨時資料時可以直接覆蓋,對我們除錯程式沒有影響。
【拓展:我們在使用C語言或其他語言時,為什麼要給區域性變數賦初始值,原因就在這,如果沒有初始化,只是申請變數空間,那麼這裡面的值就會影響我們的結果。】
以上我們是用基礎指令來演示堆疊變化,組合語言還給我們提供了一些簡化指令。
【PUSH指令】
PUSH指令它的功能是:
1、向堆疊中壓入資料
2、修改棧頂指標ESP暫存器的值
PUSH 指令的格式:
PUSH r32
PUSH r16
PUSH m16
PUSH m32
PUSH imm
我們push立即數的時候,預設為4個位元組,這裡不能使用8位暫存器或者記憶體。
例:
我們動手做實驗,輸入以下指令,如圖2-10-17所示:
PUSH 0x33333333
第二步:按F8執行並觀察資料變化,如圖2-10-18所示。
看圖2-10-18暫存器視窗ESP儲存的資料為0x0019FFEC,堆疊視窗中黑色定位游標區域為記憶體地址0x0019FFEC,為當前棧頂,記憶體地址0x0019FFEC儲存的資料正是我們壓入的資料0x33333333,所以可以總結出,PUSH 0x33333333這條指令相當於:
1、MOV DWORD PTR SS:[ESP],0x33333333
2、SUB ESP,4
學彙編我們不要記彙編的格式,要真正掌握它的工作原理,當我們能用其他的指令實現相同的功能才能真正學會了彙編。接下來我們學習另一個堆疊指令,我們記住它的功能,並能用其它指令代替,才能算掌握。
【POP指令】
POP指令功能:
1、將棧頂資料儲存到暫存器/記憶體
POP指令格式:
POP r32
POP r16
POP m16
POP m32
POP指令是將棧頂指標指向的資料取出,所以必須要有一個容器去接收POP指令彈出來的值,所以POP 後面不能是立即數
我們動手做實驗,輸入以下指令,如圖2-10-19所示,當前ESP儲存的資料為0x009FFF0,記憶體地址0x009FFF0儲存的資料為0x11111111,也是棧頂:
POP ECX
第二步:按F8執行後觀察資料變化,如圖2-10-20所示:
看圖2-10-20暫存器視窗中,ESP儲存的資料為0x0019FFF4,堆疊視窗中黑色定位游標區域為記憶體地址0x0019FFF4,為當前棧頂,ECX儲存的資料正是我們取出的資料0x11111111,所以可以總結出,POP這條指令相當於:
1、MOV DWORD PTR SS:[ESP]
2、ADD ESP,4
總結:PUSH、POP是堆疊相關的指令,PUSH表示將資料壓入堆疊中,同時棧頂提升相應寬度的位元組,POP表示將棧頂的資料取出來放到某個容器裡,同時棧頂減少相應寬度的位元組。
思考一下:如何使用3種方式實現:PUSH ECX
堆疊就是一塊記憶體,作業系統啟動時已經分配好的供程式使用的。在這裡說明一下,這裡介紹的堆疊與資料結構中的堆疊無關。為什麼要有堆疊?堆疊是程式執行過程中必須使用的一塊記憶體,任何一個程式需要用到的關鍵資料、臨時資料等,都會在堆疊中有一定的體現,所以我們可以說堆疊是程式的心臟。堆疊不平衡程式就會報錯,沒辦法執行下一個函式,比如有時會報錯C0000005錯誤。
我們知道每個應用程式都有獨立的4GB記憶體空間,是不是當前執行的應用程式擁有的4GB記憶體空間都可以使用哪?當然不是,雖然說名義上有4GB記憶體空間,但是在你真正要用某一塊記憶體時,一定要向作業系統申請,換句話說你要告訴作業系統,我們把這個過程稱為記憶體申請。
不同的語言,在申請記憶體時寫法是不同的,但它們的本質是相同的,都是申請記憶體。至於怎麼申請,怎麼實現,這就和C語言相關了,這裡暫不介紹了。
示例:我們回顧一下DTDebug.exe介面,首先開啟DTDebug.exe軟體,將飛鴿軟體拖到DTDebug.exe軟體中,如圖2-10-1所示,DTDebug.exe介面包括彙編視窗、暫存器視窗、記憶體視窗、堆疊視窗,本節主要介紹堆疊視窗和暫存器視窗中的兩個暫存器,這兩個暫存器分別為ESP、EBP。
看圖2-10-1堆疊視窗中的記憶體,都是已經向作業系統申請過的。那麼我們怎麼知道申請了多少堆疊哪?看圖2-10-1暫存器視窗中有FS位,它對應的資料是0x00272000,在DTDebug.exe軟體中找到命令視窗,輸入dd 0x00272000,如圖2-10-2所示:
在記憶體視窗中,記憶體地址0x00272004對應的資料是0x001A0000 (Top of thread"s stack),記憶體地址0x00272008對應的資料是0x0019D000 (Bottom of thread"s stack)。這就是作業系統為我們分配的堆疊地址,從0x0019D000到0x001A0000,堆疊視窗中的記憶體地址就在這個範圍的。不同的系統分配的大小會有差異。由於堆疊是作業系統專門給程式用的,程式在執行過程中必須使用堆疊,所以它又可以看做是一塊特殊的記憶體。
堆疊視窗中的記憶體地址是從高記憶體地址到低記憶體地址用的,也就是從記憶體地址0x001A0000開始使用到記憶體地址0x0019D000結束,如果用完了,就會報錯,提示堆疊溢位。那我們怎麼知道當前程式堆疊使用到哪裡了?比如圖2-10-3彙編視窗中,當前程式停在了0x77068E34,我們看暫存器視窗中,ESP暫存器儲存的資料為0x0019FFF0,則0x0019FFF0為當前程式堆疊使用到的地方。那麼從記憶體地址0x001A0000到記憶體地址0x0019FFF0都已經被使用過了如圖2-10-4所示:
為什麼是ESP暫存器哪?為什麼不是別的暫存器?在之前我們介紹過,32位彙編中8個通用暫存器都有各自的用途,ESP為棧指標,用於指向棧的棧頂,也就是說當前使用到的儲存資料的記憶體地址。有指向棧頂的指標,肯定有指向棧底的指標,那就是EBP暫存器,EBP為幀指標,指向當前活動記錄的底部。
我們知道了堆疊的知識,那我們該怎麼使用堆疊哪?接下來介紹堆疊的使用
2.10.2【堆疊的使用】
我們當前程式執行到某個階段時,中間會產生大量的資料,而這些資料將會暫時儲存在堆疊中。如果我們現在要使用堆疊,假如有臨時資料0x00000001和臨時資料0x00000002,我們怎麼能讓這些臨時資料暫時儲存在堆疊中哪?如圖2-10-4暫存器視窗中ESP儲存的當前資料為0x0019FFF0,說明堆疊已經使用到了記憶體地址為0x0019FFF0,臨時資料只能儲存在記憶體地址0x0019FFEC往上的地方。知道儲存在哪了,接下來就是寫彙編指令將臨時資料儲存在堆疊中了。
我們可以用已經學過的指令:
第一步:輸入指令,如圖2-10-5所示:
MOV DWORD PTR DS:[0x0019FFEC],0x00000001
MOV DWORD PTR DS:[0x0019FFE8],0x00000002
第二步:按F8執行,並觀察如圖2-10-6、2-10-7所示。
兩次按F8執行後,臨時資料0x00000001和臨時資料0x00000002都已經暫時儲存在了堆疊中。但ESP暫存器儲存的資料並沒有發生變化,這時我們要告訴堆疊當前已經使用到記憶體地址為0x0019FFE8的地方,若不告訴堆疊,則下一次再儲存資料時可能會覆蓋掉之前儲存的臨時資料,那麼該如何告訴堆疊哪?
第一步:輸入指令,如圖2-10-8所示:
SUB ESP,4
SUB ESP,4
因為向堆疊中寫入了2次資料且堆疊中的資料是從高地址到低地址,所以將esp的值減少8個位元組,也可寫成SUB ESP,8。
第二步:按F8執行並觀察ESP暫存器儲存的資料變化及堆疊視窗的變化,如圖2-10-9、2-10-10所示:
圖2-10-9堆疊視窗中的黑色定位游標顯示在了記憶體地址0x0019FFEC中,而ESP儲存的資料為0x0019FFEC,說明堆疊已經記錄了資料0x00000001。我們接著按F8觀察。
圖2-10-10堆疊視窗中的黑色定位游標顯示在了記憶體地址0x0019FFE8中,而ESP儲存的資料為0x0019FFE8,說明堆疊已經記錄了資料0x00000002。
我們以後再使用資料時,可以先提升棧頂,再存入資料。比如再向堆疊中存入臨時資料0x33333333,我們先提升棧頂, 在輸入資料。
第一步:先提升棧頂,如圖2-10-11,ESP儲存的資料為0x0019FFE8。
SUB ESP,4
第二步:按F8執行並觀察資料變化如圖2-10-12所示。
圖2-10-12堆疊視窗中的黑色定位游標顯示在了記憶體地址0x0019FFE4中,而ESP儲存的資料為0x0019FFE4,說明堆疊已經提升了。
第三步:向堆疊中寫入資料,如圖2-10-13所示,當前ESP儲存的資料為0x0019FFE4,記憶體地址0x0019FFE4儲存的資料為0x00000000。
MOV DWORD PTR SS:[ESP],0x33333333
第四步:按F8執行並觀察資料變化,如圖2-10-14。
圖2-10-14暫存器視窗中ESP儲存的資料為0x0019FFE4,記憶體地址0x0019FFE4儲存的資料為0x33333333。
假如我們儲存的臨時資料0x00000001、0x00000002和0x33333333,使用後不需要再用,那我們該如何釋放被臨時資料佔用的記憶體哪?由於我們使用的記憶體地址總是棧頂指標ESP的相對位置,所以我們只要修改ESP的值,就可以將這一塊記憶體釋放出來供下次使用,所以用完這3個臨時資料後,我們恢復原來的堆疊。
第一步:由於是存放了3個臨時資料,輸入指令,如圖2-10-15所示。
ADD ESP,4
ADD ESP,4
ADD ESP,4
或者ADD ESP,0xC
圖2-10-15中,ESP儲存的資料為0x0019FFE4,當前棧頂為記憶體地址0x0019FFE4。
第二步:按F8單步執行並觀察堆疊視窗中黑色游標區域棧頂的變動,如圖2-10-16所示。
圖2-10-16中,堆疊視窗中的黑色定位游標區域為記憶體地址0x0019FFF0,暫存器視窗中ESP儲存的資料為0x0019FFF0,所以成功恢復到一開始沒有儲存臨時資料的記憶體地址。
大家肯定看到圖2-10-16中我們儲存的臨時資料還存在,這裡我們已經恢復了棧頂,至於之前儲存的臨時資料對我們來說毫無影響了,下次儲存臨時資料時可以直接覆蓋,對我們除錯程式沒有影響。
【拓展:我們在使用C語言或其他語言時,為什麼要給區域性變數賦初始值,原因就在這,如果沒有初始化,只是申請變數空間,那麼這裡面的值就會影響我們的結果。】
以上我們是用基礎指令來演示堆疊變化,組合語言還給我們提供了一些簡化指令。
【PUSH指令】
PUSH指令它的功能是:
1、向堆疊中壓入資料
2、修改棧頂指標ESP暫存器的值
PUSH 指令的格式:
PUSH r32
PUSH r16
PUSH m16
PUSH m32
PUSH imm
我們push立即數的時候,預設為4個位元組,這裡不能使用8位暫存器或者記憶體。
例:
我們動手做實驗,輸入以下指令,如圖2-10-17所示:
PUSH 0x33333333
第二步:按F8執行並觀察資料變化,如圖2-10-18所示。
看圖2-10-18暫存器視窗ESP儲存的資料為0x0019FFEC,堆疊視窗中黑色定位游標區域為記憶體地址0x0019FFEC,為當前棧頂,記憶體地址0x0019FFEC儲存的資料正是我們壓入的資料0x33333333,所以可以總結出,PUSH 0x33333333這條指令相當於:
1、MOV DWORD PTR SS:[ESP],0x33333333
2、SUB ESP,4
學彙編我們不要記彙編的格式,要真正掌握它的工作原理,當我們能用其他的指令實現相同的功能才能真正學會了彙編。接下來我們學習另一個堆疊指令,我們記住它的功能,並能用其它指令代替,才能算掌握。
【POP指令】
POP指令功能:
1、將棧頂資料儲存到暫存器/記憶體
2、修改棧頂指標ESP暫存器的值
POP指令格式:
POP r32
POP r16
POP m16
POP m32
POP指令是將棧頂指標指向的資料取出,所以必須要有一個容器去接收POP指令彈出來的值,所以POP 後面不能是立即數
例:
我們動手做實驗,輸入以下指令,如圖2-10-19所示,當前ESP儲存的資料為0x009FFF0,記憶體地址0x009FFF0儲存的資料為0x11111111,也是棧頂:
POP ECX
第二步:按F8執行後觀察資料變化,如圖2-10-20所示:
看圖2-10-20暫存器視窗中,ESP儲存的資料為0x0019FFF4,堆疊視窗中黑色定位游標區域為記憶體地址0x0019FFF4,為當前棧頂,ECX儲存的資料正是我們取出的資料0x11111111,所以可以總結出,POP這條指令相當於:
1、MOV DWORD PTR SS:[ESP]
2、ADD ESP,4
總結:PUSH、POP是堆疊相關的指令,PUSH表示將資料壓入堆疊中,同時棧頂提升相應寬度的位元組,POP表示將棧頂的資料取出來放到某個容器裡,同時棧頂減少相應寬度的位元組。
思考一下:如何使用3種方式實現:PUSH ECX