是的,除了对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
的特殊情况下,是会影响增量的,简化为:
mov (%esp), %esp
英特尔手册的伪代码误导性很大
在英特尔指令集手册(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 rsp
或push 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
恢复它。