1. 程式人生 > >class 文件反編譯器的 java 實現

class 文件反編譯器的 java 實現

ret 操作 gpo 調用 部分 face 來看 然而 ati

  最近由於公司項目需要,了解了很多關於類加載方面的知識,給項目帶來了一些熱部署方面的突破。 由於最近手頭工作不太忙,同時驅於對更底層知識的好奇與渴求,因此決定學習了一下 class 文件結構,並通過一周的不懈努力,已經掌握了class 的文件結構,並用 java 實現了一個簡單的反編譯器:讀取 class 文件,反編譯成純 java 代碼。下面來看一下具體的實現思路和代碼分析。

  1. class 文件是一種平臺無關性的二進制文件,通過 IO 流可以讀取成byte[],將字節數組轉換為十六進制(字符串)之後,class 的數據結構便一目了然了,對 class 文件的解析即變成了對整個十六進制串的分割、解析

  2. 那麽如何分割呢?事實上,class 的文件采用一種“偽結構體”的形式來存儲數據,這種“偽結構體”只有兩種數據類型:無符號數和表(表中的數據也都是無符號數)。 表的概念我們都知道,那什麽是無符號數呢?我們都知道,在計算機中最基本數據單位是字節,1字節(byte)= 8位(bit),也就是8個長度的二進制,而4個長度的二進制可以代表1個長度的十六進制,因此,兩個十六進制代表一個字節,用無符號數標識即 :

      • u1代表一個字節,代表2長度的十六進制(如0x01);
      • u2代表兩個字節,代表4長度的十六進制(如0x0001);
      • u4代表4個字節,代表8長度的十六進制(如0x00000001)

  3. 整個 class 文件就是一張表,表中的字段有:魔數、虛擬機的次版本、主版本、常量池的大小、常量池、訪問標識、當前類、父類、實現的接口數量、接口集合、字段表數量、字段表集合、方法表數量、方法表集合、屬性表數量、屬性表集合。 其中,魔數、主次版本、常量池大小、訪問標識、當前類、父類、表集合數量等都是無符號數。 常量池、字段表集合、方法表集合、屬性表集合等都是表結構,有的表結構中的字段又嵌套了其他的表結構。 具體的無符號數大小和表結構在此不進行展開贅述,用一句話來說:class 文件的數據結構是一種表結構的嵌套

  4. 上邊對 class 文件的數據結構進行了簡略的介紹,現在我們開始討論如何解析並存儲 class 文件。 我們可以按照class 文件中的各種表結構,建立相應的 Bean,例如 對於整個 class 文件,即class_info,我們可以建立如下的 bean:

public class Class_info {
    private String magic;  //魔數
    private String minor_version;  //虛擬機次版本
    private String major_version;  //虛擬機主版本
    private int cp_count;  //
常量池大小 private Map<Integer, Constant_X_info> constant_pool_Map; //常量池 private String access_flag; //訪問標識 private int this_class_index; //當前類索引 private int super_class_index; //父類索引 private int interfaces_count; //接口數量 private List<Integer> interfacesList; //接口集合 private int fields_count; //字段表數量 private List<Fields_info> fields_info_List; //字段表集合 private int Methods_count; //方法表數量 private List<Methods_info> methods_info_List; //方法表集合 private int attributes_count; //屬性表數量 private List<Attribute_info> attributes; //屬性表集合 public String getMagic() { return magic; } public void setMagic(String magic) { this.magic = magic; }   
  ..... 省略其他 get set
}

  將所有的表結構都搭建好後,我們可以開始對 class 文件讀取到的 十六進制字符串進行切割,將切割到的數據填充到我們的 bean 中。在此,提供一種切割字符串的思路:創建一個靜態指針,指向切割字符串的 start 位置,每次切割length 長度後,對指針進行初始化,即 start = start + length。如果要進行切割數據,那麽只需要調用 cutString(int len) 就可以了。 代碼如下:

   private static int start_pointer = 0; 
    private static String hexString = ""; // 十六進制串

    private static String cutString(int len) {
        String cutStr = hexString.substring(start_pointer, start_pointer + len);
        // 初始化指針
        start_pointer = start_pointer + len;
        return cutStr;
    }

  

  5. 請註意,上述雖然說起來簡單,然而切割數據不可以弄錯任何一個字節的長度,如果弄錯任何一個字節的長度,那後邊的數據完全是錯位的,必須推倒重來! 經過一系列努力後,終於把所有的數據都進行切割並填充到了 bean 中,下面就是利用數據,拼裝 java 源代碼了。這一部分最重要的無非是方法體的拼裝,在編譯的過程中,編譯器已經將 方法體中的java 語句編譯成了字節碼指令,完全是內存的堆棧操作,跟我們之前的 java 代碼比完全變了形式和語法。那麽,如何根據字節碼指令,推導出java源代碼呢?總結所有的 java 語法,無非是:

    • new 對象
    • 方法調用(靜態方法、構造方法、成員方法、接口方法)
    • 參數傳遞
    • 計算、判斷、賦值
    • 其他的語句(if for while try等)

  我們需要對閱讀字節碼指令相當熟練,需要達到1.看著 java 代碼,推敲出編譯後的字節碼指令 2.看著字節碼,反推敲出 java 代碼。 在此基礎上,進行大量的規律總結,這也是反編譯最難、最核心的地方了。由於內容比較復雜,在此不進行贅述,可以查看筆者項目的源碼。

  6. 筆者實現的簡易反編譯器已經開源到 github: https://github.com/MalcolmFF/Decompiler ,其中最重要的兩個類為:com.xuanjie.app.App.java(main 方法所在類,解析 class 文件將數據存儲到 bean 中) 和 com.xuanjie.core.SrcCreator.java(用於 java 源代碼的拼裝)。

   歡迎讀者進行賞閱,提出建議一起維護完善。

class 文件反編譯器的 java 實現