我有一些来自shell代码有效载荷的示例代码,其中展示了一个for循环,使用push/pop设置计数器:
push 9
pop ecx
为什么不能只使用mov指令?
mov ecx, 9
mov ecx, 9
。它比push
/pop
更高效,因为它是一个单uop指令,可以在任何端口上运行。(这对Agner Fog测试的所有现有CPU都有效: https://agner.org/optimize/)
push imm8
/pop r32
的正常原因是机器码中没有零字节。这对于必须通过strcpy
或任何将其视为隐式长度C字符串的一部分并以0
字节终止的方法溢出缓冲区的shellcode非常重要。
mov ecx,immediate
仅适用于32位立即数,因此机器码看起来像B9 09 00 00 00
。与6a 09
push 9; 59
pop ecx相比。1
,这就是B9
和59
的来源:指令的低3位=001
)
mov r32,imm32
是5个字节(使用无ModRM编码,在操作码的低3位中放置寄存器号码),因为x86不幸地缺乏用于mov
的符号扩展imm8操作码(没有mov r/m32,imm8
)。几乎所有的ALU指令都可以追溯到8086年。mov r16,imm16
的3字节短格式与假设的mov r/m16,imm8
一样好,几乎适用于所有情况,除了将立即数移动到需要ModRM字节的内存中的情况下使用mov r/m16,imm16
形式。
由于386的32位模式没有增加特定于该模式的新操作码,只改变了默认的操作数大小和立即数宽度,因此32位模式中ISA中的这个“缺失优化”始于386。由于全宽立即数比原来多2个字节,add r32,imm32
现在比add r/m32,imm8
更长。详见x86 assembly 16 bit vs 8 bit immediate operand encoding。但是我们不能为mov
选择这个选项,因为没有MOV操作码可以符号扩展(或零扩展)其立即数。
有趣的事实: clang -Oz
(即以牺牲速度为代价优化大小)会编译 int foo(){return 9;}
成为 push 9
; pop rax
。 GCC12也支持类似的-Oz
。
如果您已经有另一个含有已知内容的寄存器,则可以使用3字节的lea ecx, [eax-0 + 9]
(如果EAX包含0
)在另一个寄存器中创建9。只需Opcode + ModRM + disp8。因此,如果您已经打算对其他寄存器进行xor-zero,则可以避免push/pop hack。lea几乎与mov一样有效,当优化速度时,您可以考虑它,因为在大规模上,较小的代码大小具有轻微的速度优劣:L1i缓存命中,有时解码(如果uop缓存尚未热)。
这可能有不同的原因。
在这种情况下,这似乎是因为代码更小:
使用push
和pop
组合的变体长度为3字节,而mov
指令长度为5字节。
然而,我猜测mov
变体更快...
最初的回答:
可能有不同的原因。
在这种情况下,这样做似乎是因为代码更小:
使用push
和pop
组合的变体长度为3字节,而mov
指令长度为5字节。
然而,我猜测mov
变体更快...
mov ecx, 9
在它的编码中确实有零。我可以想到几个原因:a)程序员刚开始学汇编语言,写的代码不好;b)这种编码比mov
更短;c)在push和pop之间有一个标签,并且pop在循环的顶部;d)有人试图将循环的顶部对齐到16字节边界上;e)有人编写代码以避免编码中的NUL字节(shell exploits)。 - Michael Petch