汇编语言PUSH和POP指令(压栈和出栈)

汇编里把一段内存空间定义为一个栈,栈总是先进后出,栈的最大空间为 64K。由于 "栈" 是由高到低使用的,所以新压入的数据的位置更低,ESP 中的指针将一直指向这个新位置,所以 ESP 中的地址数据是动态的。

PUSH 指令

PUSH 指令首先减少 ESP 的值,再将源操作数复制到堆栈。操作数是 16 位的,则 ESP 减 2,操作数是 32 位的,则 ESP 减 4。PUSH 指令有 3 种格式:

PUSH reg/mem16
PUSH reg/mem32
PUSH inm32

POP指令

POP 指令首先把 ESP 指向的堆栈元素内容复制到一个 16 位或 32 位目的操作数中,再增加 ESP 的值。如果操作数是 16 位的,ESP 加 2,如果操作数是 32 位的,ESP 加 4:

POP reg/mem16
POP reg/mem32

PUSHFD 和 POPFD 指令

PUSHFD 指令把 32 位 EFLAGS 寄存器内容压入堆栈,而 POPFD 指令则把栈顶单元内容弹出到 EFLAGS 寄存器:

pushfd
popfd

不能用 MOV 指令把标识寄存器内容复制给一个变量,因此,PUSHFD 可能就是保存标志位的最佳途径。有些时候保存标志寄存器的副本是非常有用的,这样之后就可以恢复标志寄存器原来的值。通常会用 PUSHFD 和 POPFD 封闭一段代码:

pushfd           ;保存标志寄存器
;
;任意语句序列
;
popfd           ;恢复标志寄存器

当用这种方式使用入栈和出栈指令时,必须确保程序的执行路径不会跳过 POPFD 指令。当程序随着时间不断修改时,很难记住所有入栈和出栈指令的位置。因此,精确的文档就显得至关重要!

一种不容易出错的保存和恢复标识寄存器的方法是:将它们压入堆栈后,立即弹出给一个变量:
  1. .data
  2. saveFlags DWORD ?
  3. .code
  4. pushfd ;标识寄存器内容入栈
  5. pop saveFLags ;复制给一个变量
下述语句从同一个变量中恢复标识寄存器内容:
  1. push saveFlags ;被保存的标识入栈
  2. popfd ;复制给标识寄存器

PUSHAD,PUSHA,POPAD 和 POPA

PUSHAD 指令按照 EAX、ECX、EDX、EBX、ESP(执行 PUSHAD 之前的值)、EBP、ESI 和 EDI 的顺序,将所有 32 位通用寄存器压入堆栈。

POPAD 指令按照相反顺序将同样的寄存器弹出堆栈。与之相似,PUSHA 指令按序(AX、CX、DX、BX、SP、BP、SI 和 DI)将 16 位通用寄存器压入堆栈。

POPA 指令按照相反顺序将同样的寄存器弹出堆栈。在 16 位模式下,只能使用 PUSHA 和 POPA 指令。

如果编写的过程会修改 32 位寄存器的值,则在过程开始时使用 PUSHAD 指令,在结束时使用 POPAD 指令,以此保存和恢复寄存器的内容。示例如下列代码段所示:
  1. MySub PROC
  2. pushad ;保存通用寄存器的内容
  3. .
  4. .
  5. mov eax,...
  6. mov edx,...
  7. mov ecx,...
  8. .
  9. .
  10. popad ;恢复通用寄存器的内容
  11. ret
  12. MySub ENDP
必须要指岀,上述示例有一个重要的例外:过程用一个或多个寄存器来返回结果时,不应使用 PUSHA 和 PUSHAD。假设下述 ReadValue 过程用 EAX 返回一个整数;调用 POPAD 将会覆盖 EAX 中的返回值:
  1. ReadValue PROC
  2. pushad ;保存通用寄存器的内容
  3. .
  4. .
  5. mov eax rreturn_value
  6. .
  7. .
  8. popad ;覆盖 EAX !
  9. ret
  10. ReadValue ENDP

示例:字符串反转

现在查看名为 RevStr 的程序:在一个字符串上循环,将每个字符压入堆栈,再把这些字符从堆栈中弹出(相反顺序),并保存回同一个字符串变量。由于堆栈是 LIFO(后进先出)结构,字符串中的字母顺序就发生了翻转:
  1. ;字符串翻转(Revstr.asm)
  2. .386
  3. .model flat,stdcall
  4. .stack 4096
  5. ExitProcess PROTO,dwExitCode:DWORD
  6.  
  7. .data
  8. aName BYTE "Abraham Lincoln",0
  9. nameSize = ($-aName)-1
  10.  
  11. .code
  12. main PROC
  13. ;将名字压入堆栈
  14. mov ecx,nameSize
  15. mov esi,0
  16.  
  17. L1: movzx eax,aName[esi] ;获取字符
  18. push eax ;压入堆栈
  19. inc esi
  20. loop L1
  21. ;将名字逆序弹出堆栈
  22. ;并存入aName数组
  23. mov ecx,nameSize
  24. mov esi,0
  25.  
  26. L2: pop eax ;获取字符
  27. mov aName[esi],al ;存入字符串
  28. inc esi
  29. loop L2
  30.  
  31. INVOKE ExitProcess,0
  32. main ENDP
  33. END main