Java代码编译和反编译的那些事儿
编程语言
在介绍编译和反编译之前,我们先来简单介绍下编程语言(programming language)。编程语言(programming language)分为低级语言(low-level language)和高级语言(high-level language)。
机器语言(machine language)和汇编语言(assembly language)属于低级语言,直接用计算机指令编写程序。
而c、c++、java、python等属于高级语言,用语句(statement)编写程序,语句是计算机指令的抽象表示。
举个例子,同样一个语句用c语言、汇编语言和机器语言分别表示如下:
计算机只能对数字做运算,符号、声音、图像在计算机内部都要用数字表示,指令也不例外,上表中的机器语言完全由十六进制数字组成。最早的程序员都是直接用机器语言编程,但是很麻烦,需要查大量的表格来确定每个数字表示什么意思,编写出来的程序很不直观,而且容易出错,于是有了汇编语言,把机器语言中一组一组的数字用助记符(mnemonic)表示,直接用这些助记符写出汇编程序,然后让汇编器(assembler)去查表把助记符替换成数字,也就把汇编语言翻译成了机器语言。
但是,汇编语言用起来同样比较复杂,后面,就衍生出了java、c、c++等高级语言。
什么是编译
上面提到语言有两种,一种低级语言,一种高级语言。可以这样简单的理解:低级语言是计算机认识的语言、高级语言是程序员认识的语言。
那么如何从高级语言转换成低级语言呢?这个过程其实就是编译。
从上面的例子还可以看出,c语言的语句和低级语言的指令之间不是简单的一一对应关系,一条a=b+1;语句要翻译成三条汇编或机器指令,这个过程称为编译(compile),由编译器(compiler)来完成,显然编译器的功能比汇编器要复杂得多。用c语言编写的程序必须经过编译转成机器指令才能被计算机执行,编译需要花一些时间,这是用高级语言编程的一个缺点,然而更多的是优点。首先,用c语言编程更容易,写出来的代码更紧凑,可读性更强,出了错也更容易改正。
将便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序的过程就是编译。负责这一过程的处理的工具叫做编译器
现在我们知道了什么是编译,也知道了什么是编译器。不同的语言都有自己的编译器,java语言中负责编译的编译器是一个命令:javac
javac是收录于jdk中的java语言编译器。该工具可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于java虚拟机的字节码。
当我们写完一个helloworld.java文件后,我们可以使用javac helloworld.java命令来生成helloworld.class文件,这个class类型的文件是jvm可以识别的文件。通常我们认为这个过程叫做java语言的编译。其实,class文件仍然不是机器能够识别的语言,因为机器只能识别机器语言,还需要jvm再将这种class文件类型字节码转换成机器可以识别的机器语言。
什么是反编译
反编译的过程与编译刚好相反,就是将已编译好的编程语言还原到未编译的状态,也就是找出程序语言的源代码。就是将机器看得懂的语言转换成程序员可以看得懂的语言。java语言中的反编译一般指将class文件转换成java文件。
有了反编译工具,我们可以做很多事情,最主要的功能就是有了反编译工具,我们就能读得懂java编译器生成的字节码。如果你想问读懂字节码有啥用,那么我可以很负责任的告诉你,好处大大的。比如我的博文几篇典型的原理性文章,都是通过反编译工具得到反编译后的代码分析得到的。如深入理解多线程(一)——synchronized的实现原理、深度分析java的枚举类型—-枚举的线程安全性及序列化问题、java中的switch对整型、字符型、字符串型的具体实现细节、java的类型擦除等。我最近在gitchat写了一篇关于java语法糖的文章,其中大部分内容都用到反编译工具来洞悉语法糖背后的原理。
java反编译工具
本文主要介绍3个java的反编译工具:javap、jad和cfr
javap
javap是jdk自带的一个工具,可以对代码反编译,也可以查看java编译器生成的字节码。javap和其他两个反编译工具最大的区别是他生成的文件并不是java文件,也不像其他两个工具生成代码那样更容易理解。拿一段简单的代码举例,如我们想分析java 7中的switch是如何支持string的,我们先有以下可以编译通过的源代码:
public class switchdemostring { public static void main(string[] args) { string str = "world"; switch (str) { case "hello": system.out.println("hello"); break; case "world": system.out.println("world"); break; default: break; } } }
执行以下两个命令:
javac switchdemostring.java javap -c switchdemostring.class
生成代码如下:
public class com.hollis.suguar.switchdemostring { public com.hollis.suguar.switchdemostring(); code: 0: aload_0 1: invokespecial #1 // method java/lang/object."<init>":()v 4: return public static void main(java.lang.string[]); code: 0: ldc #2 // string world 2: astore_1 3: aload_1 4: astore_2 5: iconst_m1 6: istore_3 7: aload_2 8: invokevirtual #3 // method java/lang/string.hashcode:()i 11: lookupswitch { // 2 99162322: 36 113318802: 50 default: 61 } 36: aload_2 37: ldc #4 // string hello 39: invokevirtual #5 // method java/lang/string.equals:(ljava/lang/object;)z 42: ifeq 61 45: iconst_0 46: istore_3 47: goto 61 50: aload_2 51: ldc #2 // string world 53: invokevirtual #5 // method java/lang/string.equals:(ljava/lang/object;)z 56: ifeq 61 59: iconst_1 60: istore_3 61: iload_3 62: lookupswitch { // 2 0: 88 1: 99 default: 110 } 88: getstatic #6 // field java/lang/system.out:ljava/io/printstream; 91: ldc #4 // string hello 93: invokevirtual #7 // method java/io/printstream.println:(ljava/lang/string;)v 96: goto 110 99: getstatic #6 // field java/lang/system.out:ljava/io/printstream; 102: ldc #2 // string world 104: invokevirtual #7 // method java/io/printstream.println:(ljava/lang/string;)v 107: goto 110 110: return }
我个人的理解,javap并没有将字节码反编译成java文件,而是生成了一种我们可以看得懂字节码。其实javap生成的文件仍然是字节码,只是程序员可以稍微看得懂一些。如果你对字节码有所掌握,还是可以看得懂以上的代码的。其实就是把string转成hashcode,然后进行比较。
个人认为,一般情况下我们会用到javap命令的时候不多,一般只有在真的需要看字节码的时候才会用到。但是字节码中间暴露的东西是最全的,你肯定有机会用到,比如我在分析synchronized的原理的时候就有是用到javap。通过javap生成的字节码,我发现synchronized底层依赖了acc_synchronized标记和monitorenter、monitorexit两个指令来实现同步。
jad
jad是一个比较不错的反编译工具,只要下载一个执行工具,就可以实现对class文件的反编译了。还是上面的源代码,使用jad反编译后内容如下:
命令:jad switchdemostring.class
public class switchdemostring { public switchdemostring() { } public static void main(string args[]) { string str = "world"; string s; switch((s = str).hashcode()) { default: break; case 99162322: if(s.equals("hello")) system.out.println("hello"); break; case 113318802: if(s.equals("world")) system.out.println("world"); break; } } }
看,这个代码你肯定看的懂,因为这不就是标准的java的源代码么。这个就很清楚的可以看到原来字符串的switch是通过equals()和hashcode()方法来实现的。
但是,jad已经很久不更新了,在对java7生成的字节码进行反编译时,偶尔会出现不支持的问题,在对java 8的lambda表达式反编译时就彻底失败。
cfr
jad很好用,但是无奈的是很久没更新了,所以只能用一款新的工具替代他,cfr是一个不错的选择,相比jad来说,他的语法可能会稍微复杂一些,但是好在他可以work。
如,我们使用cfr对刚刚的代码进行反编译。执行一下命令:
java -jar cfr_0_125.jar switchdemostring.class --decodestringswitch false
得到以下代码:
public class switchdemostring { public static void main(string[] arrstring) { string string; string string2 = string = "world"; int n = -1; switch (string2.hashcode()) { case 99162322: { if (!string2.equals("hello")) break; n = 0; break; } case 113318802: { if (!string2.equals("world")) break; n = 1; } } switch (n) { case 0: { system.out.println("hello"); break; } case 1: { system.out.println("world"); break; } } } }
通过这段代码也能得到字符串的switch是通过equals()和hashcode()方法来实现的结论。
相比jad来说,cfr有很多参数,还是刚刚的代码,如果我们使用以下命令,输出结果就会不同:
java -jar cfr_0_125.jar switchdemostring.class public class switchdemostring { public static void main(string[] arrstring) { string string; switch (string = "world") { case "hello": { system.out.println("hello"); break; } case "world": { system.out.println("world"); break; } } } }
所以--decodestringswitch表示对于switch支持string的细节进行解码。类似的还有--decodeenumswitch、--decodefinally、--decodelambdas等。在我的关于语法糖的文章中,我使用--decodelambdas对lambda表达式警进行了反编译。 源码:
public static void main(string... args) { list<string> strlist = immutablelist.of("hollis", "公众号:hollis", "博客:www.hollischuang.com"); strlist.foreach( s -> { system.out.println(s); } ); }
java -jar cfr_0_125.jar lambdademo.class --decodelambdas false反编译后代码:
public static /* varargs */ void main(string ... args) { immutablelist strlist = immutablelist.of((object)"hollis", (object)"\u516c\u4f17\u53f7\uff1ahollis", (object)"\u535a\u5ba2\uff1awww.hollischuang.com"); strlist.foreach((consumer<string>)lambdametafactory.metafactory(null, null, null, (ljava/lang/object;)v, lambda$main$0(java.lang.string ), (ljava/lang/string;)v)()); } private static /* synthetic */ void lambda$main$0(string s) { system.out.println(s); }
cfr还有很多其他参数,均用于不同场景,读者可以使用java -jar cfr_0_125.jar --help进行了解。这里不逐一介绍了。
如何防止反编译
由于我们有工具可以对class文件进行反编译,所以,对开发人员来说,如何保护java程序就变成了一个非常重要的挑战。但是,魔高一尺、道高一丈。当然有对应的技术可以应对反编译咯。但是,这里还是要说明一点,和网络安全的防护一样,无论做出多少努力,其实都只是提高攻击者的成本而已。无法彻底防治。
典型的应对策略有以下几种:
- 隔离java程序
- 让用户接触不到你的class文件
- 对class文件进行加密
- 提到破解难度
- 代码混淆
- 将代码转换成功能上等价,但是难于阅读和理解的形式
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。