“pushl/popl %esp”的汇编级表示是什么?

13

我正在尝试理解推送和弹出堆栈指针寄存器的行为。在AT&T格式中:

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. 

这是否正确?如果不是,我哪里出错了?


我已经使用x86汇编语言编程数十年了。我从未有过使用这些的机会。虽然它们被定义了,但在实践中真的很重要吗? - Ira Baxter
不,我的教科书提到它在实践中从未被使用过,但这是一个很好的练习,可以理解指令约定。 - amorimluc
5
可以。但我会花时间思考指令,如“enter”、“leave”、“cmpsd”、“lea”,这些指令看起来有些奇怪,但在正确的情况下非常有用。 - Ira Baxter
1
当您想将指向堆栈缓冲区的指针传递给使用堆栈参数调用约定的函数时,“pushl%esp”可能非常有用。例如,您可以使用“sub $ 8,%esp” /“push%esp” /“push $ fmt” /“call scanf”在32位代码中从stdin读取“double”。 - Peter Cordes
2个回答

13
根据Intel® 64和IA-32架构开发人员手册:综合版(实际上在vol.2或HTML爬取在https://www.felixcloutier.com/x86/push),关于push esp的说明如下:
PUSH ESP指令将执行该指令之前ESP寄存器的值推送到堆栈中。如果PUSH指令使用一个内存操作数,其中ESP寄存器用于计算操作数地址,则在减小ESP寄存器之前计算操作数的地址。
至于pop esphttps://www.felixcloutier.com/x86/pop):
POP ESP指令在写入旧顶部的数据到目标之前增加堆栈指针(ESP)。
还有pop 16(%esp) 如果ESP寄存器用作寻址内存目标操作数的基址寄存器,则POP指令在将ESP寄存器递增后计算操作数的有效地址。
所以是的,您的伪代码正确,除了修改FLAGS和%edx

好的,谢谢 nrz。你认为我写的汇编行为是正确的吗? - amorimluc
1
@amorimluc,你的代码看起来没问题,与英特尔文档相符。 - nrz

3

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

(同样,pop DST可以被建模为pop %temp / mov %temp, DST,在评估和写入目标之前完成所有弹出的效果,即使那是或涉及堆栈指针。)

push等效项,即使在ESP特殊情况下也有效

(在所有这些中,我假设32位兼容或受保护模式下SS配置正常,堆栈地址大小与模式匹配,即使可能不是这种情况。使用%rsp的64位模式等效项以相同的方式工作,具有-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,%temp ; push %temp
或者既然我们正在描述一个不可中断的事务(单个push指令),
我们在存储之前不需要移动ESP
#push %REG              # or immediate, but not memory source
   mov  %REG, -4(%esp)
   lea  -4(%esp), %esp

这个简化版本在内存源方面不会真正地汇编,只有寄存器或立即数,同时在mov和LEA之间如果中断或信号处理程序运行,也是不安全的。在实际汇编中,使用两个显式寻址模式的mov mem, mem无法被编码,但push (%eax)可以,因为内存目标是隐含的。即使对于内存源,您也可以将其视为伪代码。但是,在临时快照中进行拍摄是一个更现实的模型,就像第一个块或者像mov SRC,%temp / push %temp这样。

如果你要在实际程序中使用这样的序列,我认为没有办法完全复制push %esp而不使用临时寄存器(第一版本),或者禁用中断或具有红区的ABI(第二版本)。 (例如,x86-64系统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 精确反映了涉及 ESP 的内存寻址模式的情况: 在增量之后使用 ESP 的值。我通过在 Skylake CPU 上使用 GDB 单步调试并验证 Intel 的文档,使用 push $5 ; pop -8(%esp)。这将 dword 5 复制到由 push 写入的 dword 正下方,如果在此之前使用 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's answer中引用)才是正确的。

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)指向的值复制到指定的寄存器或内存位置,然后对于16位pop增加rSP 2个单位,对于32位pop增加4个单位,对于64位pop增加8个单位。

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

(AMD使用rSP表示根据SS选择的当前堆栈大小属性选择的SP / ESP / RSP。)

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


注1:这甚至可能是某些CPU上发生的情况(尽管我不这么认为)。例如,在Skylake上,Agner Fog测量push %esp作为前端的2个uops,而将任何其他寄存器推送到1个微型融合存储器。

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

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

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 恢复它。


我不理解你所说的特定评论。你说mov %REG, -4(%esp)在“真正的汇编”中不起作用。为什么?我刚刚测试了一下,像movl %esp, -4(%esp)这样的东西完全可以工作。请澄清一下。谢谢!(完全透明:我正在学习汇编语言,和OP一样来自同一本书。我会按照你建议的方式重新编写pushl,认为它实际上可以工作-我相信它实际上确实可以。) - user5683823
@mathguy:在没有红区的ABI中,ESP以下的数据可能会被异步覆盖。在ESP以下写入是否有效? 通常情况下是可以的,事实上,在用户空间中只有一个信号处理程序(Linux)或SEH(Windows)可以破坏它,或者如果您使用“print foo()”停止调试器并使调试器运行使用您的进程的堆栈的函数。这就是为什么我说因为我们正在描述一个不可中断的事务,因为mov %REG, -4(%esp)会使数据在ESP移动之前处于易受攻击的状态。 - Peter Cordes
好的 - 这解释了为什么不应该以那种方式复制 pushl(在某个时候我会理解你所解释的)。但是让我困惑的是非常普遍的说法,例如 movl %reg, mem 不可“编码”。也许我被“编码”这个词弄糊涂了 - 我认为它的意思是“有效”或“允许”。它似乎是“有效”的和“允许”的(即使如你所说,在某些情况下并不建议这样做)。 - user5683823
@mathguy:但我认为你在问带有括号的段落,其中在真正的汇编中,使用两个显式寻址模式的mov mem, mem是无法编码的。我进行了编辑以澄清该点;它解释了为什么那个更简单的块不能作为pushl (%eax)或其他内存源推送的替代品进行汇编,只能用于寄存器或立即数。movl (%eax), -4(%esp)不是x86机器码可以表达的内容。 - Peter Cordes
好的,明白了 - 我们同时在写。 - user5683823
@mathguy:是的,我花了一分钟回顾了一个月前我的答案,才弄清楚我自己在说什么;不奇怪其他人可能不太明白,尤其是对于那些还不熟悉我所写的内容的初学者。 :P - Peter Cordes

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