如何實現一個Java Class位元組解析器

tinylcy發表於2017-03-28

最近在寫一個私人專案,名字叫做SmallVMSmallVM的目的在於通過實現一個輕量級的Java虛擬機器,加深對Java虛擬機器的認知和理解。在Java虛擬機器載入類的過程中,需要對Class檔案進行解析,我曾經單獨實現過一個Java版的Class位元組解析器ClassAnalyzer,相比於Java版,新版(Golang版)更加健壯,思路也更加清晰。本文即闡述我實現Class位元組解析器的思路。

Class 檔案

作為類或者介面資訊的載體,每個Class檔案都完整的定義了一個類。為了使Java程式可以 “編寫一次,處處執行”,Java 虛擬機器規範Class檔案進行了嚴格的規定。構成Class檔案的基本資料單位是位元組,這些位元組之間不存在任何分隔符,這使得整個Class檔案中儲存的內容幾乎全部是程式執行的必要資料,單個位元組無法表示的資料由多個連續的位元組來表示。

根據Java虛擬機器規範,Class檔案採用一種類似於C語言結構體的偽結構來儲存資料,這種偽結構中只有兩種資料型別:無符號數和表。Java虛擬機器規範定義了u1u2u4u8來分別表示1個位元組、2個位元組、4個位元組和8個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或者是字串。表是由多個無符號數或者其它表作為資料項構成的複合資料型別,表用於描述有層次關係的複合結構的資料,因此整個Class檔案本質上就是一張表。在SmallVMu1u2u4u8分別對應於uint8uint16uint32uint64Class檔案被描述為如下結構體。

type ClassFile struct {
    magic             uint32
    minorVersion      uint16
    majorVersion      uint16
    constantPoolCount uint16
    constantPool      []constantpool.ConstantInfo
    accessFlags       uint16
    thisClass         uint16
    superClass        uint16
    interfacesCount   uint16
    interfaces        []uint16
    fieldsCount       uint16
    fields            []FieldInfo
    methodsCount      uint16
    methods           []MethodInfo
    attributesCount   uint16
    attributes        []attribute.AttributeInfo
}

type FieldInfo struct {
    accessFlags     uint16
    nameIndex       uint16
    descriptorIndex uint16
    attributesCount uint16
    attributes      []attribute.AttributeInfo
}

type MethodInfo struct {
    accessFlags     uint16
    nameIndex       uint16
    descriptorIndex uint16
    attributesCount uint16
    attributes      []attribute.AttributeInfo
}

如何解析

組成Class檔案的各個資料項中,例如魔數、Class檔案的版本、訪問標誌、類索引和父類索引等資料項,它們在每個Class檔案中都佔用固定數量的位元組,在解析時只需要讀取相應數量的位元組。除此之外,需要靈活處理的主要包括4部分:常量池、欄位表集合、方法表集合和屬性表集合。欄位和方法都可以具備自己的屬性,Class本身也有相應的屬性,因此,在解析欄位表集合和方法表集合的同時也包含了屬性表的解析。

常量池佔據了Class檔案很大一部分的資料,用於儲存所有的常量資訊,包括數字和字串常量、類名、介面名、欄位名和方法名等。Java虛擬機器規範定義了多種常量型別,每一種常量型別都有自己的結構。常量池本身是一個表,在解析時有幾點需要注意。

  • 每個常量型別都通過一個u1型別的tag來標識。
  • 表頭給出的常量池大小(constantPoolCount)比實際大1,例如,如果constantPoolCount等於47,那麼常量池中有46項常量。
  • 常量池的索引範圍從1開始,例如,如果constantPoolCount等於47,那麼常量池的索引範圍為1~46。設計者將第0項空出來的目的是用於表達 “不引用任何一個常量池專案”。
  • 如果一個CONSTANT_Long_infoCONSTANT_Double_info結構的項在常量池中的索引為n,則常量池中下一個有效的項的索引為n+2,此時常量池中索引為n+1的項有效但必須被認為不可用。
  • CONSTANT_Utf8_info型常量的結構中包含u1型別的tagu2型別的length和由lengthu1型別組成的bytes,這length位元組的連續資料是一個使用MUTF-8Modified UTF-8)編碼的字串。MUTF-8UTF-8並不相容,主要區別有兩點:一是null字元會被編碼成2位元組(0xC00x80);二是補充字元是按照UTF-16拆分為代理對分別編碼的,相關細節可以看這裡(變種 UTF-8)

屬性表用於描述某些場景專有的資訊,Class檔案、欄位表和方法表都有相應的屬性表集合。Java虛擬機器規範定義了多種屬性,SmallVM目前實現了對常用屬性的解析。和常量型別的資料項不同,屬性並沒有一個tag來標識屬性的型別,但是每個屬性都包含有一個u2型別的attribute_name_indexattribute_name_index指向常量池中的一個CONSTANT_Utf8_info型別的常量,該常量包含著屬性的名稱。在解析屬性時,SmallVM正是通過attribute_name_index指向的常量對應的屬性名稱來得知屬性的型別。

欄位表用於描述類或者介面中宣告的變數,欄位包括類級變數以及例項級變數。欄位表的結構包含一個u2型別的access_flags、一個u2型別的name_index、一個u2型別的descriptor_index、一個u2型別的attributes_countattributes_countattribute_info型別的attributes。我們已經介紹了屬性表的解析,attributes的解析方式與屬性表的解析方式一致。

Class的檔案方法表採用了和欄位表相同的儲存格式,只是access_flags對應的含義有所不同。方法表包含著一個重要的屬性:Code屬性。Code屬性儲存了Java程式碼編譯成的位元組碼指令,在SmallVM中,Code對應的結構體如下所示(僅列出了類屬性)。

type Code struct {
    pool                 []constantpool.ConstantInfo
    attributeNameIndex   uint16
    attributeLength      uint32
    maxStack             uint16
    maxLocals            uint16
    codeLength           uint32
    code                 []byte
    exceptionTableLength uint16
    exceptionTable       []ExceptionInfo
    attributesCount      uint16
    attributes           []AttributeInfo
}

type ExceptionInfo struct {
    startPc   uint16
    endPc     uint16
    handlerPc uint16
    catchType uint16
}

Code屬性中,codeLengthcode分別用於儲存位元組碼長度和位元組碼指令,每條指令即一個位元組(u1型別)。在虛擬機器執行時,通過讀取code中的一個個位元組碼,並將位元組碼翻譯成相應的指令。另外,雖然codeLength是一個u4型別的值,但是實際上一個方法不允許超過65535條位元組碼指令。

程式碼實現

整個Class位元組解析器的原始碼已放在了GitHub上,位元組解析器僅僅是SmallVM的一個小模組,對應的目錄為src/classfile。另外,可以參考ClassAnalyzerREADME,我以一個類的Class檔案為例,對該Class檔案的每個位元組進行了分析,希望對大家的理解有所幫助。

更多原創文章乾貨分享,請關注公眾號
  • 如何實現一個Java Class位元組解析器
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章