【问题标题】:learning disassembly学习拆解
【发布时间】:2011-01-10 19:35:34
【问题描述】:

为了了解底层发生的事情,我正在编写小型 C 程序,然后将其反转,并尝试了解其 objdump 输出。

C 程序是:

#include <stdio.h>

int function(int a, int b, int c) {
    printf("%d, %d, %d\n", a,b,c);
}

int main() {
    int a;
    int *ptr;

    asm("nop");
    function(1,2,3);
}

函数的 objdump 输出给了我以下信息。

080483a4 <function>:
 80483a4:   55                      push   ebp
 80483a5:   89 e5                   mov    ebp,esp
 80483a7:   83 ec 08                sub    esp,0x8
 80483aa:   ff 75 10                push   DWORD PTR [ebp+16]
 80483ad:   ff 75 0c                push   DWORD PTR [ebp+12]
 80483b0:   ff 75 08                push   DWORD PTR [ebp+8]
 80483b3:   68 04 85 04 08          push   0x8048504
 80483b8:   e8 fb fe ff ff          call   80482b8 <printf@plt>
 80483bd:   83 c4 10                add    esp,0x10
 80483c0:   c9                      leave  

请注意,在调用 printf 之前,三个偏移量为 8、16、12 的 DWORD(它们必须是 function 的参数以相反的顺序)被压入堆栈。稍后将推送一个十六进制地址,该地址必须是格式字符串的地址。

My doubt is

  1. 与其直接将 3 个 DWORDS 和格式说明符压入堆栈,我希望看到 esp 被手动递减,然后值被压入堆栈。如何解释这种行为?

【问题讨论】:

  • objdump 的-l 开关(这是一个小写的 L)可能会对您有所帮助;如果没有这个,那么在未来。
  • 推送会修改 esp。为什么你期望它也应该明确地完成?
  • 是的,来自push的描述:“递减堆栈指针,然后将源操作数存储在堆栈顶部”。
  • 这个汇编没有意义。为什么编译器会从内存中推送文字常量而不是生成立即数?那个 C 代码是真正的源代码吗?
  • 是的,那是真正的源代码。而且编译一直没有优化,可能是这个原因吧?

标签: c gcc assembly disassembly


【解决方案1】:

嗯,有些机器的堆栈指针有点像任何其他寄存器,所以你压入东西的方式是,是的,减量后存储。

但有些机器,例如 x8632/64 有一个执行宏操作的 push 指令:递减指针 执行商店。

宏操作,顺便说一句,有一段有趣的历史。有时,某些机器上的某些示例比使用简单指令执行基本操作要慢。

我怀疑今天是否经常出现这种情况。现代 x86 非常复杂。 CPU会将您的操作码本身分解为微操作,然后将其存储在缓存中。微操作有特定的管道和时隙要求,最终结果是现在 x86 内部有一个 RISC cpu,整个事情进展得非常快并且具有良好的架构层代码密度.

【讨论】:

  • 在很多情况下,在 x86 上使用微编码宏操作比将它们分解成各自的组成操作并单独发布它们要慢得多。 i7 上的 ssqrt 就是一个例子;它比使用估计的倒数运算要慢得多,然后是 NR 细化和乘法。这是因为微编码操作不是流水线的,这意味着它们后面的任何指令都必须等待整个宏操作完成,无论它们实际上是否依赖。
  • 也就是说,常用的宏操作(如PUSH)确实很快。但是,如果您使用任何 REP 字符串操作,上帝会帮助您。
【解决方案2】:

使用push 指令调整堆栈指针。所以它被复制到ebp 并且参数被推入堆栈,因此它们分别存在于两个位置:function 的堆栈和printf 的堆栈。 pushes 影响esp,因此ebp 被复制。

【讨论】:

  • 感谢您的回答和有关“-l”标志的信息。
【解决方案3】:

没有mov [esp+x], [ebp+y]指令,操作数太多。它需要两条指令并使用一个寄存器。 Push 在一条指令中完成。

【讨论】:

    【解决方案4】:

    这是 x86 机器的标准 cdecl 调用约定。有几种不同类型的调用约定。您可以在 Wikipedia 中阅读以下有关它的文章:

    http://en.wikipedia.org/wiki/X86_calling_conventions

    它解释了基本原理。

    【讨论】:

      【解决方案5】:

      你提出了一个有趣的观点,我认为到目前为止还没有直接解决。我想您已经看到了如下所示的汇编代码:

      sub esp, X
      ...
      mov [ebp+Y], eax
      call Z
      

      这种反汇编是由某些编译器生成的。它所做的是扩展堆栈,然后将新空间的值分配为eax(希望到那时已经填充了一些有意义的东西)。这实际上相当于push 助记符的作用。我无法回答为什么某些编译器会生成此代码,但我的猜测是,在某些时候这样做会被认为更有效。

      【讨论】:

      • GCC 默认执行此操作。优点是 ESP 对于整个函数保持不变(忽略调用)。这也更适合 cdecl 约定,因为编译器不必在每次调用后调整 ESP。但是,mov 指令比 push 指令要长,而且生成的代码大小也不同 can be quite significant
      【解决方案6】:

      在您努力学习汇编语言和反汇编二进制文件的过程中,您可能会发现 ODA 很有用。它是一个基于 Web 的反汇编程序,可以方便地反汇编许多不同的架构,而无需为每个架构构建 binutil 的 objdump。

      http://onlinedisassembler.com/

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-10-23
        • 2015-08-06
        • 1970-01-01
        • 1970-01-01
        • 2023-03-17
        • 2021-06-02
        • 2018-08-15
        • 2016-06-09
        相关资源
        最近更新 更多