为什么使用 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
这可能有不同的原因。
在这种情况下,这似乎是因为代码更小:
带有 push
和 pop
组合的变体是 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
,也就是B9
和59
的来源:指令的低3位=001
)
另一个use-case纯粹是code-size:mov 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 9
; pop 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 缓存还不是很热。
我有一些来自 shell 代码负载的示例代码,显示了一个 for 循环并使用 push/pop 设置计数器:
push 9
pop ecx
为什么不能只用mov?
mov ecx, 9
这可能有不同的原因。
在这种情况下,这似乎是因为代码更小:
带有 push
和 pop
组合的变体是 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
,也就是B9
和59
的来源:指令的低3位=001
)
另一个use-case纯粹是code-size:mov 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 9
; pop 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 缓存还不是很热。