Java中的類檔案結構之一:如何分析一個.class檔案的二進位制碼內容
該文為作者原創,請轉載者註明出處
以下為一個Java類--Temp4Test
package com.demo; public class Temp4Test extends Temp3Test { private int i = 1; public float f; public static String thisstr = ""; public Temp4Test(int ii, String str, float ff) { i = ii; thisstr = str; f = ff; } public static void main(String[] args) { Temp4Test t4 = new Temp4Test(100, "hello", 5.5f); System.out.println(t4); } @Override public String toString() { return "[" + i + " , " + thisstr + " , " + f + "]"; } }
其父類Temp3Test.java的實現為:
package com.demo;
public class Temp3Test implements Cloneable {
private int ii;
public static String str = "hi";
public static void main(String[] args) {
int i = 123;
}
}
所生成的Temp4Test.class的二進位制檔案為:
感興趣的同學可以自己編譯下就可以看到上述結果。
下面逐位分析一下二進位制檔案中各位的含義
a.象徵是.class檔案的魔數:頭4個Byte,看到這4個Byte就可以基本確認為一個.class檔案,固定值:0xCAFEBABE。
b.class檔案版本號,第5、6個位元組是次版本號,第7、8個位元組為主版本號,在上述二進位制碼為0x00000034,代表版本號為52.0即JDK1.8.0
c.常量池,從0x0052開始(含0x0052)
c.1 首先的兩個位元組為常量池中所含常量的數量,本例中即為0x0052,所代表的數為:0x0052==5*16+2-1==81(換為十進位制為81個常量)
c.2 從0x07開始為1-81個常量的二進位制位表,開始位如下圖所示:
常量池共有14種類型的常量,如下圖所示,截圖來源於《深入理解Java虛擬機器》
每一個常量,第一位均為該常量型別,即上述表中14項之一,以第一個常量為例0x07表示類或介面的符號引用,即CONSTANT_Class_info,此型別結構為:
因此第一個常量即為0x070002,0x0002是一個索引,表示指向第二個常量,第二個常量型別為0x01,為下圖所示二進位制位
0x01表示(上面有表)CONSTANT_Utf8_info,即字串,CONSTANT_Utf8_info型別的常量結構為下圖:
可知,第二個常量對應的二進位制碼為:0x010012(共計1*16+2=18位)636F6D2F64656D6F2F54656D703454657374,後面的字串所對應的文字為:com/demo/Temp4Test,以此類推,可以推出上述二進位制檔案中的所有常量池常量,以下是我手寫的一個推導圖,有點粗漏,^_^
以下為14種常量項的結構總表
d.訪問標誌,緊挨著常量池的兩個位元組,含義如下表:
在上述示例中為:
0x0021==0x0001|0x0020表示是一個由使用者定義的public的類
e.接下來的二進位制表示該類的繼承關係,共有三項內容:
e.1 類索引,兩個位元組,常量池中的對本類的描述
e.2 父類索引,兩個位元組,常量池中對父類的描述
e.3 介面索引,頭兩個位元組表示介面數,然後,緊跟進介面列表
本例中的二進位制碼為:
0x0001表示常量池中第一個常量為類索引,0x0003為表示常量池中第三個常量為父類索引,0x0000表示實現的介面個數為0個
f.接著的二進位制表示欄位表,頭兩位為個數,從上圖可以看出,有三個欄位(0x0003),每個欄位規則如下圖
f.1 access_flags的取值如下圖
f.2 name_index的含義,同上,對映至常量池中的常量索引
f.3 descriptor_index,描述符,表示該變數的型別,此處插一下描述符的表示規則:
f.3.* 描述符中的字元含義如下:
f.3.** 表示陣列時,每一個維度前用“[”表示
f.3.*** 描述方法,先參後返回值
下面舉一個描述符的例子:
上面的例子中,Temp4Test.java類中的建構函式的返回值及引數描述為:(ILjava/lang/String;F)V
本例中的欄位表,三個欄位分別為:
0x0002(private)00050006(檢視第五常量、第六常量)0000
0x0001(public)00070008(檢視第七常量、第八常量)0000
0x0009(0x0001|0x0008 public static)0009000A(檢視第九常量、第十常量)0000
g.接著的二進位制表示方法表,頭兩位為個數,從上圖可以看出,有四個欄位(0x0004),每個方法表述規則如下圖
g.1 access_flags的含義與欄位表有所差別,具體如下圖:
其他的name_index、descriptor_index同欄位表
g.2 在本例中,方法1-方法4的二進位制塊如下圖:
方法1:
兩個紅色豎線中間的部分
方法2:
方法3:
方法4:
g.3 下面找一個方法來說明一下方法的二進位制怎麼看,以方法2為例
0x0001 -- public
0x0014 -- 第二十個常量,方法名
0x0015 -- 第二十一個常常,方法描述
0x0001 -- 含有一個屬性表
後面一直到方法結束均為屬性表的二進位制內容
g.4 屬性是一個特別複雜的二進位制規則,之後會寫一篇文章專門說述一下屬性表的讀法,在此不一一展開描述,只說一下當前例子方法(即方法2)中的屬性表讀取規則
0x000D -- 第十三個常量,查常量表,可知,表示該屬性為Code,Code的屬性規則為:
attribute_name_index,即0x000D,表示Code
attribute_length,表示該屬性所佔位元組數(不包括attribute_name_index和attribute_length),方法2中為0x00000074==7*16+4==116
max_stack,最大堆疊數為0x0002
max_locals,最大臨時變數數為0x0004
code_length,位元組碼數量為0x00000018 == 16+8 == 24Byte
code,如下圖:
位元組碼的閱讀不在該文中討論
exception_table_length,0x0000,沒有
attributes_count,0x0002,下面內嵌了兩個屬性表,內嵌的屬性表的讀取方式同上面所述,和外層的讀取規則是一樣的,只是屬性不為Code了。因屬性表的全部分類並未羅列,也就不再讀了從0x0012開始直到方法2結束,均為這兩個屬性表的二進位制碼。
h.方法表的二進位制碼結束後,對於本例來講,基本二進位制碼就進入最後了,還剩一點是SourceFile的屬性,讀法,如下:
對應的二進位制為:
0x0050,第八十位常量--SourceFile
0x00000002,後面還有兩位,這個值在SourceFile型別中是定值,只有2位,原因就是上圖的規則定義
0x0051,第八十一位常量
結語:終於把類的二進位制檔案讀完了,當然,上述例子比較簡單,但麻雀雖小五臟俱全,複雜的檔案只是多了一些其他類別,讀法是和上述例子一致的。上面有一些內容將在後續文章中繼續詳述,會有一篇文章專門講述文字化的常量池是如何讀的,還會有一篇文章講述屬性表,再有一篇文章詳述位元組碼,文中的截圖來源於《深入理解Java虛擬機器》,在此向作者致以最深的敬意,同時,圖片若侵權,請聯絡我,將第一時間刪除。
比二進位制更方便的是文字形態的閱讀方法,詳見第二篇:Java中的類檔案結構之二:分析一個.class檔案的文字化閱讀