正確解析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