127

在阅读有关汇编程序的信息时,我经常遇到有人写他们推送处理器的某个寄存器并稍后再次弹出它以恢复它的先前状态。

  • 你怎么能推送一个寄存器?推到哪里去了?为什么需要这个?
  • 这归结为单个处理器指令还是更复杂?
4

5 回答 5

174

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

弹出意味着将堆栈顶部的任何内容恢复寄存器中。这些是基本说明:

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
于 2011-01-03T11:41:34.213 回答
52

这是推送寄存器的方法。我假设我们正在谈论 x86。

push ebx
push eax

它被压入堆栈。ESP在 x86 系统中,随着堆栈向下增长,寄存器的值会减少到推送值的大小。

需要保存这些值。一般用法是

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

Apush是 x86 中的一条指令,它在内部做两件事。

  1. 将寄存器递减ESP推送值的大小。
  2. 将压入的值存储在寄存器的当前地址ESP
于 2011-01-03T11:41:48.113 回答
42

推到哪里去了?

esp - 4. 更确切地说:

  • esp减去 4
  • 值被推到esp

pop逆转这一点。

System V ABI 告诉 Linuxrsp在程序开始运行时指出一个合理的堆栈位置:程序启动时的默认寄存器状态是什么(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 上带有可运行的断言

为什么需要这个?

确实,这些指令可以通过movadd轻松实现sub

它们之所以存在,是因为这些指令组合如此频繁,以至于英特尔决定为我们提供它们。

这些组合如此频繁的原因是它们可以轻松地将寄存器的值临时保存和恢复到内存中,这样它们就不会被覆盖。

要理解这个问题,请尝试手动编译一些 C 代码。

一个主要的困难是决定每个变量的存储位置。

理想情况下,所有变量都可以放入寄存器中,这是访问速度最快的内存(目前大约比 RAM快 100 倍)。

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

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

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

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

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

我们可以肯定的是,英特尔记录了 apush和 apop指令,因此它们在这个意义上是一个指令。

在内部,它可以扩展为多个微码,一个修改esp,一个做内存 IO,并且需要多个周期。

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

这主要是未(der)记录的:

于 2015-11-07T13:10:36.990 回答
30

推送和弹出寄存器在幕后相当于:

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 语法。

作为一对使用,这使您可以将寄存器保存在堆栈中并稍后恢复它。还有其他用途。

于 2016-09-06T01:31:54.380 回答
15

几乎所有 CPU 都使用堆栈。程序堆栈是LIFO技术,具有硬件支持的管理。

堆栈是通常分配在 CPU 内存堆顶部的程序 (RAM) 内存量,并以相反的方向增长(在 PUSH 指令处堆栈指针减少)。插入堆栈的标准术语是PUSH,从堆栈中删除的标准术语是POP

堆栈通过堆栈预期的 CPU 寄存器进行管理,也称为堆栈指针,因此当 CPU 执行POPPUSH时,堆栈指针会将寄存器或常量加载/存储到堆栈内存中,堆栈指针将根据压入的字数自动减少 x 或增加或弹出(从)堆栈。

通过汇编指令,我们可以存储到堆栈:

  1. CPU 寄存器和常量。
  2. 函数或过程的返回地址
  3. 函数/过程输入/输出变量
  4. 函数/过程局部变量。
于 2011-01-03T12:28:10.020 回答