[Golang實(shí)現(xiàn)JVM第二篇]解析class文件是萬里長(zhǎng)征第一步

正確解析class文件是萬里長(zhǎng)征第一步。本篇我們會(huì)全程使用golang完成class文件的解析工作。

數(shù)據(jù)類型

JVM的class文件完全是二進(jìn)制文件,最小單位是字節(jié),也有數(shù)據(jù)類型,但都是字節(jié)的整數(shù)倍(廢話)。規(guī)范中class文件一共有兩類數(shù)據(jù),一種是無符號(hào)整數(shù),一種是表。無符號(hào)整數(shù)一共有u1,u2, u4, u8四種類型,分別表示8bit, 16bit, 32bit, 64bit的無符號(hào)整數(shù)。表則是無符號(hào)整數(shù)的集合,class文件中在出現(xiàn)表之前都會(huì)先跟著一個(gè)u2類型的長(zhǎng)度數(shù)據(jù),表名后面表的總長(zhǎng)度,這樣才能正確解析表。

另外還要注意字節(jié)序的問題,JVM規(guī)范規(guī)定class文件統(tǒng)一采用Big Endian字節(jié)序,也就是低地址存儲(chǔ)高位,高地址存放低位。如果是用C/C++語言寫JVM,則程序使用的字節(jié)序是跟CPU綁定的,比如intel的x86平臺(tái)使用Little Endian,PowerPC則是Big Endian。不過幸好我們的主角是Go, Go統(tǒng)一采用大端,這樣就不需要操心平臺(tái)了。假設(shè)我們用一個(gè)二元素的[]byte數(shù)組來存儲(chǔ)從class文件中按順序讀到的u16類型數(shù)據(jù),那么byte[0]就是u16的高8位,byte[1]就是低8位,組合起來就是:

uint16(b[1]) | uint16(b[0]) << 8

即將高位左移8位,然后跟低位做按位或操作即可還原。

Go讀取二進(jìn)制數(shù)據(jù)常用函數(shù)

我們使用標(biāo)準(zhǔn)庫(kù)的io.Reader接口從文件中讀取字節(jié),然后從字節(jié)數(shù)組中還原原本的數(shù)據(jù)類型,例如讀取u16類型的數(shù)據(jù)可以這么寫:

func ReadInt16(bufReader io.Reader) (uint16, error) {
    numBuf := make([]byte, 2, 2)
    _, err := bufReader.Read(numBuf)
    if nil != err {
        return 0, err
    }

    var num uint16
    err = binary.Read(bytes.NewBuffer(numBuf), binary.BigEndian, &num)
    if nil != err {
        return 0, err
    }

    return num, nil
}

這里我們用了binary包替我們執(zhí)行的位運(yùn)算,但是這個(gè)方法會(huì)涉及類型查詢操作和內(nèi)存分配,所以肯定會(huì)比直接手動(dòng)組裝byte要慢一些,但是上篇就已經(jīng)說了,過早優(yōu)化是萬惡之源,不必在意。

同理,u32的讀取可以這么寫:

func ReadInt32(bufReader io.Reader) (uint32, error) {
    numBuf := make([]byte, 4, 4)
    _, err := bufReader.Read(numBuf)
    if nil != err {
        return 0, err
    }

    var num uint32
    err = binary.Read(bytes.NewBuffer(numBuf), binary.BigEndian, &num)
    if nil != err {
        return 0, err
    }

    return num, nil

如果是讀取u8,那直接讀一個(gè)byte返回就可以了:

func ReadInt8(bufReader io.Reader) (uint8, error) {
    numBuf := make([]byte, 1, 1)
    _, err := bufReader.Read(numBuf)
    if nil != err {
        return 0, err
    }

    return numBuf[0], nil
}

至此,我們已經(jīng)排除了讀取class文件的全部"技術(shù)障礙"。

class文件結(jié)構(gòu)

我們先用Go定義出一個(gè)class文件的完整結(jié)構(gòu):

// class文件定義
type DefFile struct {
    MagicNumber uint32

    MinorVersion uint16
    MajorVersion uint16

    // 常量池?cái)?shù)量
    ConstPoolCount uint16
    // 常量池
    ConstPool []interface{}

    // 訪問標(biāo)記
    AccessFlag uint16
    // 當(dāng)前類在常量池的索引
    ThisClass uint16
    // 父類索引
    SuperClass uint16

    // 接口
    InterfacesCount uint16
    Interfaces []uint16

    // 字段
    FieldsCount uint16
    Fields []*FieldInfo

    // 方法
    MethodCount uint16
    Methods []*MethodInfo

    // 屬性
    AttrCount uint16
    Attrs []interface{}
}

我們一個(gè)個(gè)的來看。

  • MagicNumber, MajorVersion, MinorVersion

上來就是一個(gè)標(biāo)識(shí)文件類型的魔術(shù)數(shù),就是那個(gè)有名的“咖啡寶貝” 0xCAFEBABE。然后是主版本號(hào)、副版本號(hào)。這些沒啥好說的。

  • ConstPoolCount, ConstPool

這是整個(gè) class文件最重要的部分,常量池。對(duì)是常量池,并不是字節(jié)碼。先是一個(gè)16位無符號(hào)整數(shù)表示常量池?cái)?shù)據(jù)項(xiàng)的數(shù)量,然后就是常量池?cái)?shù)組。 所有的符號(hào)引用和字面值(如字符串, 整數(shù))都保存在常量池中,所有其他屬性都通過保存常量池?cái)?shù)組下標(biāo)的方式來記錄自己引用了哪一條數(shù)據(jù)。要注意的一點(diǎn)是常量池?cái)?shù)組的下標(biāo)是從1開始填充數(shù)據(jù)的,下標(biāo)為0的位置不保存任何數(shù)據(jù)項(xiàng),這是為了方便表達(dá)"不指向任何一個(gè)常量"的含義。比如ConstPoolCount = 10的話,則ConstPool數(shù)組有11個(gè)元素,下標(biāo)從1開始,直到11為止。

常量池?cái)?shù)據(jù)項(xiàng)有十幾種種類型,隨著JDK版本的增加往往會(huì)有新的類型加入。每種類型的結(jié)構(gòu)都不太樣,但是都遵循先是一個(gè)uint8類型的tag用來表示數(shù)據(jù)項(xiàng)類型,然后是常量池?cái)?shù)據(jù)的結(jié)構(gòu),例如方法引用項(xiàng)(CONSTANT_Methodref):

// 方法引用常量
type MethodRefConstInfo struct {
    Tag uint8
    ClassIndex uint16
    NameAndTypeIndex uint16
}

Tag: 固定為10, 表示這是一條方法引用數(shù)據(jù)項(xiàng)

ClassIndex: 是一個(gè)常量池的數(shù)組下標(biāo),引用的是一條類引用(CONSTANT_Class)類型的數(shù)據(jù)項(xiàng),用來記錄方法屬于哪個(gè)類。

NameAndTypeIndex: 同樣是常量池的數(shù)組下標(biāo),引用的是一個(gè)NameAndType類型,用來記錄方法名、方面描述符號(hào),而方法描述符中記錄了方法的參數(shù)類型和返回值類型。

這里就是單拿一個(gè)例子來舉例,在Java8中完整的常量池類型和結(jié)構(gòu)可以直接參考Oralce的JVM規(guī)范在線文檔:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html ,就不再一一列舉了。因?yàn)榉浅7爆?,這里列出來解釋了也是云里霧里,意義不大,后面在解釋字節(jié)碼引用到常量池的時(shí)候再解釋含義。要注意的是我們并不需要實(shí)現(xiàn)全部常量池類型,只需要實(shí)現(xiàn)你的class文件中存在的常用類型即可。具體操作方法在上一篇中提到過,自己寫一個(gè)簡(jiǎn)單的java文件,編譯,然后用javap -verbose查看。

  • AccessFlag

訪問標(biāo)記,即當(dāng)前類是public, abstract, 還是final, interface等。注意每個(gè)標(biāo)記不是通過單獨(dú)的取值存儲(chǔ)的,而是通過一個(gè)二進(jìn)制位來標(biāo)記。例如0x0001表示public, 0x0010表示final,解析的時(shí)候需要遍歷每一個(gè)位,通過判斷是否為1來決定是否帶有此標(biāo)記。

完整的標(biāo)記位取值如下:

const (
    Public = 0x0001
    Private = 0x0002
    Protected = 0x0004
    Static = 0x0008
    Final = 0x0010
    Synchronized = 0x0020
    Bridge = 0x0040
    Varargs = 0x0080
    Native = 0x0100
    Abstarct = 0x0400
    Strict = 0x0800
    Synthetic = 0x1000
)
  • ThisClass, SuperClass

分別表示當(dāng)前類和父類在常量池中的索引。前者用于確定當(dāng)前類的全限定性名,后者用于確定父類的全限定性名。在JVM中,給定一個(gè)類的全限定性名就可以從classpath中找出這個(gè)類的class文件,繼而執(zhí)行加載邏輯。

  • InterfacesCount, Interfaces

因?yàn)镴ava類允許同時(shí)實(shí)現(xiàn)多個(gè)接口,因此這里在記錄實(shí)現(xiàn)了那些接口時(shí)就必須用一個(gè)數(shù)組來記錄了。同樣的,先是一個(gè)count表示有多少數(shù)據(jù)項(xiàng),然后是數(shù)據(jù)表本身。

  • FieldsCount, Fields

跟接口一樣,用于記錄當(dāng)前類級(jí)別的字段和實(shí)例級(jí)別的字段。在Fields的每個(gè)數(shù)據(jù)項(xiàng)中又記錄了實(shí)例名、類型、修飾符(如private, final)信息。

  • MethodCount, Methods

用于記錄方法信息。同理,每一個(gè)Methods數(shù)據(jù)項(xiàng)都會(huì)詳細(xì)記錄方法的所有屬性。

  • AttrCount, Attrs

屬性表集合,用于記錄一些附加信息。注意屬性表可以出現(xiàn)在class里,也可以在method, field中出現(xiàn),出現(xiàn)在哪就表名記錄的是哪一個(gè)層級(jí)的屬性。屬性表跟常量池一樣,每個(gè)數(shù)據(jù)項(xiàng)都有不同的類型,而且截至Java12,數(shù)據(jù)項(xiàng)的類型數(shù)量已經(jīng)高達(dá)29種,可以說非常復(fù)雜了。每中數(shù)據(jù)項(xiàng)都遵循著先是一個(gè)屬性名,再跟一個(gè)屬性數(shù)據(jù)的長(zhǎng)度(以字節(jié)為單位),然后是屬性本身。我們常說的字節(jié)碼,就是保存在Method中的Code屬性里的,定義如下:

// code屬性
type CodeAttr struct {
  AttrNameIndex uint16
    AttrLength uint32

    MaxStack uint16
    MaxLocals uint16

    // 字節(jié)碼長(zhǎng)度
    CodeLength uint32
    Code []byte

    // 異常表
    ExceptionTableLength uint16
    ExceptionTable []*ExceptionTable

    AttrCount uint16
    Attrs []interface{}
}

注意第一個(gè)字段,AttrNameIndex是一個(gè)16位的無符號(hào)整數(shù),保存的是一個(gè)常量池?cái)?shù)組下標(biāo),而下標(biāo)所保存的常量池?cái)?shù)據(jù)項(xiàng)類型就是一個(gè)UTF8字符串,在這里就是Code這個(gè)固定值。

下面的幾項(xiàng)分別保存了操作數(shù)棧最大深度、本地變量表最大長(zhǎng)度、字節(jié)碼長(zhǎng)度、字節(jié)碼本身、異常信息,另外最后還有屬性信息,套娃。我們以后實(shí)現(xiàn)解釋器主要就是要找到method中的Code屬性的Code字段,然后一條條的解釋字節(jié)碼。

?

以上就是class文件結(jié)構(gòu)的全部?jī)?nèi)容了,說實(shí)在的,非常復(fù)雜,解析的時(shí)候也會(huì)比較痛苦。但還是那句話,不需要全部都解析出來,只需要解析需要的那部分即可。對(duì)于每一個(gè)具體的數(shù)據(jù)類型的含義,在后面實(shí)現(xiàn)解釋器時(shí)用到了再解釋,這里不羅列了。筆者已經(jīng)實(shí)現(xiàn)了對(duì)class文件的解析邏輯,可以參考下面的地址:https://github.com/wanghongfei/mini-jvm/blob/master/vm/class/jclass.go

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容