x86汇编中使用push/pop指令对寄存器的作用是什么?

144

阅读有关汇编语言的文章时,经常遇到人们写道他们将处理器的某个寄存器“push”一下,然后稍后再“pop”回来以恢复其先前的状态。
这是什么意思呢?如何将寄存器“push”起来?它被推到哪里去了?为什么需要这样做?
这是否简化为单个处理器指令,还是更加复杂?

7
警告:目前所有答案均以英特尔汇编语法给出;而在AT&T语法中,例如push-pop使用后缀(如“b”、“w”、“l”或“q”)来表示所操作的内存大小。例如:pushl %eaxpopl %eax - Hawken
8
大多数汇编器能够接受AT&T语法(尤其是gas),如果操作数的大小可以从操作数推导出来,那么就可以省略大小后缀。在您提供的示例中,这种情况成立,因为%eax始终是32位大小。 - Gunther Piez
5个回答

192

推入一个值(不一定存储在寄存器中)意味着将其写入堆栈。

弹出意味着将位于栈顶的内容恢复到一个寄存器中。这些是基本指令:

push 0xdeadbeef      ; push a value to the stack
pop eax              ; eax is now 0xdeadbeef

; swap contents of registers
push eax
mov eax, ebx
pop ebx

10
push和pop的明确操作数是r/m,而不仅仅是寄存器,因此您可以执行push dword [esi]。甚至使用pop dword [esp]来加载,然后将相同的值存回到相同的地址。(https://github.com/HJLebbink/asm-dude/wiki/POP)。我之所以提到这一点,是因为您说“不一定是寄存器”。 - Peter Cordes
5
你也可以进入内存区域并弹出其中的值:pop [0xdeadbeef] - S.S. Anne
你好,push/pop和pushq/popq有什么区别?我在macOS/Intel上。 - Steak Overflow
1
pushq将一个qword(64位)推入堆栈,而push必须从其操作数中推断大小。 - Sourav Kannantha B
仅在适合于8位立即数的小值时,推送imm/pop reg才有用。例如,push 1(2个字节)/ pop eax(1个字节)总共3个字节,而不是mov eax,1(总共5个字节,其中包括3个零字节的imm32,因此对于shellcode也是一个问题) [参见x86 / x64机器码高尔夫技巧]。此外,这种方式交换寄存器非常糟糕,可以使用xchg eax,ebx(1个字节,在现代Intel CPU上需要3个微操作,但它们都不涉及内存访问。并且在现代AMD上仅需要2个微操作)。 - Peter Cordes
@PeterCordes 你是对的。当然,这不是一篇关于如何交换寄存器的最佳实践的论文,而是一个用来说明“push”和“pop”是什么的十一年老回答。 - Linus Kleen

60

以下是如何将寄存器压入栈中的方法,假设我们正在讨论x86。

push ebx
push eax

它被推入栈中。在x86系统中,由于栈向下增长,因此将ESP寄存器的值减少到已经推送的值的大小。

需要保存这些值。通常的用法如下:

push eax           ;   preserve the value of eax
call some_method   ;   some method is called which will put return value in eax
mov  edx, eax      ;    move the return value to edx
pop  eax           ;    restore original eax

push 是 x86 指令中的一种,它在内部执行两个操作:

  1. ESP 寄存器减去被推送值的大小。
  2. 将被推送的值存储到 ESP 寄存器当前的地址上。

“将ESP寄存器减去推送值的大小”是什么意思?减少寄存器意味着什么?为什么push命令要这样做? - coulomb
你好。这是一个相当旧的帖子,但如果你仍在阅读:能否执行 pop eax(最后一条指令)并期望恢复原始值,这取决于 some_method 保证每个 push 操作都会有相应的 pop 操作的程度。我们是否应该总是期望满足这一点,这是否归结于阅读文档并确保? - First User

46

它被推到哪里了?

esp - 4。更准确地说:

  • esp 减去 4
  • 该值被推到 esp

pop 反转这个过程。

System V ABI 告诉 Linux 在程序开始运行时使 rsp 指向一个合理的堆栈位置:程序启动时默认的寄存器状态是什么(asm,linux)? 这通常是你应该使用的。

如何推送寄存器?

最小的 GNU GAS 示例:

.data
    /* .long takes 4 bytes each. */
    val1:
        /* Store bytes 0x 01 00 00 00 here. */
        .long 1
    val2:
        /* 0x 02 00 00 00 */
        .long 2
.text
    /* Make esp point to the address of val2.
     * Unusual, but totally possible. */
    mov $val2, %esp

    /* eax = 3 */
    mov $3, %ea 

    push %eax
    /*
    Outcome:
    - esp == val1
    - val1 == 3
    esp was changed to point to val1,
    and then val1 was modified.
    */

    pop %ebx
    /*
    Outcome:
    - esp == &val2
    - ebx == 3
    Inverses push: ebx gets the value of val1 (first)
    and then esp is increased back to point to val2.
    */

上述内容在GitHub上具有可运行的断言为什么需要这个? 虽然可以通过movaddsub轻松实现这些指令,但它们存在的原因是这些指令组合非常频繁,因此英特尔决定为我们提供它们。
这些组合之所以如此频繁,是因为它们使得将寄存器的值暂时保存到内存中并防止被覆盖变得容易。
要理解这个问题,请尝试手动编译一些C代码。
一个主要的困难是决定每个变量将存储在哪里。
理想情况下,所有变量都适合存储在寄存器中,这是访问最快的内存(目前比RAM快100倍)。

当然,我们可以很容易地拥有比寄存器更多的变量,特别是对于嵌套函数的参数,所以唯一的解决方案是写入内存。

我们可以写入任何内存地址,但由于函数调用和返回的局部变量和参数适合一个漂亮的堆栈模式,这可以防止内存碎片化,所以这是处理它的最佳方法。与编写堆分配器的疯狂相比,可以进行比较。

然后我们让编译器为我们优化寄存器分配,因为那是NP完全的,并且是编写编译器最困难的部分之一。这个问题被称为寄存器分配,它与图形着色同构。

当编译器的分配器被迫将东西存储在内存中而不仅仅是寄存器时,这被称为溢出

这是否归结为单个处理器指令,还是更复杂?

我们唯一确定的是,英特尔文档记录了pushpop指令,因此从某种意义上来说它们是一个指令。

内部而言,它可能会扩展为多个微代码,一个用于修改esp,另一个用于执行内存IO,并需要多个周期。

但是,一个单独的push指令可能比其他指令的等效组合更快,因为它更具体。

这大多数时间都没有得到记录:


4
无需猜测push/pop如何解码成uops。多亏了性能计数器,实验测试变得可能,并且Agner Fog已经完成并发布了指令表。由于堆栈引擎,Pentium-M及其后续版本的CPU具有单uop的push/pop操作(请参见Agner的微架构pdf)。这也包括最近的AMD CPU,这要归功于英特尔/ AMD的专利共享协议。 - Peter Cordes
@PeterCordes 太棒了!因此,性能计数器是由英特尔记录微操作的文档? - Ciro Santilli OurBigBook.com
此外,从寄存器溢出的本地变量通常仍会在L1缓存中保持热状态,如果其中任何一个实际上正在使用。但是,从寄存器读取实际上是免费的,零延迟。因此,它比L1缓存快得多,具体取决于您如何定义术语。对于溢出到堆栈的只读本地变量,主要成本只是额外的加载uops(有时是内存操作数,有时是单独的'mov'加载)。对于溢出的非常量变量,存储转发往返需要大量额外的延迟(与直接转发相比,额外约5c,并且存储指令不便宜)。 - Peter Cordes
是的,有一些不同流水线阶段(发射/执行/退役)的总uops计数器,因此您可以计算融合域或非融合域。例如,请参见此答案。如果我现在重写那个答案,我会使用ocperf.py包装脚本来获取计数器的易于理解的符号名称。 - Peter Cordes

41

推入和弹出寄存器在幕后相当于这样:

push reg   <= same as =>      sub  $8,%rsp        # subtract 8 from rsp
                              mov  reg,(%rsp)     # store, using rsp as the address

pop  reg    <= same as=>      mov  (%rsp),reg     # load, using rsp as the address
                              add  $8,%rsp        # add 8 to the rsp

注意,这是x86-64 At&t语法。

作为一对使用,它可以让你在堆栈上保存一个寄存器,并在以后恢复它。还有其他用途。


7
是的,这些序列正确地模拟了push/pop操作(除了push/pop不会影响标志位)。 - Peter Cordes
5
最好使用lea rsp,[rsp±8]来模拟push/pop对标志位的影响,而不是使用add/sub - Ruslan

15
几乎所有的CPU都使用堆栈。程序堆栈是一种硬件支持管理的LIFO技术。
堆栈是程序(RAM)内存的一部分,通常分配在CPU内存堆的顶部,并向相反方向增长(在PUSH指令中,堆栈指针会减少)。插入到堆栈中的标准术语是PUSH,从堆栈中移除的术语是POP
堆栈通过堆栈指针所代表的CPU寄存器进行管理,因此当CPU执行POPPUSH时,堆栈指针将把寄存器或常量加载/存储到堆栈内存中,并且堆栈指针将自动递减或递增,根据推入或弹出堆栈的字数。 通过汇编指令,我们可以存储到堆栈中:
  1. CPU寄存器和常量。
  2. 函数或过程的返回地址
  3. 函数/过程的输入/输出变量
  4. 函数/过程的局部变量。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接