跳转到内容

软件工程/工具/反编译器简介

来自维基教科书,开放的书籍,开放的世界

反编译器是一个计算机程序,它执行与编译器相反的操作。也就是说,它将包含相对较低抽象级别信息的代码文件(通常设计为计算机可读而不是人类可读)转换为具有较高抽象级别信息的代码文件(通常设计为人类可读)。

术语反编译器通常指将可执行程序(编译器输出)转换为(相对)高级语言的源代码的程序,该源代码在编译后将生成一个与原始可执行程序行为相同的可执行文件。相比之下,反汇编器将可执行程序转换为汇编语言(汇编器可以将它重新组合成可执行程序)。

反编译是使用反编译器的行为,尽管该术语在用作名词时也可以指反编译器的输出。它可以用来恢复丢失的源代码,并且在某些情况下也对计算机安全、互操作性和错误更正很有用。[1] 反编译的成功取决于被反编译的代码中存在的信息量以及对其执行的分析的复杂程度。许多虚拟机(例如 Java 虚拟机或 .NET Framework 公共语言运行时)使用的字节码格式通常包含广泛的元数据和高级功能,使得反编译变得相当可行。调试数据的存在可以使重现原始变量和结构名称甚至行号成为可能。没有这种元数据或调试数据的机器语言更难反编译。[2]

一些编译器和编译后工具生成混淆代码(也就是说,它们试图生成难以反编译的输出)。这样做是为了更难反向工程可执行文件。

反编译器可以被认为是由一系列阶段组成的,每个阶段都贡献了整体反编译过程的特定方面。

加载器

[编辑 | 编辑源代码]

第一个反编译阶段加载并解析输入机器代码或中间语言程序的二进制文件格式。它应该能够发现关于输入程序的基本事实,例如体系结构(奔腾、PowerPC 等)和入口点。在许多情况下,它应该能够找到 C 程序的main函数的等效项,它是用户编写代码的开始。这排除了运行时初始化代码,如果可能,不应该反编译它。如果可用,符号表和调试数据也会被加载。前端可能能够识别使用的库,即使它们与代码链接在一起,这将提供库接口。如果它能够确定使用的编译器或编译器,它可能会提供有用的信息来识别代码习语。[3]

反汇编

[编辑 | 编辑源代码]

下一个逻辑阶段是将机器代码指令反汇编为机器无关的中间表示(IR)。例如,奔腾机器指令

   mov    eax, [ebx+0x04]

可以被翻译成 IR

   eax := m[ebx+4];

惯用机器代码序列是一系列代码,其组合语义不能立即从指令的个体语义中显而易见。作为反汇编阶段的一部分,或者作为后期分析的一部分,这些惯用序列需要被翻译成已知的等效 IR。例如,x86 汇编代码

   cdq    eax             ; edx is set to the sign-extension of eax
   xor    eax, edx
   sub    eax, edx

可以被翻译成

   eax := abs(eax);

一些惯用序列是机器无关的;有些只涉及一条指令。例如,xor eax, eax 清除eax寄存器(将其设置为零)。这可以用机器无关的简化规则来实现,例如a xor a = 0

通常,最好将惯用序列的检测尽可能地延迟到后面的阶段,这些阶段受指令排序的影响较小。例如,编译器的指令调度阶段可能会在惯用序列中插入其他指令,或更改序列中指令的顺序。反汇编阶段的模式匹配过程可能无法识别已更改的模式。后面的阶段将指令表达式组合成更复杂的表达式,并将它们修改成规范(标准化)形式,这使得即使已更改的习语也更有可能在反编译过程的后期阶段匹配更高级别的模式。

识别子程序调用、异常处理和 switch 语句的编译器习语尤其重要。一些语言还广泛支持字符串或长整数。

程序分析

[编辑 | 编辑源代码]

各种程序分析可以应用于 IR。特别是,表达式传播将几个指令的语义组合成更复杂的表达式。例如,

   mov   eax,[ebx+0x04]
   add   eax,[ebx+0x08]
   sub   [ebx+0x0C],eax

在表达式传播之后可能会产生以下 IR

   m[ebx+12] := m[ebx+12] - (m[ebx+4] + m[ebx+8]);

生成的表达式更像高级语言,并且还消除了机器寄存器eax的使用。后面的分析可能会消除ebx寄存器。

数据流分析

[编辑 | 编辑源代码]

必须使用数据流分析来跟踪寄存器内容被定义和使用的位置。相同的分析可以应用于用于临时变量和局部数据的内存位置。然后,可以为每个这样的连接的定义和使用集形成一个不同的名称。有可能同一个局部变量内存位置在原始程序的不同部分被用于多个变量。更糟糕的是,数据流分析可能会识别出一条路径,通过该路径,值可能在两个这样的使用之间流动,即使它在现实中实际上永远不会发生或重要。这可能会在糟糕的情况下导致需要将内存位置定义为类型的并集。反编译器可以允许用户显式地打破这种不自然的依赖关系,这将导致更清晰的代码。当然,这意味着一个变量可能在没有初始化的情况下被使用,因此表明原始程序中存在问题。

类型分析

[编辑 | 编辑源代码]

一个好的机器码反编译器会执行类型分析。在这里,寄存器或内存位置的使用方式会对该位置的可能类型产生约束。例如,一个and指令意味着操作数是一个整数;程序不会对浮点数(特殊库代码除外)或指针执行此类操作。一个add指令会产生三个约束,因为操作数可以是两个整数,或者一个整数和一个指针(分别具有整数和指针结果;第三个约束来自当类型不同时两个操作数的排序)。[4]

可以识别各种高级表达式,这些表达式会触发结构或数组的识别。然而,由于机器码甚至一些高级语言(如 C)允许使用强制转换和指针运算,因此很难区分许多可能性。

上一节中的示例可能导致以下高级代码

struct T1 *ebx;
   struct T1 {
       int v0004; 
       int v0008;
       int v000C;
   };
 ebx->v000C -= ebx->v0004 + ebx->v0008;

结构化

[编辑 | 编辑源代码]

倒数第二阶段的反编译涉及将 IR 结构化为更高级别的结构,例如while循环和if/then/else条件语句。例如,机器码

   xor eax, eax
l0002:
   or  ebx, ebx
   jge l0003
   add eax,[ebx]
   mov ebx,[ebx+0x4]
   jmp l0002    
l0003:
   mov [0x10040000],eax

可以翻译成

   eax = 0;
   while (ebx < 0) {
       eax += ebx->v0000;
       ebx = ebx->v0004;
   }
   v10040000 = eax;

无结构代码比已结构化代码更难翻译成结构化代码。解决方案包括复制一些代码或添加布尔变量。[5]

代码生成

[编辑 | 编辑源代码]

最后阶段是在反编译器后端生成高级代码。正如编译器可能有多个后端用于为不同的体系结构生成机器码一样,反编译器也可能有多个后端用于以不同的高级语言生成高级代码。

在代码生成之前,可能需要允许以交互方式编辑 IR,例如使用某种形式的图形用户界面。这将允许用户输入注释以及非通用变量和函数名称。但是,这些几乎可以像在反编译后的编辑中一样容易地输入。用户可能希望更改结构方面,例如将while循环转换为for循环。使用简单的文本编辑器不太容易修改这些内容,尽管源代码重构工具可以帮助完成此过程。用户可能需要输入在类型分析阶段未识别的信息,例如将内存表达式修改为数组或结构表达式。最后,可能需要更正不正确的 IR 或进行更改以使输出代码更易读。

合法性

[编辑 | 编辑源代码]

大多数计算机程序受版权法保护。尽管版权保护的具体范围因地区而异,但版权法通常赋予作者(程序员或雇主)对程序的一系列专有权利。[6] 这些权利包括复制的权利,包括复制到计算机的 RAM 中的副本[需要引用]。由于反编译过程涉及制作多个此类副本,因此在未经版权持有者授权的情况下,通常禁止进行反编译。但是,由于反编译通常是实现软件互操作性的必要步骤,因此美国和欧洲的版权法在一定程度上允许反编译。

在美国,版权合理使用抗辩在反编译案件中已成功使用。例如,在Sega v. Accolade中,法院裁定 Accolade 可以合法地进行反编译以绕过 Sega 游戏机使用的软件锁定机制。[7]

在欧洲,1991 年软件指令明确规定了为了实现互操作性而进行反编译的权利。在软件保护主义者与学者和独立软件开发人员之间进行的激烈辩论的结果中,第 6 条仅在满足一定条件的情况下才允许反编译

  • 首先,个人或实体必须有权使用要反编译的程序。
  • 其次,反编译必须是实现与目标程序或其他程序互操作性的必要条件。因此,互操作性信息不应轻易获得,例如通过手册或 API 文档。这是一个重要的限制。反编译器必须证明必要性。这一重要限制的主要目的是鼓励开发人员记录和公开其产品互操作性信息。[8]
  • 第三,反编译过程必须尽可能地限制在与互操作性相关的目标程序的部分。由于反编译的目的之一是了解程序结构,因此第三个限制可能难以满足。同样,证明责任在于反编译器。

此外,第 6 条规定,通过反编译获得的信息不得用于其他目的,也不得提供给其他人。

总的来说,第 6 条提供的反编译权将软件行业中据称的惯例法典化。很少有欧洲诉讼是从反编译权中产生的。这可以解释为以下两种情况之一:1)反编译权使用不频繁,因此反编译权可能没有必要,或者 2)反编译权运作良好,并提供足够的法律确定性,不会引起法律纠纷。在关于欧洲成员国实施软件指令的最新报告中,欧盟委员会似乎支持第二种解释。

参考文献

[编辑 | 编辑源代码]
  1. "Why Decompilation". Program-transformation.org. 2005-04-11. Retrieved 2010-09-15.
  2. Miecznikowski, Jerome (2002). "Decompiling Java Bytecode: Problems, Traps and Pitfalls". In R Nigel Horspool (ed.). Compiler Construction: 11th International Conference, proceedings / CC 2002. Springer-Verlag. pp. 111–127. ISBN 3-540-43369-4. {{cite book}}: |first2= missing |last2= (help); Unknown parameter |loast2= ignored (help)
  3. Cifuentes, Cristina; Gough, K. John (1995). "二进制程序的反编译". 软件实践与经验. 25 (7): 811–829. {{cite journal}}: 未知参数 |month= 被忽略 (帮助)
  4. Mycroft, Alan (1999). "基于类型的反编译". 在 S. Doaitse Swierstra (编). 编程语言和系统:第八届欧洲编程语言和系统研讨会. Springer-Verlag. pp. 208–223. ISBN 3-540-65699-5.
  5. C. Cifuentes. 反编译技术. 博士论文,昆士兰科技大学,1994.(可作为 压缩的 Postscript 第 6 章 获取)
  6. Rowland, Diane (2005). 信息技术法 (第 3 版). Cavendish. ISBN 1-85941-756-6.
  7. "反编译的合法性". Program-transformation.org. 2004-12-03. 检索于 2010-09-15.
  8. B. Czarnota 和 R.J. Hart,欧洲计算机程序的法律保护:EC 指令指南. 1991,伦敦:Butterworths。
[编辑 | 编辑源代码]
华夏公益教科书