我可以通过这样做将4个字节压入堆栈:
push DWORD 123
但我发现我可以在不指定操作数大小的情况下使用push
:
push 123
在这种情况下,push
指令推入堆栈的字节数是多少?推送的字节数是否取决于操作数大小(因此在我的示例中它将推送1个字节)?
答案 0 :(得分:9)
推送的字节数是否取决于操作数大小
它并不取决于数字的价值。关于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
大小,并让汇编程序选择编码。这几乎总是你想要的。如果这就是你想知道的,你现在就可以停止阅读了。
首先,了解 x86机器代码中push
的大小是多少有用:
没有其他选择。堆栈指针总是按push 2 的操作数大小递减。 (所以它可以通过推动16位而错误对齐"堆栈)。 pop
具有相同的大小选择:16,32或64,但64位模式下没有32位弹出。
这适用于您是在推送注册表还是即时注册表,无论该符号表示符号扩展imm8
还是需要imm32
(或imm16
对于16位推送)。 (64位push imm32
符号扩展为64位。没有push imm64
,只有mov reg, imm64
)
在NASM源代码中,push 123
汇总到与您所处的模式匹配的操作数大小。在您的情况下,我认为您正在编写32位代码,因此push 123
是32位推送,即使它可以(并且确实)使用push imm8
编码。
你的汇编程序总是知道它汇编的是什么类型的代码,因为当你强制执行操作数时,它必须知道何时使用或不使用操作数大小的前缀。
MASM是一样的;唯一可能不同的是强制使用不同操作数大小的语法。
您在汇编程序中编写的任何内容都将汇编为一个有效的机器代码选项(因为编写汇编程序的人知道什么是可编程的,不可编码的),所以不,你不能推动带有push
指令的单字节。如果您需要,可以使用dec esp
/ mov byte [esp], 123
从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中,一个狭窄的推动 - > wide pop占用的代码字数少于shift / OR以将两个16b整数组合成32b值。
或许在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
。 (两者都执行相同)。微软也赞同某些Windows DLL中的using a 40h
REX as padding on push rbx
。
英特尔只是说32位推送是不可编码的&#34; (长表中的N.E.)当需要REX前缀时,我不明白为什么W = 1不是push
/ pop
的标准编码,但显然选择是任意的。
Fun-fact:只有堆栈指令,而其他一些指令在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-在64位模式下位(但不是16位)。 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
args被填充到4B或8B,因此堆栈保持对齐。 (现代32位和64位调用约定,例如Linux使用的x86-32 System V psABI,在函数调用之前保持堆栈16B对齐,即使堆栈上的arg&#34; slot&#34;仍然只是4B) 。无论如何,&#34;堆栈宽度&#34;在任何架构上都是only a programming convention。
x86 ISA中最接近&#34;堆栈宽度&#34;是push
/ pop
的默认操作数大小。但是你可以根据需要操纵堆栈指针,例如sub esp,1
。你可以,但不是出于性能原因:P
答案 1 :(得分:1)
计算机中的“堆栈宽度”是可以压入堆栈的最小数据量,它被定义为处理器的寄存器大小。这意味着如果您正在处理具有16位寄存器的处理器,则堆栈宽度将为2个字节。如果处理器有32位寄存器,则堆栈宽度为4个字节。如果处理器有64位寄存器,则堆栈宽度为8个字节。
使用现代x86 / x86_64系统时不要混淆;如果系统以32位模式运行,则堆栈宽度和寄存器大小为32位或4个字节。如果切换到64位模式,那么只有那时寄存器和堆栈大小才会改变。