10

我试图了解推送和弹出堆栈指针寄存器的行为。在美国电话电报公司:

pushl %esp

popl %esp

请注意,它们将计算值存储回%esp.

我正在独立考虑这些指令,而不是按顺序考虑。我知道存储在其中的值%esp始终是递增/递减之前的值,但我怎么能用汇编语言来表示行为呢?到目前为止,这是我想出的。

对于pushl %esp(忽略 FLAGS 和对临时寄存器的影响):

movl %esp, %edx     1. save value of %esp
subl  $4, %esp      2. decrement stack pointer
movl %edx, (%esp)   3. store old value of %esp on top of stack

对于popl %esp

movl (%esp), %esp   You wouldn’t need the increment portion. 

它是否正确?如果没有,我哪里错了?

4

2 回答 2

12

正如英特尔® 64 和 IA-32 架构开发push esp人员手册:组合卷(实际上在第 2 卷,或https://www.felixcloutier.com/x86/push上的 HTML 抓取)中所述:

PUSH ESP 指令压入 ESP 寄存器的值,因为它在指令执行之前就存在。如果 PUSH 指令使用内存操作数,其中 ESP 寄存器用于计算操作数地址,则在 ESP 寄存器递减之前计算操作数的地址。

至于pop esphttps://www.felixcloutier.com/x86/pop):

POP ESP 指令在将旧堆栈顶部的数据写入目标之前递增堆栈指针 (ESP)。

pop 16(%esp)

如果 ESP 寄存器用作寻址内存中目标操作数的基址寄存器,则 POP 指令在 ESP 寄存器递增后计算操作数的有效地址。

所以是的,您的伪代码是正确的,除了修改 FLAGS 和%edx.

于 2013-02-19T22:50:58.690 回答
1

是的,除了对 FLAGS 的影响之外,这些序列是正确的,当然push %esp不会破坏%edx。相反,如果您想将其分解为单独的步骤,请想象一个内部临时1push ,而不是考虑在执行任何其他操作之前对其输入(源操作数)进行快照的原始操作。

(类似地pop DST可以建模为pop %temp/ mov %temp, DST,所有 pop 的效果在它评估并写入目标之前完成,即使那是或涉及堆栈指针。)

push即使在 ESP 特殊情况下也有效的等价物

(在所有这些中,我假设 SS 配置正常的 32 位兼容或保护模式,堆栈地址大小与模式匹配,如果情况并非如此的话。64 位模式等效于%rsp工作与-8/相同+8。16 位模式不允许(%sp)寻址模式,因此您必须将其视为伪代码。)

#push SRC         for any source operand including %esp or 1234(%esp)
   mov  SRC, %temp
   lea  -4(%esp), %esp         # esp-=4 without touching FLAGS
   mov  %temp, (%esp)

mov SRC, %temppush %temp
或者因为我们无论如何都在描述一个不可中断的事务(单push条指令),所以
我们不需要在存储之前移动 ESP

#push %REG              # or immediate, but not memory source
   mov  %REG, -4(%esp)
   lea  -4(%esp), %esp

(这个更简单的版本不会真正与内存源组装,只有寄存器或立即数,如果中断或信号处理程序在 mov 和 LEA 之间运行,则不安全。在实际组装中,mov mem, mem具有两种显式寻址模式是' t 可编码,但push (%eax)因为内存目标是隐式的。即使对于内存源,您也可以将其视为伪代码。但是临时快照是内部发生的更现实的模型,例如第一个块或mov SRC, %temp/ push %temp。)

如果您正在谈论在实际程序中实际使用这样的序列,我认为没有一种方法可以在没有临时寄存器(第一个版本)或(第二个版本)禁用中断或使用红色 ABI 的情况下完全复制push %esp-区。(就像 x86-64 System V 的非内核代码一样,所以你可以复制push %rsp.)

pop等价物:

#pop DST   works for any operand
  mov  (%esp), %temp
  lea  4(%esp), %esp      # esp += 4 without touching FLAGS
  mov  %temp, DST         # even if DST is %esp or 1234(%esp)

pop %temp/ mov %temp, DST。这准确地反映了DST涉及 ESP 的内存寻址模式的情况:使用增量后的 ESP 值。我用 ; 验证了英特尔的文档push $5pop -8(%esp). 当我在 Skylake CPU 上的 GDB 中单步执行它时,这会将dword复制5到所写的 dword 的正下方。push如果-8(%esp)在该指令执行之前使用 ESP 进行地址计算,则会有 4 个字节的间隙。

在 的特殊情况下pop %esp,是的,它会增加增量,简化为:

#pop %esp  # 3 uops on Skylake, 1 byte
   mov  (%esp), %esp             # 1 uop on Skylake.  3 bytes of machine-code size

英特尔手册有误导性伪代码

英特尔在其指令集手册条目(SDM vol.2)的操作部分中的伪代码不能准确反映堆栈指针的特殊情况。只有描述部分中的额外段落(在@nrz 的答案中引用)才能做到这一点。

https://www.felixcloutier.com/x86/pop显示(对于 StackAddrSize = 32 和 OperandSize = 32)加载到 DEST然后递增 ESP

     DEST ← SS:ESP; (* Copy a doubleword *)
     ESP ← ESP + 4;

但这具有误导性,pop %esp因为它意味着 ESP += 4 发生在 ESP = load(SS:ESP) 之后。正确的伪代码将使用

 if ... operand size etc.
     TEMP ← SS:ESP; (* Copy a doubleword *)
     ESP ← ESP + 4;

 ..
 // after all the if / else size blocks:
 DEST ← TEMP 

pshufb对于其他指令,例如伪代码开始TEMP ← DEST于快照读写目标操作数的原始状态,英特尔获得了这一权利。

同样,https://www.felixcloutier.com/x86/push#operation显示 RSP 首先递减,而不显示在此src之前快照的操作数。只有文本描述部分中的额外段落才能正确处理这种特殊情况。


AMD 的手册第 3 卷:通用和系统说明(2021 年 3 月)对此同样错误(我强调):

将堆栈指针 (SS:rSP) 指向的值复制到指定的寄存器或内存位置 ,然后将 rSP 递增 2(对于 16 位弹出)、4(对于 32 位弹出)或 8(对于 64 位)流行音乐。

与英特尔不同,它甚至没有记录弹出堆栈指针本身或涉及 rSP 的内存操作数的特殊情况。至少不在这里,并且搜索push rsppush esp没有找到任何东西。

(AMDrSP根据 SS 选择的当前堆栈大小属性来表示 SP / ESP / RSP。)

AMD没有像英特尔那样的伪代码部分,至少没有像推/弹出这样的简单指令。(有一个用于pusha。)


脚注 1:这甚至可能发生在某些 CPU 上(尽管我不这么认为)。例如,在 Skylake 上,Agner Fog 测得 push %esp前端为 2 微秒,而推送任何其他寄存器为 1 微融合存储。

我们确实知道 Intel CPU 确实有一些寄存器像架构寄存器一样被重命名,但只能通过微码访问。例如https://blog.stuffedcow.net/2013/05/measuring-rob-capacity/提到“一些额外的架构寄存器供内部使用”。所以mov %esp, %temp/push %temp理论上可能是它的解码方式。

但更可能的解释是,在长指令序列中额外测量的 uopspush %esp只是堆栈同步uops ,就像我们得到的任何时候 OoO 后端在 push/pop 操作后显式读取 ESP 一样。例如push %eax/mov %esp, %edx也会导致堆栈同步 uop。(“堆栈引擎”避免了需要额外的 uopesp -= 4部分push

push %esp有时很有用,例如推送您刚刚保留的一些堆栈空间的地址:

  sub   $8, %esp
  push  %esp
  push  $fmt         # "%lf"
  call  scanf
  movsd 8(%esp), %xmm0

  # add $8, %esp    # balance out the pushes at some point, or just keep using that allocated space for something.  Or clean it up just before returning along with the space for your local var.

pop %esp在 Skylake 上花费 3 微指令,一个负载 (p23) 和两个 ALU,用于任何整数 ALU 端口 (2p0156)。所以它的效率更低,但它基本上没有用例。您不能有用地保存/恢复堆栈上的堆栈指针;如果您知道如何到达保存位置,则可以使用add.

于 2021-10-08T02:28:44.963 回答