從一個基本的類的反編譯說起讓你讀懂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 |
標誌名稱 | 值 | 解釋 |
---|---|---|
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 型別
|
|
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將其展示。