1. 程式人生 > >從一個基本的類的反編譯說起讓你讀懂Java位元組碼檔案

從一個基本的類的反編譯說起讓你讀懂Java位元組碼檔案

從一個基本的類的反編譯說起:

javap是一個能夠將class檔案反彙編成人類可讀的格式的工具。可以方便的查閱Java的位元組碼。
例如下面的例子:

public class Coo{

    private int tryBlock;

    private int catchBlock;

    private int finallyBlock;

    private int methodExit;

    public void test(){

        try{
            tryBlock = 0;
        }catch(Exception e){
            catchBlock = 1;
        }finally{
            finallyBlock = 2;
        }

        methodExit = 3;
    }

}

使用以下兩條命令:

javac Coo.java

javap -p -v Coo

得到:

Classfile /C:/Users/dell/Desktop/bob/Coo.class
  Last modified ×××; size 540 bytes
  MD5 checksum d94c55f366ae593c8bb83fceda66f50a
  Compiled from "Coo.java"
public class Coo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #7.#25         // Coo.tryBlock:I
   #3 = Fieldref           #7.#26         // Coo.finallyBlock:I
   #4 = Class              #27            // java/lang/Exception
   #5 = Fieldref           #7.#28         // Coo.catchBlock:I
   #6 = Fieldref           #7.#29         // Coo.methodExit:I
   #7 = Class              #30            // Coo
   #8 = Class              #31            // java/lang/Object
   #9 = Utf8               tryBlock
  #10 = Utf8               I
  #11 = Utf8               catchBlock
  #12 = Utf8               finallyBlock
  #13 = Utf8               methodExit
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               test
  #19 = Utf8               StackMapTable
  #20 = Class              #27            // java/lang/Exception
  #21 = Class              #32            // java/lang/Throwable
  #22 = Utf8               SourceFile
  #23 = Utf8               Coo.java
  #24 = NameAndType        #14:#15        // "<init>":()V
  #25 = NameAndType        #9:#10         // tryBlock:I
  #26 = NameAndType        #12:#10        // finallyBlock:I
  #27 = Utf8               java/lang/Exception
  #28 = NameAndType        #11:#10        // catchBlock:I
  #29 = NameAndType        #13:#10        // methodExit:I
  #30 = Utf8               Coo
  #31 = Utf8               java/lang/Object
  #32 = Utf8               java/lang/Throwable
{
  private int tryBlock;
    descriptor: I
    flags: ACC_PRIVATE

  private int catchBlock;
    descriptor: I
    flags: ACC_PRIVATE

  private int finallyBlock;
    descriptor: I
    flags: ACC_PRIVATE

  private int methodExit;
    descriptor: I
    flags: ACC_PRIVATE

  public Coo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: iconst_0
         2: putfield      #2                  // Field tryBlock:I
         5: aload_0
         6: iconst_2
         7: putfield      #3                  // Field finallyBlock:I
        10: goto          35
        13: astore_1
        14: aload_0
        15: iconst_1
        16: putfield      #5                  // Field catchBlock:I
        19: aload_0
        20: iconst_2
        21: putfield      #3                  // Field finallyBlock:I
        24: goto          35
        27: astore_2
        28: aload_0
        29: iconst_2
        30: putfield      #3                  // Field finallyBlock:I
        33: aload_2
        34: athrow
        35: aload_0
        36: iconst_3
        37: putfield      #6                  // Field methodExit:I
        40: return
      Exception table:
         from    to  target type
             0     5    13   Class java/lang/Exception
             0     5    27   any
            13    19    27   any
      LineNumberTable:
        line 14: 0
        line 18: 5
        line 19: 10
        line 15: 13
        line 16: 14
        line 18: 19
        line 19: 24
        line 18: 27
        line 21: 35
        line 22: 40
      StackMapTable: number_of_entries = 3
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 7 /* same */
}
SourceFile: "Coo.java"

預設情況下反編譯會列印所有的非私有的欄位和方法,當加入的-p引數,會打印出私有的欄位和方法。加入的-v引數,目的是為了儘可能的列印所有的資訊。如果你只需要查閱某個類中方法對應的位元組碼可以使用-c而不使用-v引數。

-v引數主要輸入分為以下幾點:

1. 基本的資訊:
1)包括原class檔案的相關資訊

2)class檔案的版本號(minor version:0, major version:52)-> 指的是編譯生成該 class 檔案時所用的JRE版本

3) 該類的訪問許可權(flag:(0X0021) ACC_PUBLIC,ACC_SUPER)

4)該類(this_class:#7)以及名字

5)父類(super_class:#8)以及名字

6)所實現的介面(interfaces:0)

7)欄位(feilds:4)

8)方法(methods:2)

9)屬性(attributes:1)的數目 -> 該屬性指class檔案所攜帶的輔助的資訊,比如該class檔案的原始檔的名稱。本資訊經常用於JVM的驗證和執行,以及程式的除錯。

注意:從JdK9之後從4)到9)中不在含有這些資訊,如果該類實現一個介面,那麼會顯式的宣告在前面
這樣做的目的為了減少JVM的驗證工作,提高JVM載入class檔案時的效率。

Classfile /C:/Users/dell/Desktop/bob/Coo.class
  Last modified ×××; size 568 bytes
  MD5 checksum 156df5eb19ac7b2f6afb29df6cc3376b
  Compiled from "Coo.java"
public class Coo implements java.io.Serializable
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER

此時的本地的環境是java versionn10.0.1
重現執行

對應的2)中的minor_version 和major_version項的值是此class檔案的次要版本號和主要版本號。主要版本號和次要版本號一起確定class檔案格式的版本 。如果class檔案具有主版本號M和次版本號m,則我們將其class檔案格式的版本表示為M.m。因此,class檔案格式版本可以按字典順序排序,例如,1.5 <2.0 <2.1。Java虛擬機器實現可以支援一個class檔案的版本v當且僅當v在一些連續範圍Mi.0 ≤ v ≤ Mj.m,該範圍基於實現符合的Java SE平臺版本。符合給定Java SE Platform版本的實現必須支援如下表中指定的範圍。(對於歷史案例,將顯示JDK版本而不是Java SE平臺版本)。當我們將高版本的JRE上javac編譯而成的class檔案,不能在舊版本中執行,反之亦然。否則,會報如下錯誤:

Exception in thread “main” java.lang.UnsupportedClassVersionError:Coo has been compiled by a …

Java SE 版本 class檔案中的版本範圍
1.0.2 45.0 v 45.3
1.1 45.0 v 45.65535
1.2 45.0 v 46.0
1.3 45.0 v 47.0
1.4 45.0 v 48.0
5.0 45.0 v 49.0
6 45.0 v 50.0
7 45.0 v 51.0
8 45.0 v 52.0
9 45.0 v 53.0
10 45.0 v 54.0
對應的3)類的訪問許可權通常為 ACC_ 開頭的常量。具體的每個常量的意義可以查閱[Java虛擬機器規範4.1小節](https://docs.oracle.com/javase/specs/jvms/se10/html/jvms-4.html#jvms-4.1 “Java虛擬機器規範4.1小節”)。
標誌名稱 解釋
ACC_PUBLIC 0×0001 宣告public; 可以從其包的外部訪問
ACC_FINAL 0×0010 宣告final; 不允許的有子類
ACC_SUPER 0×0020 invokespecial指令呼叫時特殊處理超類方法
ACC_INTERFACE 0×0200 是一個介面,而不是一個類
ACC_ABSTRACT 0x0400 宣告abstract; 不得被直接例項化
ACC_SYNTHETIC 0x1000 合成標誌(宣佈為其他類進行合成,一般在執行時生成的類,比如動態代理中,生成的類會是該標誌); 不在原始碼中進行設定
ACC_ANNOTATION 0x2000 宣告為註解型別的類
ACC_ENUM 0x4000 宣告為enum型別

ACC_MODULE

0x8000 該標誌表示此class檔案被定義為模組(即在JDK9及其以上加入的專門用於定義模組的類),而不是類或介面,在該標識下會有一些特殊的規則

如果ACC___MODULE在access_flags專案中設定了標誌,則access_flags可以不設定專案中的其他標誌,並且要滿足以下規則適用於Class檔案結構的其餘部分 :

  • major version,minor version: ≥ 53.0(即JDK 9以上)

  • this_class: module-info

  • super_class, interfaces_count, fields_count, methods_count:0

  • attributes:Module必須存在一個屬性。除Module,ModulePackages,ModuleMainClass,InnerClasses,SourceFile, SourceDebugExtension,RuntimeVisibleAnnotations和 RuntimeInvisibleAnnotations,沒有預先定義的屬性。

2.常量池

用來存放各種常量以及符號引用。常量池中的每一項都有一個對應的索引(如#1),並且可能引用其他的常量池項(#1 = Methodref #8.#25)。

Constant pool:
   #1 = Methodref          #8.#25         // java/lang/Object."<init>":()V
      ...
   #8 = Class              #32            // java/lang/Object
      ...
  #15 = Utf8               <init>
  #16 = Utf8               ()V
      ...
  #25 = NameAndType        #15:#16        // "<init>":()V
      ...
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/io/Serializable
  #34 = Utf8               java/lang/Throwable

如上 1 號常量池項是一個指向 Object 類構造器的符號引用。它是由另外兩個常
量池項所構成。如果將它看成一個樹結構的話,那麼它的葉節點會是字串常量,如下圖所示:
常量池中的引用關係圖

3. 欄位區域

用來列舉該類中的各個欄位。這裡最主要的資訊便是該欄位的型別(descriptor:
I)以及訪問許可權(flags: (0x0002) ACC_PRIVATE)。對於宣告為 final 的靜態欄位而言,
如果它是基本型別或者字串型別,那麼欄位區域還將包括它的常量值。

private int tryBlock;
    descriptor: I
    flags: ACC_PRIVATE

Java 虛擬機器同樣使用了“描述符”(descriptor)來描述欄位的型別。具體的對照如下
表所示。其中比較特殊的,高亮顯示。
描述符

4.方法區域

用來列舉該類中的各個方法。除了方法描述符以及訪問許可權之外,每個方法還包
括最為重要的程式碼區域。

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: iconst_0
         ...
        10: goto          35
         ...
        34: athrow
        35: aload_0
        36: iconst_3
        37: putfield      #6                  // Field methodExit:I
        40: return
      Exception table:
         from    to  target type
             0     5    13   Class java/lang/Exception
             0     5    27   any
            13    19    27   any
      LineNumberTable:
        line 16: 0
        line 20: 5
           ...
        line 23: 35
        line 24: 40
      StackMapTable: number_of_entries = 3
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
           ...

程式碼區域一開始會宣告該方法中的運算元棧(stack=2)和區域性變數數目(locals=3)的最大值,以及該方法接收引數的個(args_size=1)。(這裡區域性變數指的是位元組碼中的區域性變數,而非 Java 程式中的區域性變數)。

緊接著是該方法的位元組碼。每條位元組碼均標註了對應的偏移量(bytecode index,BCI),這
是用來定位位元組碼的。比如說偏移量為 10 的跳轉位元組碼 10: goto 35,將跳轉至偏移量為 35
的位元組碼 35: aload_0

緊跟著的異常表(Exception table:)也會使用偏移量來定位每個異常處理器所監控的範圍(由
from 到 to 的程式碼區域),以及異常處理器的起始位置(from到target)。除此之外,它還會宣告所
捕獲的異常型別(type)。其中,any 指代任意異常型別。

再接下來的行數表(LineNumberTable:)則是 Java 源程式到位元組碼偏移量的對映。如果你在
編譯時使用了 -g 引數(javac -g Foo.java),那麼這裡還將出現區域性變量表
(LocalVariableTable:),展示 Java 程式中每個區域性變數的名字、型別以及作用域。
行數表和區域性變量表均屬於除錯資訊。Java 虛擬機器並不要求 class 檔案必備這些資訊。

  LocalVariableTable:
    Start  Length  Slot  Name   Signature
       14       5     1     e   Ljava/lang/Exception;
        0      41     0  this   LCoo;

最後則是位元組碼運算元棧的對映表(StackMapTable: number_of_entries = 3)。該表描述的
是位元組碼跳轉後運算元棧的分佈情況,一般被 Java 虛擬機器用於驗證所載入的類,以及即時編譯
相關的一些操作。

我們再利用jclasslib bytecode viwer開啟該位元組碼檔案:

基本資訊如下:

基本資訊
基本資訊

常量區如下:

常量池

欄位區域如下:

欄位區域

方法區域如下:

方法區域

從上圖中可以看出和我們分析的一致,並且可以推斷出jclasslib bytecode viwer底層也是使用javap命令來獲取對應的資料,,並利用Swing將其展示。