来源:
要理解这个问题,先要看「正」编译的过程是怎样的。
你有一个想法,这是一种人类自然语言可以表达出来的东西。你利用编程技能,把它「翻译」成你熟悉的一种编程语言:
这个过程叫做编程。
然后你使用编译器(compiler)将它翻译成机器所能理解的语言:
这个过程叫做编译。编程和编译都是「信息丢失」的过程。比如你说,我有一组整数,我要把这些数排个序,然后轻车熟路地写了个冒泡排序。然而一定程度上,你的原始动机其实已经从代码里丢失了——有经验的人可以一眼看出这段代码是在排序,而新手小明看到的只有一些 for 和 if 之类的东西。如果是更复杂的功能,可能过一段时间你自己都看不懂自己当时是想干什么。从程序语言到机器语言的过程其实也是一样的。这两个过程其实都是把「做什么」转换成「怎么做」的过程,转换完成之后,究竟一开始是要做什么,这个信息已经丢失了。
所谓「反编译」,其实就是找回这些丢失的信息的过程。从这个角度上来说,你阅读一段代码的过程,其实就是在将它「反编译」成自然语言。如果要完美地反编译,那只存在一种可能,就是信息完全没有丢失——比如说你阅读的这段代码有充分的注释,或者它使用了一种你所知晓的模式(这也是为什么大家一再强调注释和设计模式的重要性)。对于从机器语言到程序语言的反编译过程,也是一样。
比如说有比反编译更低级(非贬义)的过程,叫做反汇编:
严格来说汇编语言也是一种编程语言,不过我们在这里把它和我们常说的高级编程语言(包括C语言)区分开。
这个步骤里,我在汇编和机器语言里使用的是双向实线箭头,因为它们是可以互相转换的。从汇编语言到机器语言的过程中没有丢失任何信息——因为两者的指令是一一对应的,因此反汇编可以轻松达成。
这就是很多程序语言只能反汇编、不能(难以,下同)反编译的原因。一般我们管这种语言叫「编译语言」,又称「原生语言」。代表有C、C++等。
那为什么有的语言可以反编译呢?这又要从机器语言说起。就像不同地域的人所用的语言不同一样,不同的机器说的语言也不尽相同。用行话说,叫「指令集不同」。比方说,你的电脑和你的手机,指令集通常是不一样的。一段程序要让不同的机器都能执行,只能分别翻译(编译)成相应的机器语言。这个过程太麻烦了,于是人们想了个办法,搞出了一种叫解释语言的东西(此处未考证解释语言是否就是因此发明的,只是帮助理解)。如下图:
解释语言有两种执行方式,这取决于执行端的「解释器」是如何工作的。一种是直接解释执行,中间就没有机器语言什么事情了,但这种方式效率很低。因此现代的解释语言基本上都会采用第二种方式,也就是经由图中上部的路径,先通过JIT编译的方式翻译成机器语言,然后再执行,保证执行效率。JIT编译大致可以理解为「用到什么就编译什么」,这个过程常常是在执行过程中同步进行的。
「解释器」的英文interpreter,其实就是名词「翻译」的意思。这好比你国外交部发了封文件到各国大使馆,再由大使馆的工作人员分别翻译成相应的语言,传达给目标国相关部门。代表性的解释语言如Javascript,它要在不同机器的浏览器上都能正确执行,所以采用这种方式。但是这样一来,程序代码就必须提供给每一台执行端机器了。这可是泄密啊。对于防止泄密,最直接的方式自然是加密。
有锁就有钥匙,同时也有开锁术;有加密解密,也有相应的破解方式。这时候所谓的「反编译」,其实就是破解加密算法。这一点就不展开聊了。
后来,人们觉得解释语言执行得实在有点慢,于是又想了一个办法:把一些可以前期做掉的工作先做掉,只留着那些跟目标机器有关的工作,到时候再说。于是程序被处理成了一种叫做「中间语言」,或者叫「字节码」的东西:
这个过程一般也叫做编译。中间语言词汇少,比较精炼,执行起来也更快。这些语言一般也会用上JIT技术,进一步把中间语言编译成机器语言(而非解释执行),执行效率也就跟那些原生的编译语言不相上下了。这种语言代表性的有C#、Java等。
程序语言可以编译成中间语言,反过来,中间语言也可以在一定程度上反编译成程序语言。这是因为采用这种编译方式的编程语言为了保证它们的高级特性(比如说反射),在编译的过程中保留了源程序的绝大部分信息,只有很少的信息丢失;也正是因为丢失了这一部分信息,中间语言通常不能完美地反编译——最常见的就是反编译出来的程序中局部变量的名字都丢了,被替换成了由反编译器自动生成的名字。但这样反编译出来的程序,结构和功能都是完备的,可读性也有一定的保障。一般来说,我们所说的可以反编译的程序都是指这样一类语言写就的程序。
中间语言可以被反编译;加密又会被破解,而且执行前还要解密,会带来额外的性能开销。有没有办法能让代码既能有效执行,又不被截获代码的人所利用呢?这时候人们从一些职业素养很差的程序员那里得到了启发。
实现一个相同的功能,可以有无数种形式的代码。你恪尽职守,认认真真地写注释,准确地命名函数和变量, 严格按照规范进行缩进和换行;小明却相反,完全没有注释,变量全部用abcd乃至故意误导别人(var mySon = laowang.Son),缩进换行邋遢,尤其是在大括号前不换行,让大家很不满。于是老板想,我们先把小明开除掉,然后给你发奖金并要求大家按照你的方式写代码,并且开发一个工具,唤作「混淆器」,在发布时再把代码处理成小明写的那种样子:
这样代码即使被反编译和解密了,别人看也看不懂,不小心还会被带到坑里去。代码毕竟是写给人看的,只是偶尔让机器跑一跑,所以没有可读性的代码是没有价值的。这种方法一出,广受好评,于是变成了一种非常普遍的做法。注意图中省略了中间代码和JIT的步骤,混淆通常会跟这些技术一起使用。
--------------------------------------------------------------------------
附:代码混淆-维基百科
代码混淆(Obfuscation)是将的代码,转换成一种功能上等价,但是难于阅读和理解的形式的行为。
代码混淆可以用于程序,也可以用于程序编译而成的中间代码。执行代码混淆的程序被称作代码混淆器。当前已经存在许多种功能各异的代码混淆器。
其主要工作有:
- 将代码中的各种元素,如变量,函数,类的名字改写成无意义的名字。比如改写成单个字母,或是简短的无意义字母组合,甚至改写成“__”这样的符号,使得阅读的人无法根据名字猜测其用途。
- 重写代码中的部分逻辑,将其变成功能上等价,但是更难理解的形式。比如将for循环改写成while循环,将循环改写成递归,精简中间变量,等等。
- 打乱代码的格式。比如删除空格,将多行代码挤到一行中,或者将一行代码断成多行等等。
- 添加花指令,通过特殊构造的指令来使得反汇编器出错,进而干扰反编译工作的进行。
代码混淆器也会带来一些问题。主要的问题包括:
- 被混淆的代码难于理解,因此也变得困难起来。开发人员通常需要保留原始的未混淆的代码用于调试。
- 对于支持的语言,代码混淆有可能与反射发生冲突。
- 代码混淆并不能真正阻止反向工程,只能增大其难度。因此,对于对安全性要求很高的场合,仅仅使用代码混淆并不能保证源代码的安全。