本文假定讀者對Java Class檔案格式有一些基本的瞭解,建議結合相關書籍進行對照閱讀。
Class檔案格式資訊
例項程式碼
package chapter6;
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
複製程式碼
使用JDK1.8編譯成class檔案,然後通過WinHex開啟
魔數(magic)
型別:u4
位元組地址:00000000~00000003
值:0xCAFEBABE
Class檔案版本
次版本號(minor_version)
型別:u2
位元組地址:00000004~00000005
值:0x0000
主版本號(major_version)
型別:u2
位元組地址:00000006~00000007
值:0x0034
將0x0034轉換為十進位制,計算得到52,對應版本號為JDK 1.8。
常量池
常量池容量計數值(constant_pool_count)
型別:u2
位元組地址:00000008~00000009
值:0x0016
將0x0016轉換為十進位制,計算得到22。由於容量計數是從1開始(如果沒有特殊情況,通常都是從0開始),因此常量池中有21項常量,索引值範圍為1~21。
常量池中每一項常量都是一個表,表開始的第一位是一個u1型別的標誌位(tag)。
第1項常量
tag型別:u1
tag位元組地址:0000000A
tag值:0x07
查表可知這個常量屬於CONSTANT_Class_info結構,代表一個類或者介面的符號引用。
name_index型別:u2
name_index位元組地址:0000000B~0000000C
name_index值:0x0002
0x0002指向了常量池中的第2項常量。
第2項常量
tag型別:u1
tag位元組地址:0000000D
tag值:0x01
查表可知這個常量屬於CONSTANT_Utf8_info結構,代表一個UTF-8編碼的字串。
length型別:u2
length位元組地址:0000000E~0000000F
length值:0x0012
將0x0012轉換為十進位制,計算得到18。
bytes型別:u1
bytes位元組地址:00000010~00000021(length表明地址範圍為18個位元組)
bytes值:下方圖片淺藍底對應的所有位元組內容
通過WinHex檢視,對應內容為chapter6/TestClass,即類的全限定名。
通過逐個位元組對照ASCII字元表,我們同樣可以得到內容為chapter6/TestClass。
- 獲取ASCII字元表:在Linux上執行man ascii,翻頁在Tables項可以看到字元表。
- 查詢字元:先找橫座標,再找縱座標,橫豎交叉的位置即為位元組對應的字元。
例如0x63為c,0x68為h,0x61為a,0x70為p,0x74為t,0x65為e,0x72為r,連起來代表單詞chapter。
第3項常量
tag型別:u1
tag位元組地址:00000022
tag值:0x07
這個常量屬於CONSTANT_Class_info結構,代表一個類或者介面的符號引用。
name_index型別:u2
name_index位元組地址:00000023~00000024
name_index值:0x0004
0x0004指向了常量池中的第4項常量。
第4項常量
tag型別:u1
tag位元組地址:00000025
tag值:0x01
這個常量屬於CONSTANT_Utf8_info結構,代表一個UTF-8編碼的字串。
length型別:u2
length位元組地址:00000026~00000027
length值:0x0010
將0x0010轉換為十進位制,計算得到16。
bytes型別:u1
bytes位元組地址:00000028~00000037(length表明地址範圍為16個位元組)
bytes值:下方圖片淺藍底對應的所有位元組內容
通過WinHex檢視,對應內容為java/lang/Object,即類的全限定名。
第5項常量
tag型別:u1
tag位元組地址:00000038
tag值:0x01
這個常量屬於CONSTANT_Utf8_info結構,代表一個UTF-8編碼的字串。
length型別:u2
length位元組地址:00000039~0000003A
length值:0x0001
bytes型別:u1
bytes位元組地址:0000003B(length表明地址範圍為1個位元組)
bytes值:0x6D
通過WinHex檢視,對應內容為例項變數m。
其他常量可以通過類似的方法進行分析,但這樣一個個分析確實挺辛苦的。
其實,JDK已經為我們提供了一個Class檔案位元組碼工具:javap,可以讓我們較為直觀的看到Class檔案的位元組碼內容。
執行命令:javap -verbose TestClass.class,擷取常量池部分內容如下:
可以看到,版本號及前5個常量與我們分析的結果是一致的。所以,能用1行程式碼搞定的事兒,就不要用2行(浪費筆墨)。
常量池最後一個位元組:000000D8
訪問標誌(access_flags)
型別:u2
位元組地址:000000D9~000000DA
值:0x0021
檢視類或介面訪問標誌含義表可知,該類的訪問標誌為ACC_PUBLIC(0x0001)、ACC_SUPER(0x0020)。
另外,通過類的定義public class TestClass,同樣可以推斷出類的訪問標誌為ACC_PUBLIC、ACC_SUPER,而ACC_INTERFACE、ACC_ENUM、ACC_FINAL、ACC_ABSTRACT、ACC_ANNOTATION、ACC_SYNTHETIC都可以排除。
所以,access_flags應該為0x0001|0x0020=0x0021,結果與檢視位元組碼相同。
類索引(this_class)
型別:u2
位元組地址:000000DB~000000DC
值:0x0001
this_class指向常量池的第1個常量,基於前面的分析可知:
- 第1個常量的型別為Class,Class名稱索引指向第2個常量。
- 第2個常量型別為Utf8,對應內容為chapter6/TestClass。
因此,類索引(this_class)指向的類為chapter6/TestClass。
父類索引(super_class)
型別:u2
位元組地址:000000DD~000000DE
值:0x0003
同樣,super_class指向常量池的第3個常量。
- 第3個常量的型別為Class,Class名稱索引指向第4個常量。
- 第4個常量型別為Utf8,對應內容為java/lang/Object。
因此,父類索引(super_class)指向的類為java/lang/Object。
介面計數器(interfaces_count)
型別:u2
位元組地址:000000DF~000000E0
值:0x0000
介面計數器值為0,說明該類沒有實現任何介面。
介面表(interfaces)
無
類索引(this_class)、父類索引(super_class)和介面索引(interfaces)這三項資料共同確定了當前類以及其繼承關係,相關常量池內容如下:
完整地址範圍:000000DB~000000E0
欄位
欄位計數器(fields_count)
型別:u2
位元組地址:000000E1~000000E2
值:0x0001
說明當前類有1個欄位。
欄位表(fields)
訪問標誌(access_flags)
型別:u2
位元組地址:000000E3~000000E4
值:0x0002
對應的訪問標誌為ACC_PRIVATE。
名稱索引(name_index)
型別:u2
位元組地址:000000E5~000000E6
值:0x0005
對應常量池中的第5項常量,即欄位名為m。
描述符(descriptor_index)
型別:u2
位元組地址:000000E7~000000E8
值:0x0006
對應常量池中的第6項常量,值為I,即int型別。
因此,該欄位的定義為private int m;
屬性計數器(attributes_count)
型別:u2
位元組地址:000000E9~000000EA
值:0x0000
說明該欄位沒有屬性資訊。
屬性表(attributes)
無。
欄位完整地址範圍:000000E1~000000EA
最後是方法和屬性,由於內容複雜度及篇幅原因,我們下篇再續。
參考
《Java虛擬機器規範》(Java SE 8版)
《深入理解Java虛擬機器 JVM高階特性與最佳實踐》
個人公眾號
更多文章,請關注公眾號:二進位制之路