实例查看 ESP 的变化:
; Test18_1.asm.386.model flat, stdcallinclude windows.incinclude kernel32.incinclude masm32.incinclude debug.incincludelib kernel32.libincludelib masm32.libincludelib debug.lib.data ddVal1 dd 1 ddVal2 dd 2 dwVal1 dw 3 dwVal2 dw 4.codemain 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 retmain endpend main使用参数压栈的方式调用函数, 同时揭示 invoke 的本质:
; Test18_2.asm.386.model flat, stdcallinclude windows.incinclude kernel32.inc;include masm32.inc;include debug.incincludelib kernel32.lib;includelib masm32.lib;includelib debug.libinclude user32.incincludelib user32.lib.data szMsg db 'Hello World!', 0 szCaption db 'Hi', 0.codemain 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 edxmain endpend main从上面的例子看出, 函数调用是需要先压栈(PUSH)参数的; PUSH 另一重要作用是保护数据, 调用函数前, 最先需要保护的就是 EIP, 这是执行完函数后的下一条指令的地址. call 指令会先把 EIP 传给 ESP; ret 指令最后把 ESP 恢复给 EIP. 所以, 压栈出栈保护的是 ESP. 但因 ESP 是动态的, 所以一般先 mov ebp, esp, 然后 push ebp ... 像这样:
mov ebp, esppush ebp;...函数或子过程pop ebpmov esp, ebp;leave ;可以使用 leave 指令代替上面两行, 它是对上面两行的简化从调试器中查看编译器添加的保护 ESP 的代码:
; Test18_3.asm; 这是用于调试的例子.386.model flat, stdcallinclude windows.incinclude kernel32.incinclude masm32.incinclude debug.incincludelib kernel32.libincludelib masm32.libincludelib debug.lib.code;求和函数sumProc proc v1:dword, v2:dword, v3:dword mov eax, v1 add eax, v2 add eax, v3 retsumProc endp;main proc invoke sumProc, 11, 22, 33 PrintDec eax ;66 retmain endpend main;--------------------------;Ctrl + T 是设置或取消断点;Ctrl + D 是调试运行;从调试器中看到 sumProc 函数的代码变成了:PUSH EBPMOV EBP,ESPMOV 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, stdcallinclude windows.incinclude kernel32.incinclude masm32.incinclude debug.incincludelib kernel32.libincludelib masm32.libincludelib debug.lib.codemain 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 retmain endpend main总结 PUSH 和 POP 的主要用途: 1、暂存与恢复数据; 2、处理函数参数.
压栈、出栈指令汇总:
PUSH(PUSHW、PUSHD) / POP ;进出 16 位或 32 位操作数, 默认 32 位PUSHAD / POPAD ;进出 EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDIPUSHA / POPA ;进出 AX、 CX、 DX、 BX、 SP、 BP、 SI、 DIPUSHFD / POPFD ;进出 EFLAGSPUSHF / POPF ;进出 EFLAGS 的低 16 位