class 文件反編譯器的 java 實現


  最近由於公司項目需要,了解了很多關於類加載方面的知識,給項目帶來了一些熱部署方面的突破。 由於最近手頭工作不太忙,同時驅於對更底層知識的好奇與渴求,因此決定學習了一下 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 源代碼的拼裝)。

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM