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文件进行加密
    • 提到破解难度
  • 代码混淆
    • 将代码转换成功能上等价,但是难于阅读和理解的形式

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • java防反编译最简单的技巧分享

    前言 本文主要跟大家介绍了关于java防反编译的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 示例代码: 源码使用try-catch代码包装下,如下: public class CompileForbid { public static void main(String[] args) { try { System.out.println("-------"); //防止反编译的处理 if (654789 == new Random().nextInt()

  • android apk反编译到java源码的实现方法

    Android由于其代码是放在dalvik虚拟机上的托管代码,所以能够很容易的将其反编译为我们可以识别的代码. 之前我写过一篇文章反编译Android的apk包到smali文件 然后再重新编译签名后打包实现篡改apk的功能. 最近又有一种新的方法来实现直接从Android apk包里的classes.dex文件,把dex码反编译到java的.class二进制码,然后从.class二进制码反编译到java源码想必就不用我来多说了吧. 首先我们需要的工具是dex2jar和jd-gui 其中第一个工具

  • 分享7款开源Java反编译工具

    今天我们要来分享一些关于Java的反编译工具,反编译听起来是一个非常高上大的技术词汇,通俗的说,反编译是一个对目标可执行程序进行逆向分析,从而得到原始代码的过程.尤其是像.NET.Java这样的运行在虚拟机上的编程语言,更容易进行反编译得到源代码.当然,也有一些商业软件,对其程序进行了混淆加密,这样我们就很难用工具反编译了. 1.Java反编译插件 -- Jadclipse JadClipse是Jad的Eclipse插件,是一款非常实用而且方便地Java反编译插件,我们只需将下载的插件包复制到e

  • Java代码编译和反编译的那些事儿

    编程语言 在介绍编译和反编译之前,我们先来简单介绍下编程语言(Programming Language).编程语言(Programming Language)分为低级语言(Low-level Language)和高级语言(High-level Language). 机器语言(Machine Language)和汇编语言(Assembly Language)属于低级语言,直接用计算机指令编写程序. 而C.C++.Java.Python等属于高级语言,用语句(Statement)编写程序,语句是计算

  • Android反编译代码和防止反编译

    一.反编译apk文件 安装ApkTool工具,该工具可以解码得到资源文件,但不能得到Java源文件.          安装环境:需要安装JRE1.6 1> 到http://code.google.com/p/android-apktool/ 下载apktool1.3.2.tar.bz2 和apktool-install-windows-2.2_r01-3.tar.bz2 文件.      解压两个文件,然后把解压后的文件放在一起,如:c:\apktool 2> 在系统变量PATH中添加进aa

  • 使用SNK密钥文件保护你的DLL和代码不被反编译教程

    大家做项目开发一般都是分层的,比如UI层,业务层,数据访问层.业务层引用数据访问层的DLL(比如dataAccess.dll),并使用dataAccess.dll中的方法.当项目完成并给客户用了,可有些心里BT的客户这个时候也可以请个稍微懂NET的人来引用你的dataAccess.dll并调用其中的方法搞破坏.比如可以直接使用里面的ChangePwd(string UserName,string Pwd)方法把其他用户的密码改了,这个时候就你就....... 好了,该开始说怎么保护我们的代码了:

  • 破解.net程序(dll文件)编译和反编译方法

    我只做个简单的小例子,给大家一个思路,吼吼~~~~ 1使用工具 Reflector.exe 用来查看.net代码 这个就不用多说了它是学.net必备神器 Ildasm.exe:用来将dll,exe编译为IL文件 ilasm.exe:用来将IL文件编译回来dll或exe EmEditor文本处理工具这个大家如果没有我这有下载里面有注册码EmEditor.rar 2Ildasm和ilasm基本知识: ilasm.exe与ildasm.exe的使用 首先打开cmd命令.cd 到 C:\Program

  • 详解Android的反编译和代码混淆

    前言 包括以下内容 要反编译apk需要下面3个工具 反编译资源文件 反编译类文件 代码混淆 要反编译apk需要下面3个工具 1.apktool(资源文件获取) 作用:资源文件获取,可以提取图片文件和布局文件进行使用查看 2.dex2jar(源文件获取) 作用:将APK反编译成java源码(classes.dex转化成jar文件) 3.jd-gui 作用:查看APK中classes.dex转化成的jar文件,即源码文件 下面进行反编译资源文件和类文件: 反编译资源文件 资源文件:包括图片资源.布局

  • Android反编译看看手Q口令红包的实现原理

    首篇作为开始,先讲讲简单的反编译.反编译通常有几种目的:互相学习.借来用用.嘿嘿(干你,又分为小干干类似微信红包,和大干干改别人的apk帮他上架). 因为没带kvm回来,mbpr屏幕太小,所以下文环境为windows. 一.反编译 让我们从实战开始,先实践一下怎么去反编译一个apk,看看某些功能的实现.毕竟没有实践的原理都是耍流氓. 这里我们保留互相学习的心态,所以是友善的第一种目的,嘻嘻. 1.准备 工具 Apktool jadx(新一代反编译大杀器) 安装包 手机QQ 6.2.3 (目标就设

  • 浅谈Android应用安全防护和逆向分析之apk反编译

    概述 这里是Mac环境,如果是window环境的同学,在环境搭建和工具上可以选择Window环境的.先看看需要到的工具: 1.apktool:https://ibotpeaches.github.io/Apktool/install/ 2.dex2jar:https://github.com/pxb1988/dex2jar 3.jd-gui:http://jd.benow.ca 注意:工具一定要是当前最新版本的,否则很容易出现一些莫名其妙的错误. 先看一下项目的包结构 然后在简单看MainAct

  • 使用jar包反编译形成pom工程

    目录 1. 反编译jar得到源码 1.1 使用jd-gui工具 1.2 点击file - > open file 选中需要反编译的jar 1.3 保存反编译的源代码 2. ideal新建pom工程 2.1 file ->new ->project 2.2 GroupId 和 ArtifactId 可以自定义填写 2.3 项目名称和项目文件保存路径会自动填入 2.4 新建好的项目结构如下 3. 复制jar源码到pom工程 3.1 解压上面1.3那个步骤输出的zip 3.2 补充pom文件里

随机推荐