☕[Java技術指南](1)Class類檔案的結構介紹(上篇)

liboware發表於2021-06-09

Java原始碼被編譯為Class檔案之後,Class檔案結構是JVM載入Class,例項化物件,和進行方法呼叫的重要依據.


每個 Class 檔案都是由 8 位元組為單位的位元組流組成,所有的 16 位、32 位和 64 位長度的數 據將被構造成 2 個、4 個和 8 個 8 位元組單位來表示。多位元組資料項總是按照 Big-Endian1的順 序進行儲存。在Java SDK中,訪問這種格式的資料可以使用java.io.DataInput、 java.io.DataOutput 等介面和 java.io.DataInputStream 和 java.io.DataOutputStream 等類來實現。


Class 檔案的內容可用一組私有資料型別來表示,它們包括 u1,u2 和 u4,分別代 表了1、2和4個位元組的無符號數。在 Java SDK 中這些型別的資料可以通過實現介面 java.io.DataInput 中的 readUnsignedByte、readUnsignedShort 和 readInt 方法進 行讀取。


ClassFile 結構 每一個 Class 檔案對應於一個如下所示的 ClassFile 結構體,其包含的屬性如下表:

幾個問題

  1. 執行時常量池和靜態常量池有什麼區別?
  2. Class檔案裡面都有什麼內容?
  3. Class檔案反彙編之後的格式裡面分別有什麼,嘗試解讀裡面方法中的彙編指令
  4. 本地變數表和運算元棧是如何工作?

以十六進位制檢視Class檔案

技巧:

vim + xxd = 十六進位制編輯器vim + xxd = 十六進位制編輯器

  • vim -b xxx.class 可以以二進位制將class檔案開啟;
  • vim內呼叫::%!xxd 以十六進位制顯示當前檔案;
  • 修改完成之後,如果想儲存,執行以下命令把十六進位制轉換回二進位制- :%!xxd -r

輸出包括行號,本地變數反彙編等資訊

javap

  • -v -verbose:輸出附加資訊(包括行號、本地變數表、反彙編等資訊)
  • -c:對程式碼進行反彙編
    如:
  • javap -c xxx.class
  • javap -verbose Test.class

更多關於javap的介紹:docs.oracle.com/javase/7/docs/tech...

關於反彙編:

反彙編(Disassembly):把目的碼轉為彙編程式碼的過程,也可以說是把機器語言轉換為組合語言程式碼、低階轉高階的意思。軟體一切神祕的執行機制全在反彙編程式碼裡面。

JVM規範中的Class檔案解讀


ClassFile {
    u4             magic; // 魔數
    u2             minor_version; // 副版本號
    u2             major_version; // 主版本號
    u2             constant_pool_count; // 常量池計數器
    cp_info        constant_pool[constant_pool_count-1]; // 常量池資料區
    u2             access_flags; // 訪問標誌
    u2             this_class; // 類索引
    u2             super_class; // 父類索引
    u2             interfaces_count; // 介面計數器
    u2             interfaces[interfaces_count]; // 介面表
    u2             fields_count; // 欄位計數器
    field_info     fields[fields_count]; // 欄位表
    u2             methods_count; // 方法計數器
    method_info    methods[methods_count]; // 方法表
    u2             attributes_count; // 屬性計數器
    attribute_info attributes[attributes_count]; // 屬性表
}

Class檔案是一組以8位位元組為基礎單位的二進位制流。類結構有兩種資料型別:

  • 無符號數:無符號數屬於基本屬性型別,用u1, u2, u4, u8分別代表1個位元組,2個位元組,4個位元組和8個位元組的無符號數,可以用它描述數字、索引引用、數量值或者utf8編碼的字串值;
  • 表:由多個無符號數或者其他表作為資料項構成的複合資料型別,以命名_info結尾。
    cafe babe 0000 0034 001d 0a00 0600 0f09
    0010 0011 0800 120a 0013 0014 0700 1507
    0016 0100 063c 696e 6974 3e01 0003 2829
    5601 0004 436f 6465 0100 0f4c 696e 654e
    756d 6265 7254 6162 6c65 0100 046d 6169
    6e01 0016 285b 4c6a 6176 612f 6c61 6e67
    2f53 7472 696e 673b 2956 0100 0a53 6f75
    7263 6546 696c 6501 0008 4c6f 672e 6a61
    7661 0c00 0700 0807 0017 0c00 1800 1901
    000c 6865 6c6c 6f20 776f 726c 6421 0700
    1a0c 001b 001c 0100 1263 6f6d 2f68 656c
    6c6f 2f74 6573 742f 4c6f 6701 0010 6a61
    7661 2f6c 616e 672f 4f62 6a65 6374 0100
    106a 6176 612f 6c61 6e67 2f53 7973 7465
    6d01 0003 6f75 7401 0015 4c6a 6176 612f
    696f 2f50 7269 6e74 5374 7265 616d 3b01
    0013 6a61 7661 2f69 6f2f 5072 696e 7453
    7472 6561 6d01 0007 7072 696e 746c 6e01
    0015 284c 6a61 7661 2f6c 616e 672f 5374
    7269 6e67 3b29 5600 2100 0500 0600 0000
    0000 0200 0100 0700 0800 0100 0900 0000
    1d00 0100 0100 0000 052a b700 01b1 0000
    0001 000a 0000 0006 0001 0000 0003 0009
    000b 000c 0001 0009 0000 0025 0002 0001
    0000 0009 b200 0212 03b6 0004 b100 0000
    0100 0a00 0000 0a00 0200 0000 0500 0800
    0600 0100 0d00 0000 0200 0e
    根據以上的Class檔案結構,我們可以梳理出以下的Class檔案結構圖:

魔數 magic

 用於標識這個檔案的格式,Class檔案格式的魔數為 0xCAFEBABE。
 Class 檔案的第 1 - 4 個位元組代表了該檔案的魔數。

魔數的唯一作用是確定這個檔案是否為一個能被虛擬機器所接受的 Class 檔案。魔數值固定為 0xCAFEBABE,不會改變。魔數的唯一作用是確定這個檔案是否為一個能被虛擬機器所接受的 Class 檔案。魔數值固定為 0xCAFEBABE,不會改變。

副版本號 minor_version,主版本號 major_version

  • Class 檔案的第 5 - 6 個位元組代表了 Class 檔案的副版本號。
  • Class 檔案的第 7 - 8 個位元組代表了 Class 檔案的主版本號。

主版本號和次版本號共同決定了類檔案格式的版本。 如果類檔案的主版本號為M,次版本號為m,則將其類檔案格式的版本表示為M.m。

因此,可以按字典順序對類檔案格式版本進行排序,例如1.5 <2.0 <2.1。minor_version和major_version專案的值是此類檔案的次要版本號和主要版本號。


  • Java 虛擬機器例項只能支援特定範圍內的主版本號(Mi 至 Mj)和 0 至特定範圍 內(0 至 m)的副版本號。
  • 假設一個 Class 檔案的格式版本號為 V,僅當 Mi.0 ≤ v ≤ Mj.m 成立時,這個 Class 檔案才可以被此 Java 虛擬機器支援。不同版本的 Java 虛擬機器實現 支援的版本號也不同,高版本號的 Java 虛擬機器實現可以支援低版本號的 Class 檔案

下表列出了各個版本 JDK 的十六進位制版本號資訊:

上述 class 檔案 0000 0034 對應的就是表格中的 JDK1.8。

常量池計數器 constant_pool_count

緊跟版本資訊之後的是常量池資訊,其中前 2 個位元組表示常量池計數器,其後的不定長資料則表示常量池的具體資訊。constant_pool表的索引[1,constant_pool_count-1]

  • 常量池描述著整個Class檔案中所有的字面量資訊。常量池計數器(constant_pool_count)的值等於常量池(constant_pool)表中的條目數加一。
  • 如果constant_pool索引大於零且小於constant_pool_count,則該索引被視為有效。對於 long 和 double 型別有例外情況。
  • 在 Class 檔案的常量池中,所有的 8 位元組的常量都佔兩個表成員(項)的空間。如果一個 CONSTANT_Long_info 或 CONSTANT_Double_info 結構的項在常量池中的索引為 n,則常量池中下一個有效的項的索引為 n+2,此時常量池中索引為 n+1 的項有效但必須被認為不可用。

class 檔案位元組碼對應的內容是:001d,其值為 29,表示一共有 29 - 1 = 28 個常量。

常量池表 constant_pool[]

緊跟著常量池計數器後面就是 28 個常量了,因為每個常量都對應不同的型別,需要一個個具體分析。


constant_pool[]是一個結構表,表示各種字串常量,類和介面名稱,欄位名稱以及在ClassFile結構及其子結構中引用的其他常量。 每個constant_pool表條目的格式由其第一個“標籤”位元組指示。 所有型別的常量池表專案有以下通用的格式:

cp_info {
    u1 tag;
    u1 info[];
}

常量池中,每個 cp_info 項的格式必須相同,它們都以一個表示 cp_info 型別的單位元組 “tag”項開頭。後面 info[]項的內容 tag 由的型別所決定。tag 有效的型別和對應的取值在下圖表示。每個 tag 項必須跟隨 2 個或更多的位元組,這些位元組用於給定這個常量的資訊,附加位元組的資訊格式由 tag 的值決定。

常量池中的14種常量結構

" class="reference-link">

這些 cp_info 表結構又有不同的資料結構,其對應的資料結構如下圖所示。

" class="reference-link">

接下來我們開始分析上述 Log.class 檔案每個位元組的含義,前面第一句話已經說了,緊跟著常量池計數器後面的就是常量池了。下面開始分析:

第 1 個常量

緊接著 001d 的後一個位元組為 0A,為十進位制數字 10,查表可知其為方法引用型別(CONSTANT_Methodref_info)的常量。在 cp_info 中結構如下所示:

查詢的方式是先確定 tag 值,tag 值判斷當前屬於哪一個常量。這裡 tag 為 10。

然後看其結構顯示還有兩個 U2 的index,說明後面 4 個位元組都是屬於第一個常量,其中第 2 - 3 個位元組表示類資訊,第 4 - 5 個位元組表示名稱及類描述符。
接下來我們取出這部分的資料:0a 0600 000f :


該常量項:

第 2 - 3 個位元組,其值為 00 06,表示指向常量池第 6 個常量所表示的資訊。根據後面我們分析的結果知道第 6 個常量是 java/lang/Object。


第 4 - 5 個位元組,其值為 000f,表示指向常量池第 15 個常量所表示的資訊,根據 javap 反編譯出來的資訊可知第 15 個常量是 :()V。


將兩者組合起來:java/lang/Object.:V,即 Objectinit 初始化方法。

javap -v Log.class
Classfile /Users/xxx/Desktop/Log.class
  Last modified 2020-1-8; size 427 bytes
  MD5 checksum 745be5a6df4d9554e783dbbcecaf9b6d
  Compiled from "Log.java"
public class com.hello.test.Log
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // hello world!
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // com/hello/test/Log
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Log.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               hello world!
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               com/hello/test/Log
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public com.hello.test.Log();
    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 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
}
SourceFile: "Log.java"

其實從上面的結果也可以看出來,第一個常量對應的是第6,15個常量,組合起來的含義後面註釋也寫著了。
其他很多常量都是類似的,接下來我們看看字串是怎麼來得。

第 21 個常量

第 21 個常量,資料為

0100 1263 6f6d 2f68 656c 6c6f 2f74 6573 742f 4c6f 67

這裡 tag 值是 01,對應的結構如下:


length 是 u2,對應著 0012,說明後面跟著 18 個位元組:63 6f6d 2f68 656c 6c6f 2f74 6573 742f 4c6f 67;查 ASCII 表可得 63-c, 6f-o, 6d-m, 2f-/ ··· 4c-L,6f-o, 67-g,

組合起來就是:com/hello/test/Log 。
相信通過上面兩個例子,大家就知道如何去分析常量池裡面的索引了。但很多時候我們可以藉助 JDK 提供的 javap 命令直接檢視 Class 檔案的常量池資訊,但是手動分析能夠讓你更加了解結果為啥是這樣的。其實 javap 出來的就是人家分析總結好的。

access_flags 訪問標誌


在常量池結束之後,緊接著的兩個位元組代表類或介面的訪問標(access_flags)。這裡的資料為 00 21。


access_flags 是一種掩碼標誌,用於表示某個類或者介面的訪問許可權及基礎屬性。access_flags 的取值範圍和相應含義見下表:

LK


  • 第一列是標記名;
  • 第二列是對應的值;
  • 第三列是對應的說明。

帶有 ACC_SYNTHETIC 標誌的類,意味著它是由編譯器自己產生的而不是由程式設計師 編寫的原始碼生成的。

帶有 ACC_ENUM 標誌的類,意味著它或它的父類被宣告為列舉型別。

帶有 ACC_INTERFACE 標誌的類,意味著它是介面而不是類,反之是類而不是介面。


如果一個 Class 檔案被設定了 ACC_INTERFACE 標誌,那麼同時也得設定 ACC_ABSTRACT 標誌。同時它不能再設定ACC_FINAL、 ACC_SUPER 和 ACC_ENUM 標誌。


ANNOTATION註解型別必定帶有 ACC_ANNOTATION 標記,如果設定了 ANNOTATION 標記, ACC_INTERFACE 也必須被同時設定。如果沒有同時設定 ACC_INTERFACE 標記, 那麼這個 Class 檔案可以具有表 4.1 中的除 ACC_ANNOTATION 外的所有其它標記。 當然 ACC_FINALACC_ABSTRACT 這類互斥的標記除外。

ACC_SUPER 標誌用於確定該 Class 檔案裡面的 invokespecial 指令使用的是哪 一種執行語義。目前 Java 虛擬機器的編譯器都應當設定這個標誌。ACC_SUPER 標記 是為了向後相容舊編譯器編譯的 Class 檔案而存在的,在 JDK1.0.2 版本以前的編 譯器產生的 Class 檔案中,access_flag 裡面沒有 ACC_SUPER 標誌。同時, JDK1.0.2 前的 Java 虛擬機器遇到 ACC_SUPER 標記會自動忽略它。


在表中沒有使用的 access_flags 標誌位是為未來擴充而預留的,這些預留的標誌為在編譯器中會被設定為 0, Java 虛擬機器實現也會自動忽略它們。

類索引、父類索引、介面索引

在訪問標記後,則是類索引、父類索引、介面索引的資料,這裡資料為:00 05 、00 06 、00 00。

類索引和父類索引都是一個 u2 型別的資料,而介面索引集合是一組 u2 型別的資料的集合,這個可以由前面 Class 檔案的構成可以得到。 Class 檔案中由這三項資料來確定這個類的繼承關係。

this_class 類索引

類索引,this_class 的值必須是對 constant_pool 表中專案的一個有效索引值。

constant_pool 表在這個索引處的項必須為 CONSTANT_Class_info 型別常量,表示這個 Class 檔案所定義的類或介面。這裡的類索引是 00 05 表示其指向了常量池中第 5 個常量,通過我們之前的分析,我們知道第 5 個常量其最終的資訊是 Log 類。

super_class 父類索引

super_class 值必須為 0 或者對 constant_pool 表中專案的一個有效索引值。


如果它的值不為 0,那 constant_pool 表在這個索引處的項 必須為 CONSTANT_Class_info 型別常量,表示這個 Class 檔案所定義的 類的直接父類。


當前類的直接父類,以及它所有間接父類的 access_flag 中都不能有 ACC_FINAL 標記。對於介面來說,它的 Class 檔案的 super_class 項的值必須是 對 constant_pool 表中專案的一個有效索引值。


constant_pool 表在這個索引處的 項必須為代表 java.lang.Object 的 CONSTANT_Class_info 型別常量。


如果 Class 檔案的 super_class 的值為 0,那這個 Class 檔案只可能是定義的是 java.lang.Object 類,只有它是唯一沒有父類的類。這裡的父類索引是 00 06 表示其指向了常量池中第 6 個常量,通過我們之前的分析,我們知道第 6 個常量其最終的資訊是 Object 類。


因為其並沒有繼承任何類,所以 Demo 類的父類就是預設的 Object 類。

interfaces_count 介面計數器

interfaces_count 的值表示當前類或介面的直接父介面數量。

interfaces[] 介面表

interfaces[] 陣列中的每個成員的值必須是一個對 constant_pool 表中項 目的一個有效索引值,它的長度為 interfaces_count。每個成員 interfaces[i] 必 須為CONSTANT_Class_info型別常量,其中0 ≤ i < interfaces_count。

在 interfaces[]陣列中,成員所表示的介面順序和對應的源 程式碼中給定的介面順序(從左至右)一樣,即 interfaces[0]對應的是原始碼中最左 邊的介面。
這裡 Log 類的位元組碼檔案中,因為並沒有實現任何介面,所以緊跟著父類索引後的兩個位元組是0x0000,這表示該類沒有實現任何介面。因此後面的介面索引表為空。


未完待續……

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章