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

在阅读汇编程序时,我经常遇到人们写到,他们 用力某个寄存器的处理器和 爸爸它再次以后恢复它的以前的状态。

  • 你怎样才能推动一个收银机? 它在哪里被推动? 为什么需要这个?
  • 这是归结为单个处理器指令还是更复杂?
403751 次浏览

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

弹出 意味着恢复堆栈 进入顶部的寄存器。这些是基本的指令:

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

这里是如何推送一个收银机。我假设我们谈论的是 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寄存器的当前地址。

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

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

堆栈是通过堆栈预期的 CPU 寄存器来管理的,也称为堆栈指针,所以当 CPU 执行 POP用力时,堆栈指针将加载/存储寄存器或常量到堆栈内存中,堆栈指针将自动减少或根据推入(从)堆栈的字数增加。

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

  1. CPU 寄存器和常量。
  2. 函数或函数的返回地址 程序
  3. 输入/输出功能/程序 变量
  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 代码。

一个主要的困难,是决定每个变量将存储在哪里。

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

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

我们可以写入任何内存地址,但是由于函数调用和返回的本地变量和参数符合一个很好的堆栈模式,这可以防止 内存碎片,因此这是处理它的最佳方法。相比之下,编写堆分配器简直是疯了。

然后我们让编译器为我们优化寄存器分配,因为这是 NP 完成的,也是编译器编写过程中最难的部分之一。这个问题叫做 登记册分配,它与 图的着色图的着色是同构的。

当编译器的分配器被迫在内存中存储内容而不仅仅是寄存器时,这就是所谓的 说吧

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

我们所知道的只是 Intel 记录了一个 push和一个 pop指令,所以它们在这个意义上是一个指令。

在内部,它可以扩展到多个微代码,一个用于修改 esp,一个用于执行内存 IO,并且需要多个周期。

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

这基本上是没有记录在案的:

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

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

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