如何實現一個Java Class字節解析器(Golang版)

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

Class文件

做爲類或者接口信息的載體,每一個Class文件都完整的定義了一個類。爲了使Java程序能夠「編寫一次,到處運行」,Java虛擬機規範Class文件進行了嚴格的規定。構成Class文件的基本數據單位是字節,這些字節之間不存在任何分隔符,這使得整個Class文件中存儲的內容幾乎所有是程序運行的必要數據,單個字節沒法表示的數據由多個連續的字節來表示。github

根據Java虛擬機規範,Class文件採用一種相似於C語言結構體的僞結構來存儲數據,這種僞結構中只有兩種數據類型:無符號數和表。Java虛擬機規範定義了u1u2u4u8來分別表示1個字節、2個字節、4個字節和8個字節的無符號數,無符號數能夠用來描述數字、索引引用、數量值或者是字符串。表是由多個無符號數或者其它表做爲數據項構成的複合數據類型,表用於描述有層次關係的複合結構的數據,所以整個Class文件本質上就是一張表。在SmallVMu1u2u4u8分別對應於uint8uint16uint32uint64Class文件被描述爲以下結構體。golang

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自己也有相應的屬性,所以,在解析字段表集合和方法表集合的同時也包含了屬性表的解析。ui

常量池佔據了Class文件很大一部分的數據,用於存儲全部的常量信息,包括數字和字符串常量、類名、接口名、字段名和方法名等。Java虛擬機規範定義了多種常量類型,每一種常量類型都有本身的結構。常量池自己是一個表,在解析時有幾點須要注意。this

  • 每一個常量類型都經過一個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)code

屬性表用於描述某些場景專有的信息,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文件的每一個字節進行了分析,但願對你們的理解有所幫助。

相關文章
相關標籤/搜索