使用调试信息反编译C代码?

3

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

我找不到令人信服的答案,为什么-g选项中的信息对于反编译来说是不足够的,但对于调试却足够了呢? Python/Java字节码中包含的额外信息是什么,使得反编译变得容易?


答案最适合于Stack Exchange网络的Area51反向工程。 - Koushik Shetty
1
@Koushik:那个网站才五天,看起来是私人测试版。 - NPE
@NPE 哦,完全忘记了...希望公共测试版很快开始。 - Koushik Shetty
http://en.wikipedia.org/wiki/Interactive_Disassembler - ta.speot.is
3个回答

9
以下是原因:
  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++中,关系是多对多的,在源前端存在大量重叠(如头文件)。

1
我只想补充一点,-g选项只会给你标签名称,而没有它,二进制文件只包含绝对地址和偏移量。这有助于阅读反编译的汇编代码,但永远无法将其还原为C语言。 - c.fogelklou
@c.fogelklou: 什么是“反编译”汇编代码?难道它就是C语言吗? - Saswat Padhi
我的意思是将一个二进制文件转换为文本...反编译可能不是正确的术语,但最终结果是将汇编代码包含在可读的文本格式中。 - c.fogelklou
@SaswatPadhi所谓“反编译”汇编代码未必是C语言的,也可为C++、D、B、Fortran等。即它取决于具体情况。 - Koushik Shetty
@Koushik:是的,它可以反编译成任何语言。但我只是坚持上下文。 - Saswat Padhi
显示剩余2条评论

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的答案之外,还要检查 这个


0

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

在某些处理器和操作系统上,可以将数据执行为代码,并将代码用作数据。这使得明确区分两者变得困难。而且,这正是C/C++程序经常可以轻松完成的。

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

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

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

我猜各种字节码更少歧义且更受限制,这就使得反编译更容易。


嗨,Alexey,感谢你的回答 :) 我的想法和你有些相似,字节码在它们自己的虚拟机上运行而不是实际的CPU上,因此它们相当受限制且明确无歧。 - Saswat Padhi
虽然你说的是对的,但我怀疑任何好的现代编译器都不会执行第一、二、三段中概述的任何操作。当然,这并不意味着它们不可能发生(尽管可能性较小)。 - NPE
@IgorSkochinsky 完全不同?怎么会呢?理论上,你可以在模棱两可的情况下提供反汇编作为注释,并将原始代码保留为字节序列。 - Alexey Frunze
@AlexeyFrunze:查看这个答案,了解反编译所需的一些常见步骤。只有第一个步骤与您所描述的有些相关。 - Igor Skochinsky
1
@IgorSkochinsky 当你处理机器码时,正确的反汇编是反编译的基础,而不是选项,也不是10个步骤中的第5步或其他任何东西。我不同意你对这个问题的处理方式。 - Alexey Frunze
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接