x86 反彙編/堆疊

一般來說,堆疊是一種資料結構,它將資料值連續儲存在記憶體中。但是,與陣列不同的是,您只能訪問(讀取或寫入)堆疊的“頂部”資料。從堆疊讀取被稱為“出棧”,寫入堆疊被稱為“入棧”。堆疊也稱為 LIFO 佇列(後進先出),因為值從堆疊中彈出的順序與它們被推入堆疊的順序相反(想想您是如何在一張桌子上堆放盤子的)。出棧的資料從堆疊中消失。
所有 x86 架構都使用堆疊作為 RAM 中的臨時儲存區域,允許處理器快速儲存和檢索記憶體中的資料。esp 暫存器指向堆疊的當前頂部。堆疊“向下”增長,從高記憶體地址到低記憶體地址,因此最近被推入堆疊的值位於高於 esp 指標的記憶體地址中。沒有暫存器專門指向堆疊的底部,儘管大多數作業系統監控堆疊邊界以檢測“下溢”(彈出空堆疊)和“上溢”(將太多資訊推入堆疊)條件。
當從堆疊中彈出值時,該值將保留在記憶體中,直到被覆蓋。但是,您永遠不要依賴 esp 之下記憶體地址的內容,因為其他函式可能會在您不知情的情況下覆蓋這些值。
Windows ME、98、95、3.1(及更早版本)的使用者可能仍然記得臭名昭著的“藍色畫面宕機”——有時是由堆疊溢位異常引起的。當寫入堆疊的資料過多,堆疊“增長”超出其限制時,就會發生這種情況。現代作業系統使用更好的邊界檢查和錯誤恢復來減少堆疊溢位的發生,並在堆疊溢位後保持系統穩定性。
以下 ASM 程式碼行基本等效
push eax
|
sub esp, 4
mov DWORD PTR SS:[esp], eax
|
pop eax
|
mov eax, DWORD PTR SS:[esp]
add esp, 4
|
但單個命令的執行速度實際上比替代方法快得多。可以將其視覺化為堆疊從右到左增長,並且 esp 在堆疊大小增長時減小。
| 壓棧 | 出棧 |
|---|---|
| 此程式碼示例使用 MASM 語法 |
假設我們想要快速丟棄之前推入堆疊的 3 個專案,而不儲存這些值(換句話說,"清理"堆疊)。以下方法有效(注意它會覆蓋 eax 暫存器)
pop eax
pop eax
pop eax
但是,有一種更快的方法,也不會影響除堆疊指標以外的任何暫存器。我們可以簡單地對 esp 執行一些基本算術運算,使指標“超過”資料項,這樣就不能再讀取它們,並且可以在下一輪 push 命令中用它們覆蓋。
add esp, 12 ; 12 is 3 DWORDs (4 bytes * 3)
同樣,如果我們想要在堆疊上為大於 DWORD 的專案保留空間,我們可以使用減法來人工向前移動 esp。然後,我們可以直接將保留的記憶體訪問為記憶體指標,或者我們可以將它間接訪問為 esp 本身的偏移量值。
假設我們想要在堆疊上建立一個位元組值陣列,長度為 100 個專案。我們想要將指向該陣列基址的指標儲存在 edi 中。我們該怎麼做?以下是一個示例
sub esp, 100 ; num of bytes in our array
mov edi, esp ; copy address of 100 bytes area to edi
要銷燬該陣列,我們只需編寫以下指令
add esp, 100
要讀取堆疊上的值而不將其從堆疊中彈出,可以使用 esp 以及偏移量。例如,要將堆疊頂部的 3 個 DWORD 值讀取到 eax(但不使用 pop 指令),我們將使用以下指令
mov eax, DWORD PTR SS:[esp]
mov eax, DWORD PTR SS:[esp + 4]
mov eax, DWORD PTR SS:[esp + 8]
請記住,由於 esp 在堆疊增長時向下移動,因此可以使用正偏移量訪問堆疊上的資料。永遠不要使用負偏移量,因為堆疊“上方”的資料不能保證保持您離開時的狀態。從堆疊讀取而不彈出的操作通常被稱為“窺視”,但由於這不是正式術語,因此本華夏公益教科書不會使用它。
計算機記憶體中有兩個區域可以儲存程式資料。第一個是我們一直在討論的堆疊。它是一個線性 LIFO 緩衝區,允許快速分配和釋放,但大小有限。堆通常是一個非線性資料儲存區域,通常使用連結串列、二叉樹或其他更復雜的方法實現。堆比堆疊更難與之互動和維護,分配/釋放執行得更慢。但是,堆可以隨著資料的增長而增長,並且當資料量變得太大時,可以分配新的堆。
正如我們將在後面看到的那樣,顯式宣告的變數是在堆疊上分配的。堆疊變數的數量有限,並且具有確定的尺寸。堆變數的數量和尺寸可以是可變的。我們將在後面更詳細地討論這些主題。