Java代碼的編譯與反編譯那些事兒

GitHub 2.5k Star 的Java工程師成神之路 ,不來了解一下嗎?java

GitHub 2.5k Star 的Java工程師成神之路 ,真的不來了解一下嗎?git

GitHub 2.5k Star 的Java工程師成神之路 ,真的肯定不來了解一下嗎?程序員

編程語言

在介紹編譯和反編譯以前,咱們先來簡單介紹下編程語言(Programming Language)。編程語言(Programming Language)分爲低級語言(Low-level Language)和高級語言(High-level Language)。github

機器語言(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的反編譯工具:javapjadcfr

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標記和monitorentermonitorexit兩個指令來實現同步。

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文件進行加密
    • 提到破解難度
  • 代碼混淆
    • 將代碼轉換成功能上等價,可是難於閱讀和理解的形式

相關文章
相關標籤/搜索