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 args) {
List<String> strList = ImmutableList.of("Hollis", ":Hollis", "博客:www.hollischuang");
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");
strList.forEach((Consumer<String>)afactory(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);
常用的java编译器有哪些
}
CFR还有很多其他参数,均⽤于不同场景,读者可以使⽤java -jar cfr_0_125.jar --help进⾏了解。这⾥不逐⼀介绍了。
如何防⽌反编译
由于我们有⼯具可以对Class⽂件进⾏反编译,所以,对开发⼈员来说,如何保护Java程序就变成了⼀个⾮常重要的挑战。但是,魔⾼⼀尺、道⾼⼀丈。当然有对应的技术可以应对反编译咯。但是,这⾥还是要说明⼀点,和⽹络安全的防护⼀样,⽆论做出多少努⼒,其实都只是提⾼攻击者的成本⽽已。⽆法彻底防治。
典型的应对策略有以下⼏种:
隔离Java程序
让⽤户接触不到你的Class⽂件
对Class⽂件进⾏加密
提到破解难度
代码混淆
将代码转换成功能上等价,但是难于阅读和理解的形式

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。