如何實現一個Java Class位元組解析器
最近在寫一個私人專案,名字叫做SmallVM,SmallVM
的目的在於通過實現一個輕量級的Java
虛擬機器,加深對Java
虛擬機器的認知和理解。在Java
虛擬機器載入類的過程中,需要對Class
檔案進行解析,我曾經單獨實現過一個Java
版的Class
位元組解析器ClassAnalyzer,相比於Java
版,新版(Golang
版)更加健壯,思路也更加清晰。本文即闡述我實現Class
位元組解析器的思路。
Class 檔案
作為類或者介面資訊的載體,每個Class
檔案都完整的定義了一個類。為了使Java
程式可以 “編寫一次,處處執行”,Java 虛擬機器規範對Class
檔案進行了嚴格的規定。構成Class
檔案的基本資料單位是位元組,這些位元組之間不存在任何分隔符,這使得整個Class
檔案中儲存的內容幾乎全部是程式執行的必要資料,單個位元組無法表示的資料由多個連續的位元組來表示。
根據Java
虛擬機器規範,Class
檔案採用一種類似於C
語言結構體的偽結構來儲存資料,這種偽結構中只有兩種資料型別:無符號數和表。Java
虛擬機器規範定義了u1
、u2
、u4
和u8
來分別表示1
個位元組、2
個位元組、4
個位元組和8
個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或者是字串。表是由多個無符號數或者其它表作為資料項構成的複合資料型別,表用於描述有層次關係的複合結構的資料,因此整個Class
檔案本質上就是一張表。在SmallVM
中u1
、u2
、u4
和u8
分別對應於uint8
、uint16
、uint32
和uint64
,Class
檔案被描述為如下結構體。
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_info
或CONSTANT_Double_info
結構的項在常量池中的索引為n
,則常量池中下一個有效的項的索引為n+2
,此時常量池中索引為n+1
的項有效但必須被認為不可用。 -
CONSTANT_Utf8_info
型常量的結構中包含u1
型別的tag
、u2
型別的length
和由length
個u1
型別組成的bytes
,這length
位元組的連續資料是一個使用MUTF-8
(Modified UTF-8)
編碼的字串。MUTF-8
與UTF-8
並不相容,主要區別有兩點:一是null
字元會被編碼成2
位元組(0xC0
和0x80
);二是補充字元是按照UTF-16
拆分為代理對分別編碼的,相關細節可以看這裡(變種 UTF-8)。
屬性表用於描述某些場景專有的資訊,Class
檔案、欄位表和方法表都有相應的屬性表集合。Java
虛擬機器規範定義了多種屬性,SmallVM
目前實現了對常用屬性的解析。和常量型別的資料項不同,屬性並沒有一個tag
來標識屬性的型別,但是每個屬性都包含有一個u2
型別的attribute_name_index
,attribute_name_index
指向常量池中的一個CONSTANT_Utf8_info
型別的常量,該常量包含著屬性的名稱。在解析屬性時,SmallVM
正是通過attribute_name_index
指向的常量對應的屬性名稱來得知屬性的型別。
欄位表用於描述類或者介面中宣告的變數,欄位包括類級變數以及例項級變數。欄位表的結構包含一個u2
型別的access_flags
、一個u2
型別的name_index
、一個u2
型別的descriptor_index
、一個u2
型別的attributes_count
和attributes_count
個attribute_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
屬性中,codeLength
和code
分別用於儲存位元組碼長度和位元組碼指令,每條指令即一個位元組(u1
型別)。在虛擬機器執行時,通過讀取code
中的一個個位元組碼,並將位元組碼翻譯成相應的指令。另外,雖然codeLength
是一個u4
型別的值,但是實際上一個方法不允許超過65535
條位元組碼指令。
程式碼實現
整個Class
位元組解析器的原始碼已放在了GitHub上,位元組解析器僅僅是SmallVM
的一個小模組,對應的目錄為src/classfile
。另外,可以參考ClassAnalyzer的README,我以一個類的Class
檔案為例,對該Class
檔案的每個位元組進行了分析,希望對大家的理解有所幫助。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 如何實現一個Java Class解析器Java
- 自己動手實現一個 Java Class 解析器Java
- 如何閱讀JAVA 位元組碼(一)Java
- Java Class 位元組碼檔案結構詳解Java
- JAVA——一個漢字佔用多少位元組?Java
- 使用javap分析Java位元組碼的一個例子Java
- nodejs實現一個word文件解析器NodeJS
- Class檔案結構&位元組碼指令
- Java 位元組碼Java
- 如何將一個Java檔案編譯成classJava編譯
- 從 Java 位元組碼到 ASM 實踐JavaASM
- 用 class 寫法完整實現一個 PromisePromise
- Java位元組碼指令Java
- Java陣列物件佔用多少個位元組?Java陣列物件
- Java char 型別究竟佔幾個位元組?Java型別
- Java基本型別佔用的位元組數(char佔用幾個位元組問題)Java型別
- JavaScript實現一個簡單的Markdown語法解析器JavaScript
- ES5 如何實現 Class
- [譯] 用javascript實現一門程式語言-寫一個解析器JavaScript
- JAVA動態位元組碼Java
- java 位元組陣列取反Java陣列
- 【Java】JVM位元組碼分析JavaJVM
- Go語言實現位元組記錄鎖Go
- 【原創】用Java實現按位元組長度擷取字串的方法Java字串
- 位元組面試問我如何高效設計一個LRU,當場懵面試
- 幾百行程式碼實現一個 JSON 解析器行程JSON
- 實現一個自己的語法解析器與執行引擎
- 一個C語言宣告解析器的設計與實現C語言
- 如何修改檔案中間的幾個位元組
- 位元組跳動近日申請多個“位元組遊戲”商標遊戲
- [譯] 不用 Class,如何寫一個類
- 使用java動態位元組碼技術簡單實現arthas的trace功能。Java
- Java位元組碼指令表Java
- Java 位元組的常用封裝Java封裝
- 輕鬆看懂Java位元組碼Java
- Java位元組碼忍者禁術Java
- Java IO3:位元組流Java
- Java的魔力:位元組碼(轉)Java