由於 "棧" 是由高到低使用的, 所以新壓入的數據的位置更低.
ESP 中的指針將一直指向這個新位置, 所以 ESP 中的地址數據是動態的.
每次 PUSH, ESP = ESP - x; 每次 POP, ESP = ESP + x;
其中的 x 只能是 4 或 2, 因為 Win32 的 PUSH 只可以壓入 32 位(默認)或 16 位的數據.
ESP 有個名字叫 "棧頂", 其實它指向的是棧中最低位置的數據.
實例查看 ESP 的變化:
; Test18_1.asm
.386
.model flat, stdcall
include Windows.inc
include kernel32.inc
include masm32.inc
include debug.inc
includelib kernel32.lib
includelib masm32.lib
includelib debug.lib
.data
ddVal1 dd 1
ddVal2 dd 2
dwVal1 dw 3
dwVal2 dw 4
.code
main proc
PrintHex esp ;0012FFA4
push ddVal1
PrintHex esp ;0012FFA0
push ddVal2
PrintHex esp ;0012FF9C
push dwVal1
PrintHex esp ;0012FF9A
push dwVal2
PrintHex esp ;0012FF98
pop dwVal2
PrintHex esp ;0012FF9A
pop dwVal1
PrintHex esp ;0012FF9C
pop ddVal2
PrintHex esp ;0012FFA0
pop ddVal1
PrintHex esp ;0012FFA4
ret
main endp
end main
使用參數壓棧的方式調用函數, 同時揭示 invoke 的本質:
; Test18_2.asm
.386
.model flat, stdcall
include Windows.inc
include kernel32.inc
;include masm32.inc
;include debug.inc
includelib kernel32.lib
;includelib masm32.lib
;includelib debug.lib
include user32.inc
includelib user32.lib
.data
szMsg db 'Hello World!', 0
szCaption db 'Hi', 0
.code
main proc
;invoke MessageBox, NULL, addr szMsg, addr szCaption, MB_OK
;用壓棧的方式調用 MessageBox 函數; 本來就是如此, invoke 只是簡化了這個步驟
push MB_OK ;C 函數和系統函數讀取參數的順序是: 從右到左; 最左邊的參數最後使用, 要先壓入
push offset szCaption
push offset szMsg
push NULL ;一個常數會默認當作 32 位數據壓入
call MessageBox
pop edx ;隨便出棧到一個地方, 已經沒用了, 相當於進回收站
pop edx ;盡管沒用, 不出是不行的, 因為 push 和 pop 要成對出現
pop edx
pop edx
;invoke ExitProcess, NULL
;用壓棧的方式調用 ExitProcess 函數
push NULL
call ExitProcess
pop edx
main endp
end main
從上面的例子看出, 函數調用是需要先壓棧(PUSH)參數的;
PUSH 另一重要作用是保護數據, 因為在 call 其他函數或子過程時可能會覆蓋掉某些寄存器的數據.
最先需要保護的就是 ESP 中的數據!
你的函數(或子過程)可能要調用其他函數, 你的函數也可能會被其他函數調用, 甚至是嵌套調用;
在這種情況下(其實這是最正常不過的事情), ESP 的變化就會比較復雜.
所以, 每次調用函數(或子過程)都應該先對 ESP 執行保護, 調用完畢再恢復;
但因 ESP 的動態有些捉摸不定, 所以一般是先 mov ebp, esp, 然後 push ebp ... 像這樣:
mov ebp, esp
push ebp
;...函數或子過程
pop ebp
mov esp, ebp
;leave ;可以使用 leave 指令代替上面兩行, 它是對上面兩行的簡化
從調試器中查看編譯器添加的保護 ESP 的代碼:
; Test18_3.asm; 這是用於調試的例子
.386
.model flat, stdcall
include Windows.inc
include kernel32.inc
include masm32.inc
include debug.inc
includelib kernel32.lib
includelib masm32.lib
includelib debug.lib
.code
;求和函數
sumProc proc v1:dword, v2:dword, v3:dWord
mov eax, v1
add eax, v2
add eax, v3
ret
sumProc endp
;
main proc
invoke sumProc, 11, 22, 33
PrintDec eax ;66
ret
main endp
end main
;--------------------------
;Ctrl + T 是設置或取消斷點
;Ctrl + D 是調試運行
;從調試器中看到 sumProc 函數的代碼變成了:
PUSH EBP
MOV EBP,ESP
MOV EAX,DWord PTR SS:[EBP+8]
ADD EAX,DWord PTR SS:[EBP+C]
ADD EAX,DWord PTR SS:[EBP+10]
LEAVE
;看來保護 ESP 的工作是由編譯器做的
;從這裡也看出了 EBP 寄存器的主要用途就是中轉 ESP 中的數據
利用 ESP 的地址偏移讀取棧中的數據:
; Test18_4.asm
.386
.model flat, stdcall
include Windows.inc
include kernel32.inc
include masm32.inc
include debug.inc
includelib kernel32.lib
includelib masm32.lib
includelib debug.lib
.code
main proc
push 111
push 222
push 333
push 444
mov eax, [esp]
PrintDec eax ;444
mov eax, [esp+4]
PrintDec eax ;333
mov eax, [esp+12]
PrintDec eax ;111
pop edx
pop edx
pop edx
pop edx
ret
main endp
end main
總結 PUSH 和 POP 的主要用途: 1、暫存與恢復數據; 2、處理函數參數.