推送的字节数是否取决于操作数大小
它不依赖于数字的值。表示push 推送多少字节的技术 x86 术语是“操作数大小”,但这与数字是否适合 imm8 是分开的。
另见Does each PUSH instruction push a multiple of 8 bytes on x64?
(所以在我的示例中它将推送 1 个字节)?
不,立即数的大小不是操作数大小。它总是在 32 位代码中推送 4 个字节,或者在 64 位代码中推送 64 个字节,除非你做了一些奇怪的事情。
建议:始终只写push 123 或push 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 显然似乎将 byte 和 dword 覆盖视为适用于立即数的大小,但 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。
脚注:
-
实际上 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 位代码中做到这一点。或者更有可能是疏忽,如果它是可编码的,就会发生这种情况,但事实并非如此。
-
除了段寄存器是 16 位的,但普通的push fs 仍然会按堆栈宽度(操作数大小)递减堆栈指针。英特尔文档表明,在这种情况下,最新的英特尔 CPU 仅进行 16b 存储,其余 32 或 64b 未修改。
x86 没有正式的在硬件中强制执行的stack width。这是一个软件/调用约定术语,例如char 和 short 在任何调用约定中在堆栈上传递的参数都被填充为 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