【问题标题】:Decompile C code with debug info?使用调试信息反编译 C 代码?
【发布时间】:2013-03-14 14:49:12
【问题描述】:

Java 和 Python 字节码比 C/C++ 编译器生成的已编译机器码相对容易反编译。

我无法找到令人信服的答案来解释为什么 -g 选项中的信息不足以进行反编译,但足以进行调试? Python/Java 字节码中包含哪些额外的东西,使反编译变得容易?

【问题讨论】:

  • 答案最适合堆栈交换网络的 area51 逆向工程
  • @Koushik:该网站已有 5 天的历史,似乎处于私人测试阶段。
  • @NPE 哦完全忘记了..希望公共测试版很快开始

标签: java c++ c reverse-engineering decompiling


【解决方案1】:

以下是其中的一些原因:

  1. Java 和 Python 字节码相对简单且高级,而某些 CPU(想想 x86)的指令集则极其复杂。
  2. 字节码与设计语言的结构非常相似。
  3. 在生成字节码时,Java 和 Python 执行的优化很少。这导致字节码与原始源代码的结构紧密对应。一个好的优化 C 或 C++ 编译器能够生成远离原始源代码的程序集。
  4. Java 和 Python 编译器很少,而 C 和 C++ 编译器很多。如果您针对的是单个已知编译器(或一小组已知编译器),则更容易生成高质量的反编译器。
  5. 与 C++ 相比,Python 和 Java 是相对简单的语言(这一点不适用于 C)。
  6. C++ 模板对质量反编译提出了许多挑战(这一点也不适用于 C)。
  7. C/C++ 预处理器。
  8. 在 Python 中,源文件和字节码文件之间存在一对一的关系。在 Java 中,关系是一个或多个字节码文件的一个来源。在 C 和 C++ 中,这种关系是多对多的,在源代码前端有很多重叠(想想标题)。

【讨论】:

  • 您好 NPE,感谢您的回答 :) 这些观点完全有道理。但是,您知道任何可以令人满意地反编译 -O0 -g 编译代码的 C/C++ 反编译器吗?
  • 我将添加 -g 只是为您提供标签名称,而没有它,二进制文件仅包含绝对地址和偏移量。这有助于阅读反编译的程序集,但永远不会让你回到 C。
  • @c.fogelklou:什么是“反编译”程序集?那不是C吗?
  • 我的意思是二进制文件转换为文本...反编译可能不是正确的词,但最终结果是包含在文本可读格式中的汇编代码。
  • @SaswatPadhi“反编译”程序集不必是 c。它可以是 c++/D/B/fortran 等。即取决于。
【解决方案2】:

我无法找到令人信服的答案来解释为什么来自 -g 选项的信息不足以进行反编译,但足以进行调试?

调试信息基本上只包含生成代码中的地址和源文件行号之间的映射。调试器不需要反编译代码——它只显示原始源代码。如果源文件丢失,调试器不会神奇地显示它们。

也就是说,调试信息的存在确实使反编译更容易。如果调试信息包括使用的类型和函数原型的布局,反编译器可以使用它并提供更精确的反编译。然而,在许多情况下,它仍可能与原始来源不同。

例如,这里有一个使用 Hex-Rays 反编译器反编译的函数,没有使用调试信息:

int __stdcall sub_4050A0(int a1)
{
  int result; // eax@1

  result = a1;
  if ( *(_BYTE *)(a1 + 12) )
  {
    result = sub_404600(*(_DWORD *)a1);
    *(_BYTE *)(a1 + 12) = 0;
  }
  return result;
}

由于它不知道a1 的类型,对其字段的访问表示为加法和强制转换。

这是加载符号文件后的相同功能:

void __thiscall mytree::write_page(mytree *this, PAGE *src)
{
  if ( src->isChanged )
  {
    cache::set_changed(this->cache, src->baseAddr);
    src->isChanged = 0;
  }
}

你可以看到它已经改进了很多。

至于为什么反编译字节码通常更容易,除了NPE的回答检查还有this

【讨论】:

    【解决方案3】:

    某些处理器,例如 x86 处理器,具有可变长度的指令。如果控制传递到指令的中间(= 第一个字节之后的任何位置),那也可以是有效指令(或多条指令)。这使得明确反汇编机器代码变得困难。 C/C++ 代码可以利用这个特性。

    在某些处理器和操作系统上,可以像执行代码一样执行数据,并像使用数据一样使用代码。这使得很难明确地将两者分开。而且,这也是 C/C++ 程序通常可以轻松完成的事情。

    在某些处理器和操作系统上,动态生成代码并执行它很容易,并且可以在运行时修改现有代码。这也导致了反编译代码的歧义。 C/C++ 程序通常也可以做到这一点。

    编辑:另外,一些 CPU 对同一条指令有多种不同的编码。例如,x86 CPU 有 2 条指令 mov reg, reg/memmov reg/mem, reg。这些使您可以在寄存器和内存位置(在任一方向)之间以及两个寄存器之间移动数据。这两条指令都可用于在两个寄存器之间传输数据,但它们具有不同的编码。如果程序以某种方式依赖于特定的编码(例如,为了通过校验和验证其完整性),那么从像 mov eax, ebx 这样的反汇编中,您将无法分辨它最初是两条 mov 指令中的哪一条,并且所以如果你试图重新组装反汇编,你可能会破坏程序。

    您可以使用调试器来调试带有或不带有调试/符号信息的程序。这些信息只会使人类更容易浏览代码和数据,因为许多(但不一定是全部)例程和变量可以使用它们的名称和类型来识别和显示,而不仅仅是原始地址和原始无类型数据。

    我猜各种字节码不那么模棱两可,而且它们的功能受到更多限制,这使得反编译它们变得更容易。

    【讨论】:

    • 嗨,Alexey,感谢您的回答 :) 我有点沿着这些思路思考,字节码在它们自己的虚拟机上运行,​​而不是在实际的 CPU 上运行,因此它们相当受限制且明确。
    • 虽然你说的是真的,但我怀疑任何好的现代编译器都会做前三段中概述的任何事情。当然,这并不是说它们不再有可能(如果不太可能的话)。
    • @NPE 编译器可能不会这样做,但是用它编译的程序可以,许多现实世界的程序都可以。
    • 正确反汇编的困难是与反编译问题完全不同(尽管有些相关)的问题。即使有完美的反汇编反编译仍然很难。
    • @IgorSkochinsky 当你处理机器代码时,正确的反汇编是反编译的基础,而不是一个选项,不是 10 中的第 5 步或类似的东西。我不同意你对这个问题的处理。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-08
    • 1970-01-01
    • 2016-08-02
    • 1970-01-01
    • 2017-12-24
    • 1970-01-01
    相关资源
    最近更新 更多