在x86汇编中寄存器上使用的push/pop指令的功能是什么?

Ars*_*ble 79 x86 assembly stack terminology

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

  • 怎么能推一个寄存器?它在哪里推?为什么需要这个?
  • 这可归结为单处理器指令还是更复杂?

Lin*_*een 129

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

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

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
Run Code Online (Sandbox Code Playgroud)

  • push和pop的显式操作数是`r/m`,而不仅仅是register,所以你可以`push dword [esi]`.甚至`pop dword [esp]`加载然后将相同的值存储回同一个地址.(https://github.com/HJLebbink/asm-dude/wiki/POP).我只提到这个因为你说"不一定是寄存器". (3认同)
  • 你也可以`pop`到内存区域:`pop [0xdeadbeef]` (2认同)

Mad*_*uja 39

这是你如何推送一个寄存器.我假设我们正在谈论x86.

push ebx
push eax
Run Code Online (Sandbox Code Playgroud)

它被推到堆栈上.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
Run Code Online (Sandbox Code Playgroud)

A push是x86中的单个指令,它在内部执行两项操作.

  1. 将推送值存储在ESP寄存器的当前地址.
  2. ESP寄存器减小到推动值的大小.

  • 1.和2.应该重新排列 (6认同)

Cir*_*四事件 34

它在哪里推?

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.
    */
Run Code Online (Sandbox Code Playgroud)

上面的GitHub上有runnable断言.

为什么需要这个?

这是事实,这些指令可以通过轻松实现mov,addsub.

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

这些组合如此频繁的原因是它们可以很容易地将寄存器的值保存并暂时恢复到存储器中,这样它们就不会被覆盖.

要了解问题,请尝试手动编译一些C代码.

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

理想情况下,所有变量都适合寄存器,这是访问速度最快的存储器(目前比RAM 100倍).

但是,当然,我们可以轻松地拥有比寄存器更多的变量,特别是嵌套函数的参数,因此唯一的解决方案是写入内存.

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

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

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

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

我们所知道的只是英特尔记录了push一条pop指令,因此它们就是这方面的一条指令.

在内部,它可以扩展到多个微码,一个用于修改esp,一个用于执行内存IO,并且可以进行多个循环.

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

这主要是未记录的:

  • @Downvoters请解释,以便我可以学习和改进. (3认同)
  • 您无需猜测`push` /`pop`如何解码成uops。得益于性能计数器,可以进行实验测试,并且[Agner Fog完成了该工作并发布了说明表](http://agner.org/optimize/)。由于堆栈引擎,Pentium-M和更高版本的CPU具有单uu的“ push” /“ pop”(请参阅​​Agner的microarch pdf)。这要归功于Intel / AMD专利共享协议,其中包括最近的AMD CPU。 (2认同)

gow*_*ath 17

推送和弹出寄存器在幕后等效于此:

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
Run Code Online (Sandbox Code Playgroud)

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

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

  • 是的,这些序列正确模拟推/弹.(推/弹除外不影响标志). (4认同)
  • 你最好使用`lea rsp,[rsp±8]`而不是`add` /`sub`来更好地模拟`push` /`pop`对标志的影响. (2认同)

GJ.*_*GJ. 12

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

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

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

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

  1. CPU寄存器和常量.
  2. 返回函数或过程的地址
  3. 函数/过程输入/输出变量
  4. 函数/过程局部变量.