为什么使用 Push/Pop 而不是 Mov 将数字放入 shellcode 的寄存器中?

Why use Push/Pop instead of Mov to put a number in a register in shellcode?

我有一些来自 shell 代码负载的示例代码,显示了一个 for 循环并使用 push/pop 设置计数器:

push 9
pop ecx

为什么不能只用mov?

mov ecx, 9

这可能有不同的原因。

在这种情况下,这似乎是因为代码更小:

带有 pushpop 组合的变体是 3 个字节长,mov 指令是 5 个字节长。

但是,我猜 mov 变体更快...

本质上完全一样。将 9 推入堆栈然后将其弹出到 ecx 寄存器,这与 mov ecx, 9 基本相同。我个人认为 9 到 ecx 可能比将 9 推入堆栈然后将其弹出到 ecx 更有效,但我认为处理时间是这不是问题,所以考虑到代码有多小,它们都同样快。

是的,出于性能原因,通常您应该始终使用 mov ecx, 9 它 运行 比 push/[=12= 更有效],作为可以在任何端口上 运行 的 single-uop 指令。 (在 Agner Fog 测试过的所有现有 CPU 中都是如此:https://agner.org/optimize/


push imm8 / pop r32 的正常原因是机器代码没有零字节。这对于 shellcode 很重要,它必须通过 strcpy 或任何其他将其视为 implicit-length C 字符串一部分的方法来溢出缓冲区,并以 0 字节.

mov ecx, immediate 仅适用于 32 位立即数,因此机器代码看起来像 B9 09 00 00 00。对比 6a 09 推 9 ; 59 弹出 ecx.

(ECX是寄存器号1,也就是B959的来源:指令的低3位=001)


另一个use-case纯粹是code-sizemov r32, imm32是5个字节(使用no ModRM编码把寄存器号放在操作码的低 3 位),因为不幸的是 x86 缺少 mov 的 sign-extended imm8 操作码(没有 mov r/m32, imm8)。几乎所有可追溯到 8086 的 ALU 指令都存在这种情况。

在 16 位 8086 中,该编码不会保存任何 space:3 字节 short-form mov r16, imm16 与假设的 [=29] 一样好=] 几乎所有的东西,除了将立即数移动到需要 mov r/m16, imm16 形式(带有 ModRM 字节)的内存中。

由于 386 的 32 位模式没有添加特定于该模式的新操作码,只是更改了默认值 operand-size 和即时宽度,因此 32 位模式下 ISA 中的这种“优化缺失”始于386. full-width 立即数长 2 个字节,add r32,imm32 现在比 add r/m32, imm8 长。参见 。但是我们没有 mov 的选项,因为没有 sign-extends(或 zero-extends)它的立即数的 MOV 操作码。

有趣的事实:clang -Oz(即使牺牲速度也能优化大小)will compile int foo(){return 9;}push 9pop rax。 GCC12也支持类似的-Oz.

另请参阅 Codegolf.SE 上的 Tips for golfing in x86/x64 machine code(一个关于优化大小的网站,通常是为了好玩,而不是将代码放入小 ROM 或引导扇区。但对于机器代码,优化大小有时确实有实际应用,即使以牺牲性能为代价。)

如果您已经有了另一个内容已知的寄存器,则可以用 3 字节 lea ecx, [eax-0 + 9] 在另一个寄存器中创建 9(如果 EAX 保持 0)。只需 Opcode + ModRM + disp8。因此,如果您已经打算 xor-zero 任何其他寄存器,则可以避免 push/pop 黑客攻击。 lea 的效率略低于 mov,您可以在优化速度时考虑它,因为较小的 code-size 在大规模情况下具有较小的速度优势:L1i 缓存命中,有时解码uop 缓存还不是很热。