我试图理解堆栈指针寄存器的压入和弹出行为。在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.
是这样吗?如果不是,我错在哪里?
2条答案
按热度按时间ahy6op9u1#
正如它在Intel® 64 and IA-32 Architectures Developer's Manual: Combined Volumes中所说的那样(实际上是在第一卷中)。2,或HTML scraping at https://www.felixcloutier.com/x86/push):
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
之外,您的伪代码是正确的。ivqmmu1c2#
是的,这些序列是正确的,除了对FLAGS的影响,当然
push %esp
不会击败%edx
。相反,如果你想把它分解成单独的步骤,想象一个内部临时1,而不是考虑一个push
原语操作,它在做任何其他事情之前快照它的输入(源操作数)。(类似地,
pop DST
可以被建模为pop %temp
/mov %temp, DST
,pop的所有效果在它评估并写入目的地之前完成,即使这是或涉及堆栈指针。)push
等价物,即使在ESP特殊情况下也有效(In所有这些,我假设32位compat或保护模式,SS配置正常,堆栈地址大小匹配模式,如果它甚至可能不是这种情况。与
%rsp
等效的64位模式与-8
/+8
的工作方式相同。16位模式不允许(%sp)
寻址模式,因此您必须将其视为伪代码。)即
mov SRC, %temp
;push %temp
或者因为我们描述的是一个不可中断的事务(一条
push
指令),我们don't need to move ESP before storing:
(This更简单的版本不会与内存源进行真实的的汇编,仅限于寄存器或立即,以及如果中断或信号处理程序在mov和莱亚之间运行,则不安全。在真实的汇编中,具有两种显式寻址模式的
mov mem, mem
是不可编码的,但push (%eax)
是可编码的,因为内存目的地是隐式的。您可以将其视为伪代码,即使是内存源。但是在临时内存中进行快照是一种更真实的内部模型,比如第一个块或mov SRC, %temp
/push %temp
。)如果你说的是在一个真实的程序中实际使用这样的序列,我不认为有一种方法可以 * 精确地 * 复制
push %esp
而不使用临时寄存器(第一个版本),或者(第二个版本)禁用中断或使用带有红色区域的ABI。(类似于x86-64 System V的非内核代码,因此您可以复制push %rsp
。)pop
等价:即
pop %temp
/mov %temp, DST
。这准确地反映了DST
是涉及ESP的内存寻址模式的情况:使用增量后ESP * 的值。我用push $5
验证了Intel的文档;pop -8(%esp)
.当我在Skylake CPU上的GDB中单步执行push
时,它将dword5
复制到push
写入的dword的正下方。如果-8(%esp)
地址计算在该指令执行之前使用ESP进行,则会有4字节的间隙。在
pop %esp
的特殊情况下,是的,它会逐步增加,简化为:英特尔手册有误导性伪代码
英特尔在其指令集手册条目的操作部分中的伪代码(SDM vol.2)不能准确地反映堆栈指针的特殊情况。只有描述部分中的额外段落(在@nrz的回答中引用)才能做到这一点。
https://www.felixcloutier.com/x86/pop显示(对于StackAddrSize = 32和OperandSize = 32)加载到DEST,然后 * 递增ESP
但这对于
pop %esp
来说是误导性的,因为它意味着ESP += 4发生在ESP = load(SS:ESP)之后。正确的伪代码将使用英特尔对其他指令(如
pshufb
)也有这种权利,其中伪代码以TEMP ← DEST
开始,以快照读写目标操作数的原始状态。类似地, www.example.com 。只有文本描述部分中的额外段落才能正确处理这种特殊情况。
AMD手册第三册:通用和系统指令(2021年3月)在这方面同样错误(我的重点):
将堆栈指针(SS:rSP)指向的值复制到指定的寄存器或内存位置**,然后**将rSP递增2(16位弹出)、4(32位弹出)或8(64位弹出)。
与Intel不同,它甚至没有记录弹出堆栈指针本身或涉及rSP的内存操作数的特殊情况。至少这里没有,在
push rsp
或push esp
上搜索没有找到任何东西。(AMD根据SS选择的当前堆栈大小属性,使用
rSP
表示SP / ESP / RSP。)AMD没有像英特尔那样的伪代码部分,至少不是用于像push/pop这样的简单指令。(
pusha
有一个。)脚注1:这甚至可能发生在一些CPU上(尽管我不这么认为)。例如,在Skylake上,Agner Fog measured
push %esp
作为前端的2个uop与1个微熔丝存储器,用于推送任何其他寄存器。我们知道英特尔CPU确实有一些寄存器被重命名,就像架构寄存器一样,但这些寄存器只能通过微码访问。例如https://blog.stuffedcow.net/2013/05/measuring-rob-capacity/提到“一些用于内部使用的额外架构寄存器。“所以
mov %esp, %temp
/push %temp
理论上可能是它解码的方式。但一个更可能的解释是
push %esp
指令的长 * 序列 * 中额外测量的uop只是堆栈同步uop,就像我们在推送/弹出操作后OoO后端显式读取ESP时得到的任何情况一样。例如push %eax
/mov %esp, %edx
也会导致堆栈同步uop。(“stack engine”是避免push
的esp -= 4
部分需要额外uop的原因)push %esp
有时是有用的,e.例如,推送您刚刚保留的某个堆栈空间的地址:pop %esp
在Skylake上花费3个uops,一个负载(p23)和两个ALU用于任意整数ALU端口(2 p0156)。所以它的效率更低,但它基本上没有用例。您不能有效地保存/恢复堆栈上的堆栈指针;如果你知道如何到达你保存它地方,你可以用add
还原它。