【问题标题】:x86 Assembly Why use Push/Pop instead of Mov?x86 Assembly 为什么使用 Push/Pop 而不是 Mov?
【发布时间】:2019-10-30 07:26:04
【问题描述】:

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

push 9
pop ecx

为什么不能只用mov?

mov ecx, 9

【问题讨论】:

  • 你有没有机会在漏洞利用或 shellcode 中看到这一点?我问是因为这种技术的优点是它不会在编码中添加 NUL(0) 字节。 mov ecx, 9 在其编码中确实有零。我可以看到这一点有几个原因 a) 程序员是汇编新手并且代码很糟糕,(b) 它的编码比mov 短 (c) 在 push 和 pop 之间有一个标签,并且 pop 位于循环的顶部,(d) 有人试图在 16 字节边界上对齐循环顶部,(e) 有人正在编码以避免编码中的 NUL 字节(shell 漏洞利用)
  • (f) 这是编译器生成的,可能是代码生成和有限/无优化(错过优化)的结果
  • 这也可能是需要避免 NUL 字节的 shell 代码。
  • 谢谢,这是来自恶意代码示例 :)
  • 没问题。有一种预感,这可能是一种利用;-)。这就是为什么我觉得有必要在我的第一条评论中提问。这是它实际上最有意义的一个地方。我会更新标签和问题。

标签: assembly x86 exploit shellcode


【解决方案1】:

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


push imm8 / pop r32 的正常原因是机器码没有零字节。这对于必须通过strcpy 或任何其他将缓冲区视为以0 字节终止的隐式长度C 字符串的一部分的方法溢出缓冲区的shellcode 很重要。

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

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


另一个用例是纯代码大小mov r32, imm32 是 5 个字节(使用将寄存器号放在操作码的低 3 位的无 ModRM 编码),因为 x86不幸的是,缺少mov 的符号扩展 imm8 操作码(没有mov r/m32, imm8)。几乎所有可追溯到 8086 的 ALU 指令都存在这种情况。

在 16 位 8086 中,这种编码不会节省任何空间:对于几乎所有内容,3 字节短格式 mov r16, imm16 与假设的 mov r/m16, imm8 一样好,除了将立即数移动到内存其中需要mov r/m16, imm16 形式(带有 ModRM 字节)。

由于 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(即使以牺牲速度为代价优化大小)will compileint foo(){return 9;}push 9pop rax.

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

如果您已经有另一个已知内容的寄存器,则可以使用 3 字节 lea ecx, [eax-0 + 9] 在另一个寄存器中创建 9(如果 EAX 持有 0)。只需操作码 + ModRM + disp8。因此,如果您已经对任何其他寄存器进行异或归零,则可以避免 push/pop hack。 lea 的效率几乎不低于 mov,您可以在优化速度时考虑它,因为较小的代码大小在大规模中对速度的好处很小:L1i 缓存命中,如果 uop 缓存还没有,有时会解码热。

【讨论】:

    【解决方案2】:

    这可能有不同的原因。

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

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

    不过,我猜mov 变体更快...

    【讨论】:

    • 这是可信的。
    【解决方案3】:

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

    【讨论】:

    • 这不是答案,您可以评论问题
    • 它们的速度并不一样快。遇到性能无关紧要的情况意味着所有事情都同样快!您是如何决定性能无关紧要的?
    猜你喜欢
    • 2020-07-07
    • 2012-10-18
    • 2014-04-11
    • 2010-09-30
    • 1970-01-01
    • 1970-01-01
    • 2013-09-30
    • 1970-01-01
    • 2016-12-27
    相关资源
    最近更新 更多