【问题标题】:How many bytes does the push instruction push onto the stack when I don't specify the operand size?未指定操作数大小时,push 指令将多少字节压入堆栈?
【发布时间】:2017-12-21 00:46:12
【问题描述】:

我可以通过这样做将 4 个字节压入堆栈:

push DWORD 123

但我发现我可以在不指定操作数大小的情况下使用push

push 123

在这种情况下,push 指令将多少字节压入堆栈?推送的字节数是否取决于操作数的大小(所以在我的示例中它将推送 1 个字节)?

【问题讨论】:

  • 本机寄存器大小,以保持堆栈对齐。在 32 位模式下,它将推送 4 个字节。在 64 位模式下,它将推送 8 个字节。
  • 顺便说一句,您可以使用调试器对此进行测试。只需单步执行指令,即可查看esp/rsp 的变化。您还可以查看反汇编输出并注意到它们都汇编为相同的机器代码。

标签: assembly x86


【解决方案1】:

推送的字节数是否取决于操作数大小

它不依赖于数字的值。表示push 推送多少字节的技术 x86 术语是“操作数大小”,但这与数字是否适合 imm8 是分开的。

另见Does each PUSH instruction push a multiple of 8 bytes on x64?

(所以在我的示例中它将推送 1 个字节)?

不,立即数的大小不是操作数大小。它总是在 32 位代码中推送 4 个字节,或者在 64 位代码中推送 64 个字节,除非你做了一些奇怪的事情。

建议:始终只写push 123push 0x12345 以使用默认的push 大小,让汇编器选择编码。这几乎总是你想要的。如果这就是您想知道的全部内容,请立即停止阅读。


首先,了解push 在 x86 机器码中的大小是很有用的

  • 在 16 位模式下,您可以推送 16 位或(在 386 及更高版本上具有操作数大小前缀)32 位。
  • 在 32 位模式下,您可以推送 32 位或(带有操作数大小前缀)16 位。
  • 在 64 位模式下,您可以推送 64 位或(带有操作数大小前缀)16 位。
    REX.W=0 前缀not 允许您对 32 位推送进行编码。1

没有其他选择。堆栈指针总是按 push2 的操作数大小递减。 (因此有可能通过推送 16 位来“错位”堆栈)。 pop 具有相同的大小选择:16、32 或 64,除了在 64 位模式下没有 32 位弹出。

无论您是推送寄存器还是立即数,无论立即数是否适合符号扩展 imm8 或它需要 imm32(或 imm16 用于 16 位推送),这都适用。 (一个64位的push imm32符号扩展到64位。没有push imm64,只有mov reg, imm64

在 NASM 源代码中,push 123 汇编为与您所在模式匹配的操作数大小。 在您的情况下,我认为您正在编写 32 位代码,所以 @ 987654347@ 是 32 位推送,尽管它可以(并且确实)使用 push imm8 编码。

您的汇编器总是知道它正在汇编什么样的代码,因为当您强制操作数大小时,它必须知道何时使用或不使用操作数大小前缀。

MASM 是一样的;唯一可能不同的是强制使用不同操作数大小的语法。

您在汇编程序中编写的任何内容都将汇编为有效的机器代码选项之一(因为编写汇编程序的人知道什么是可编码的,什么是不可编码的),所以不,您不能用 a 推送单个字节push 指令。如果你想这样,你可以用dec esp / mov byte [esp], 123 模拟它


NASM 示例:

nasm -l /dev/stdout 的输出将列表连同原始源代码行转储到终端。

轻微编辑以将操作码和前缀字节与操作数分开。 (与objdump -drwC -Mintel 不同,NASM 的反汇编格式不会在机器码 hexdump 中的字节之间留下空格)。

 68 80000000         push 128
 6A 80               push -128                 ;; signed imm8 is -128 to +127
 6A 7B               push byte 123
 6A 7B               push dword 123            ;; still optimized to the imm8 encoding
 68 7B000000         push strict dword 123
 6A 80               push strict byte 0x80     ;; will decode as push -128
 ******************       warning: signed byte value exceeds bounds [-w+number-overflow]

dword 通常是操作数大小的东西,而strict dword 是您要求汇编器不将其优化为更小的编码的方式。

上述所有指令都是 32 位推送(或 64 位模式下的 64 位,具有相同的机器代码)。以下所有指令都是 16 位推送,无论您以何种模式组装它们。(如果以 16 位模式组装,它们将没有 0x66 操作数大小前缀)

 66 6A 7B            push word 123
 66 68 8000          push word 128
 66 68 7B00          push strict word 123

NASM 显然似乎将 bytedword 覆盖视为适用于立即数的大小,但 word 适用于指令的操作数大小。实际上在 64 位模式下使用 o32 push 12 也不会收到警告。 push eax 确实如此:“错误:64 位模式不支持指令”。

请注意,push imm8 在所有模式下都被编码为6A ib。没有操作数大小前缀,操作数大小是模式的大小。 (例如,6A FF 在长模式下解码为 64 位操作数大小的推送,操作数为 -1,将 RSP 减 8 并执行 8 字节存储。)


地址大小前缀仅影响用于使用内存源推送的显式寻址模式,例如在 64 位模式下:push qword [rsi](无前缀)与 push qword [esi](32 位寻址模式的地址大小前缀)。 push dword [rsi] 不可编码,因为在 64 位代码1 中,没有任何东西可以使操作数大小变为 32 位。 push qword [esi] 不会将 rsp 截断为 32 位。显然“堆栈地址宽度”是另一回事,可能设置在段描述符中。 (在普通操作系统上,64 位代码始终是 64,我认为即使是 Linux's x32 ABI:长模式下的 ILP32。)


您什么时候想要推送 16 位?如果您出于性能原因使用 asm 编写,那么可能永远不会。在my code-golf adler32 中,将两个 16b 整数组合成一个 32b 值相比,窄推 -> 宽弹出所需的代码字节数比 shift/OR 少。

或者在 64 位代码的利用中,您可能希望将一些数据无间隙地推送到堆栈中。您不能只使用push imm32,因为该符号或零会扩展到 64 位。您可以使用多个 16 位推送指令在 16 位块中执行此操作。但对于mov rax, imm64 / push rax (对于 8B imm 有效载荷,10B+1B = 11B)可能仍然更有效。或push 0xDEADBEEF / mov dword [rsp+4], 0xDEADC0DE(5B + 8B = 13B 并且不需要寄存器)。四个 16 位推送将占用 16B。


脚注

  1. 实际上 REX.W=0 被忽略,并且不会修改操作数大小使其远离其默认的 64 位。 NASM、YASM 和 GAS 都将push r12 组装到41 54,而不是49 54。 GNU objdjump 认为 49 54 不寻常,并将其解码为 49 54 rex.WB push r12。 (两者执行相同)。 Microsoft 也同意,using a 40h REX as padding on push rbx 在某些 Windows DLL 中。

    英特尔只是说 32 位推送在长模式下是“不可编码的”(表中的 N.E.)。当需要 REX 前缀时,我不明白为什么 W=1 不是 push / pop 的标准编码,但显然选择是任意的。

    有趣的事实:只有堆栈指令和其他一些在 64 位模式下默认为 64 位操作数大小。在机器代码中,add rax, rdx 需要一个 REX 前缀(设置了 W 位)。否则它将解码为add eax, edx。但是你不能在默认为 64 位时使用 REX.W=0 来减小操作数大小,只能在默认为 32 时增加它。

    http://wiki.osdev.org/X86-64_Instruction_Encoding#REX_prefix 列出了在 64 位模式下默认为 64 位的指令。请注意,jrcxz 并不严格属于该列表,因为它检查的寄存器 (cx/ecx/rcx) 是由地址大小而不是操作数大小决定的,因此它可以被覆盖为 32 位(但不是16 位)在 64 位模式下。 loop 也一样。

    奇怪的是英特尔的指令参考手册条目push(HTML 摘录:http://felixcloutier.com/x86/PUSH.html显示了在 64 位模式下 32 位操作数大小的推送会发生什么(堆栈地址宽度可以为 64 的唯一情况,因此它使用 rsp)。也许它可以通过代码段描述符中的一些非标准设置以某种方式实现,所以你不能在正常操作系统下运行的正常 64 位代码中做到这一点。或者更有可能是疏忽,如果它是可编码的,就会发生这种情况,但事实并非如此。

  2. 除了段寄存器是 16 位的,但普通的push fs 仍然会按堆栈宽度(操作数大小)递减堆栈指针。英特尔文档表明,在这种情况下,最新的英特尔 CPU 仅进行 16b 存储,其余 32 或 64b 未修改。

    x86 没有正式的在硬件中强制执行的stack width。这是一个软件/调用约定术语,例如charshort 在任何调用约定中在堆栈上传递的参数都被填充为 4B 或 8B,因此堆栈保持对齐。 (现代 32 位和 64 位调用约定,例如 Linux 使用的 x86-32 System V psABI,在函数调用之前保持堆栈 16B 对齐,即使堆栈上的 arg“槽”仍然只有 4B)。无论如何,“堆栈宽度”在任何架构上都是 only a programming convention

    x86 ISA 中最接近“堆栈宽度”的是默认操作数大小push/pop。但是您可以随心所欲地操纵堆栈指针,例如sub esp,1。你可以,但不要出于性能原因:P

【讨论】:

  • 我有一个问题。你说过 它总是在 32 位代码中推送 4 个字节,或者在 64 位代码中推送 64,除非你做了一些奇怪的事情 并且 在 64 位模式下,你可以推送 64 或 (带操作数大小前缀)16 位。这意味着手动显式设置操作数大小很奇怪?
  • 无论如何...它对我来说似乎仍然有些魔力...为什么不能推送 64 位立即数,而 64 位寄存器可以?所以我们必须先将mov 的立即数放入寄存器,然后再将push。 2 条指令而不是 1 条...
  • @St.Antario:是的,16 位 push 很奇怪,除了 16 位模式。它会使堆栈错位,因此您只能将其用于疯狂的黑客攻击like merging two 16-bit values into a 32-bit value with minimum code-size,而以牺牲性能为代价。
  • @St.Antario:这是一个指令编码问题:唯一采用imm64 的指令是mov。其他的,如pushaddimul,都只采用 32 位(或 8 位)立即数,因为大多数常量都很小。你不想要一个 8 字节的立即数,这样你就可以添加或推送 1024。我不确定是否存在关于为什么 x86-64 设计为主要使用符号扩展的问答 -imm32(和符号扩展-disp32 在寻址模式中);我快速浏览了一下,但在为 stackoverflow.com/questions/48705762/pushing-imm32-ends-up-in 寻找更多重复项时没有找到一个
【解决方案2】:

计算机中的“堆栈宽度”,即可以压入堆栈的最小数据量,被定义为处理器的寄存器大小。这意味着如果您正在处理具有 16 位寄存器的处理器,则堆栈宽度将为 2 个字节。如果处理器有 32 位寄存器,则堆栈宽度为 4 字节。如果处理器有 64 位寄存器,则堆栈宽度为 8 字节。

在使用现代 x86/x86_64 系统时不要混淆;如果系统运行在 32 位模式下,堆栈宽度和寄存器大小为 32 位或 4 字节。如果切换到 64 位模式,那么只有这样,寄存器和堆栈的大小才会改变。

【讨论】:

  • IIRC,在 x86-64 中,您也可以推送 2 个字节的值,即堆栈宽度的 1/4。
  • @RudyVelthuis 是正确的。请参阅this code-golf answer,我使用它而不是 shift/OR 来合并事物。现在再看一遍,我的 cmets 说 push r16 仅将 RSP 修改了 2,而不是填充到“堆栈宽度”。
  • 另外,第一段并不完全准确:386 和更高版本上的 16 位模式仍然具有 32 位寄存器(可通过操作数大小前缀访问),但 push 没有操作数-大小前缀仍然推送 16 位。
  • 可以在 32 位或 64 位模式下推送 WORD,但您需要 PUSH 上的大小覆盖前缀。我认为任何汇编程序都不会这样做,除非您使用 WORDWORD PTR 指令明确要求它。第一段中更准确的措辞是the "operand-size attribute of the current code segment",但这确实很笨拙。它只是与当前 operating 模式匹配的寄存器宽度,因此很明显,如果您处于 16 位模式,它将是 2 个字节,即使 32 位寄存器可用。跨度>
  • 这绝对是错误的。在 64 位长模式下,无法将 32 位值压入堆栈。该表非常清楚这不是有效的编码。我怀疑这只是英特尔很难在其手册中清楚记录的情况之一,特别是因为他们尝试了这种千篇一律的方法,其中一份手册必须记录数十年的微处理器和多个指令集. 非常清楚的是:“在 64 位模式下,堆栈指针的大小始终为 64 位。” @Peter
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-03-02
  • 2019-11-23
  • 2014-04-23
  • 2012-06-03
  • 1970-01-01
  • 2016-11-12
相关资源
最近更新 更多