【问题标题】:What is an assembly-level representation of pushl/popl %esp?pushl/popl %esp 的汇编级表示是什么?
【发布时间】:2013-02-04 19:16:50
【问题描述】:

我试图了解推送和弹出堆栈指针寄存器的行为。在 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 汇编程序进行编码。我从来没有任何机会使用这些。我确定它们已定义,但在实践中真的很重要吗?
  • 不,我的教科书提到它从未在实践中使用过,但它是理解指令约定的一个很好的练习。
  • 可能是。但我会花时间考虑诸如“enter”、“leave”、“cmpsd”、“lea”之类的指令,所有这些指令在适当的情况下都非常有用。
  • pushl %esp 在您想将指向堆栈缓冲区的指针传递给具有堆栈参数调用约定的函数时可能很有用。例如您可以使用 sub $8, %esp / push %esp / push $fmt / call scanf 从标准输入中读取 32 位代码中的 double

标签: assembly x86 stack-memory instruction-set stack-pointer


【解决方案1】:

正如 Intel® 64 and IA-32 Architectures Developer's Manual: Combined Volumes 中的 push esp 所说(实际上是在 vol.2 中,或在 https://www.felixcloutier.com/x86/push 处的 HTML 抓取):

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

关于pop esp (https://www.felixcloutier.com/x86/pop):

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

pop 16(%esp)

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

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

【讨论】:

  • 好的,谢谢nrz。你会说我写的汇编行为是正确的吗?
  • @amorimluc 您的代码在我看来是正确的,因为它符合英特尔文档。
【解决方案2】:

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

(类似地,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, %temp ; push %temp
或者因为我们无论如何都在描述一个不可中断的事务(单个 push 指令),
我们don't need to move ESP before storing:

#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 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 $5 验证了英特尔的文档; pop -8(%esp)。当我在 Skylake CPU 上的 GDB 中单步执行它时,这将 dword 5 复制到了由 push 编写的 dword 正下方的 dword。如果 -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'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's manual Volume 3: General-Purpose and System Instructions (March 2021) 对此同样错误(我强调):

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

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

(AMD 使用rSP 表示 SP / ESP / RSP,具体取决于 SS 选择的当前堆栈大小属性。)

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


脚注 1:这甚至可能发生在某些 CPU 上(尽管我不这么认为)。例如,在 Skylake 上,Agner Fog measuredpush %esp 作为前端的 2 微指令与用于推送任何其他寄存器的 1 微融合存储。

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

但更可能的解释是,在push %esp 指令的长序列中额外测量的微指令只是堆栈同步微指令 ,就像我们在推送/弹出操作后任何时候 OoO 后端显式读取 ESP 一样。例如push %eax / mov %esp, %edx 也会导致堆栈同步微操作。 (“stack engine”避免了pushesp -= 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 uop,一个负载 (p23) 和两个用于任何整数 ALU 端口 (2p0156) 的 ALU。所以它的效率更低,但它基本上没有用例。您不能有用地保存/恢复堆栈上的堆栈指针;如果您知道如何到达保存位置,您可以使用add 恢复它。

【讨论】:

  • 我不明白您的具体评论。你说mov %REG, -4(%esp) 在“真正的汇编”中是行不通的。为什么不?我刚刚测试过,像movl %esp, -4(%esp) 这样的东西工作得很好。请说清楚。谢谢! (完全披露:我只是在我们说话时学习汇编,并且来自与 OP 相同的书。我会完全按照您建议的“伪代码”重写pushl,认为它确实可以工作 - 我相信它确实可以.)
  • @mathguy:在没有红色区域的 ABI 中,ESP 下的数据可以被异步破坏。 Is it valid to write below ESP? 通常 没关系,事实上在用户空间中只有信号处理程序 (Linux) 或 SEH (Windows) 可以破坏它,或者如果您使用调试器停止并使用 print foo()使调试器使用您的进程堆栈在您的进程中运行一个函数。这就是为什么我说因为我们正在描述一个不间断的事务,因为mov %REG, -4(%esp) 使数据易受攻击,直到 ESP 移动。
  • 好的 - 这解释了为什么不应该以这种方式复制pushl(在某些时候我会理解你的解释)。但让我感到困惑的是,movl %reg, mem 之类的东西不是“可编码”的。也许我对“可编码”这个词感到困惑——我认为它的意思是“有效”或“允许”。它似乎是“有效的”和“允许的”(即使,正如你所说,在某些情况下是不可取的)。
  • @mathguy:但我认为您是在询问括号中的段落,在实际汇编中,具有两种显式寻址模式的mov mem, mem 是不可编码的。我进行了编辑以澄清这一点;它解释了为什么那个更简单的块不会组装为pushl (%eax) 或其他内存源推送的替代品,只能注册或立即。 movl (%eax), -4(%esp) 不是 x86 机器码可以表达的。
  • 好的,明白了——我们同时在写作。
猜你喜欢
  • 2010-11-04
  • 2014-12-10
  • 2011-02-02
  • 2018-08-07
  • 2013-01-29
  • 1970-01-01
  • 1970-01-01
  • 2014-05-15
  • 2019-09-06
相关资源
最近更新 更多