是的,除了对 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 rsp 或push 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”避免了push 的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 uop,一个负载 (p23) 和两个用于任何整数 ALU 端口 (2p0156) 的 ALU。所以它的效率更低,但它基本上没有用例。您不能有用地保存/恢复堆栈上的堆栈指针;如果您知道如何到达保存位置,您可以使用add 恢复它。