Golang实现JAVA虚拟机-指令集和解释器
原文鏈接:https://gaoyubo.cn/blogs/f57f32cf.html
前置
Golang實現(xiàn)JAVA虛擬機-解析class文件
Golang實現(xiàn)JAVA虛擬機-運行時數(shù)據(jù)區(qū)
一、字節(jié)碼、class文件、指令集的關(guān)系
class文件(二進制)和字節(jié)碼(十六進制)的關(guān)系
class文件
-
經(jīng)過編譯器編譯后的文件(如javac),一個class文件代表一個類或者接口;
-
是由字節(jié)碼組成的,主要存儲的是字節(jié)碼,字節(jié)碼是訪問jvm的重要指令
-
文件本身是2進制,對應(yīng)的是16進制的數(shù)。
字節(jié)碼
-
包括
操作碼(Opcode)和操作數(shù):操作碼是一個字節(jié) -
如果方法不是抽象的,也不是本地方法,方法的Java代碼就會被編譯器編譯成字節(jié)碼,存放在method_info結(jié)構(gòu)的Code屬性中
如圖:操作碼為B2,助記符為助記符是getstatic。它的操作數(shù)是0x0002,代表常量池里的第二個常量。
操作數(shù)棧和局部變量表只存放數(shù)據(jù)的值, 并不記錄數(shù)據(jù)類型。結(jié)果就是:指令必須知道自己在操作什么類型的數(shù)據(jù)。
這一點也直接反映在了操作碼的助記符上。
例如,iadd指令:對int值進行加法操作;
dstore指令:把操作數(shù)棧頂?shù)膁ouble值彈出,存儲到局部變量表中;
areturn:從方法中返回引用值。
助記符
如果某類指令可以操作不同類型的變量,則助記符的第一個字母表示變量類型。助記符首字母和變量類型的對應(yīng)關(guān)系如下:
指令分類
Java虛擬機規(guī)范把已經(jīng)定義的205條指令按用途分成了11類, 分別是:
- 常量(constants)指令
- 加載(loads)指令
- 存儲(stores)指令
- 操作數(shù)棧(stack)指令
- 數(shù)學(xué)(math)指令
- 轉(zhuǎn)換(conversions)指令
- 比較(comparisons)指令
- 控制(control)指令
- 引用(references)指令
- 擴展(extended)指令
- 保留(reserved)指令:
- 操作碼:202(0xCA),助記符:breakpoint,用于調(diào)試器的斷點調(diào)試
- 254(0xFE),助記符:impdep1
- 266(0xFF),助記符:impdep2
- 這三條指令不允許出現(xiàn)在class文件中
本章將要實現(xiàn)的指令涉及11類中的9類
二、JVM執(zhí)行引擎
執(zhí)行引擎是Java虛擬機四大組成部分中一個核心組成(另外三個分別是類加載器子系統(tǒng)、運行時數(shù)據(jù)區(qū)、垃圾回收器),
Java虛擬機的執(zhí)行引擎主要是用來執(zhí)行Java字節(jié)碼。
它有兩種主要執(zhí)行方式:通過字節(jié)碼解釋器執(zhí)行,通過即時編譯器執(zhí)行
解釋和編譯
在了解字節(jié)碼解釋器和即使編譯器之前,需要先了解解釋和編譯
- 解釋是將代碼逐行或逐條指令地轉(zhuǎn)換為機器代碼并立即執(zhí)行的方式,適合實現(xiàn)跨平臺性。
- 編譯是將整個程序或代碼塊翻譯成機器代碼的方式,生成的機器代碼可反復(fù)執(zhí)行,通常更快,但不具備跨平臺性。
字節(jié)碼解釋器
字節(jié)碼解釋器將逐條解釋執(zhí)行Java字節(jié)碼指令。這意味著它會逐個讀取字節(jié)碼文件中的指令,并根據(jù)每個指令執(zhí)行相應(yīng)的操作。雖然解釋執(zhí)行相對較慢。
逐行解釋和執(zhí)行代碼。它會逐行讀取源代碼或字節(jié)碼,將每一行翻譯成計算機指令,然后立即執(zhí)行該指令。
因此具有平臺無關(guān)性,因為字節(jié)碼可以在不同的平臺上運行。
即時編譯器(Just-In-Time Compiler,JIT)
即時編譯器將字節(jié)碼編譯成本地機器代碼,然后執(zhí)行本地代碼。
這種方式更快,因為它避免了字節(jié)碼解釋的過程,但編譯需要一些時間。
即時編譯器通常會選擇性地編譯某些熱點代碼路徑,以提高性能。
解釋器規(guī)范
Java虛擬機規(guī)范的2.11節(jié)介紹了Java虛擬機解釋器的大致邏輯,如下所示:
do {
atomically calculate pc and fetch opcode at pc;
if (operands) fetch operands;
execute the action for the opcode;
} while (there is more to do);
- 從當前程序計數(shù)器(Program Counter,通常簡稱為 PC)中獲取當前要執(zhí)行的字節(jié)碼指令的地址。
- 從該地址獲取字節(jié)碼指令的操作碼(opcode),并執(zhí)行該操作碼對應(yīng)的操作。
- 如果指令需要操作數(shù)(operands),則獲取操作數(shù)。
- 執(zhí)行指令對應(yīng)的操作。
- 更新 PC,以便繼續(xù)執(zhí)行下一條字節(jié)碼指令。
- 循環(huán)執(zhí)行上述步驟,直到?jīng)]有更多的指令需要執(zhí)行。
每次循環(huán)都包含三個部分:計算pc、指令解碼、指令執(zhí)行
可以把這個邏輯用Go語言寫成一個for循環(huán),里面是個大大的switch-case語句。但這樣的話,代碼的可讀性將非常差。
所以采用另外一種方式:把指令抽象成接口,解碼和執(zhí)行邏輯寫在具體的指令實現(xiàn)中。
這樣編寫出的解釋器就和Java虛擬機規(guī)范里的偽代碼一樣簡單,偽代碼如下:
for {
pc := calculatePC()
opcode := bytecode[pc]
inst := createInst(opcode)
inst.fetchOperands(bytecode)
inst.execute()
}
三、指令和指令解碼
本節(jié)先定義指令接口,然后定義一個結(jié)構(gòu)體用來輔助指令解碼
Instruction接口
為了便于管理,把每種指令的源文件都放在各自的包里,所有指令都共用的代碼則放在base包里。
因此instructions目錄下會有如下10個子目錄:
base目錄下創(chuàng)建instruction.go文件,在其中定義Instruction接口,代碼如下:
type Instruction interface {
FetchOperands(reader *BytecodeReader)
Execute(frame *rtda.Frame)
}
FetchOperands()方法從字節(jié)碼中提取操作數(shù),Execute()方法執(zhí)行指令邏輯。
有很多指令的操作數(shù)都是類似的。為了避免重復(fù)代碼,按照操作數(shù)類型定義一些結(jié)構(gòu)體,并實現(xiàn)FetchOperands()方 法。
無操作數(shù)指令
在instruction.go文件中定義NoOperandsInstruction結(jié)構(gòu)體,代碼如下:
type NoOperandsInstruction struct {}
NoOperandsInstruction表示沒有操作數(shù)的指令,所以沒有定義 任何字段。FetchOperands()方法自然也是空空如也,什么也不用 讀,代碼如下:
func (self *NoOperandsInstruction) FetchOperands(reader *BytecodeReader) {
// nothing to do
}
跳轉(zhuǎn)指令
定義BranchInstruction結(jié)構(gòu)體,代碼如下:
type BranchInstruction struct {
//偏移量
Offset int
}
BranchInstruction表示跳轉(zhuǎn)指令,Offset字段存放跳轉(zhuǎn)偏移量。
FetchOperands()方法從字節(jié)碼中讀取一個uint16整數(shù),轉(zhuǎn)成int后賦給Offset字段。代碼如下:
func (self *BranchInstruction) FetchOperands(reader *BytecodeReader) {
self.Offset = int(reader.ReadInt16())
}
存儲和加載指令
存儲和加載類指令需要根據(jù)索引存取局部變量表,索引由單字節(jié)操作數(shù)給出。把這類指令抽象成Index8Instruction結(jié)構(gòu)體,定義Index8Instruction結(jié)構(gòu)體,代碼如下:
type Index8Instruction struct {
//索引
Index uint
}
FetchOperands()方法從字節(jié)碼中讀取一個int8整數(shù),轉(zhuǎn)成uint后賦給Index字段。代碼如下:
func (self *Index8Instruction) FetchOperands(reader *BytecodeReader) {
self.Index = uint(reader.ReadUint8())
}
訪問常量池的指令
有一些指令需要訪問運行時常量池,常量池索引由兩字節(jié)操作數(shù)給出,用Index字段表示常量池索引。定義Index16Instruction結(jié)構(gòu)體,代碼如下:
type Index16Instruction struct {
Index uint
}
FetchOperands()方法從字節(jié)碼中讀取一個 uint16整數(shù),轉(zhuǎn)成uint后賦給Index字段。代碼如下
func (self *Index16Instruction) FetchOperands(reader *BytecodeReader) {
self.Index = uint(reader.ReadUint16())
}
指令接口和“抽象”指令定義好了,下面來看BytecodeReader結(jié)構(gòu)體
BytecodeReader結(jié)構(gòu)體
base目錄下創(chuàng)建bytecode_reader.go文件,在 其中定義BytecodeReader結(jié)構(gòu)體
type BytecodeReader struct {
code []byte // bytecodes
pc int
}
code字段存放字節(jié)碼,pc字段記錄讀取到了哪個字節(jié)。
為了避免每次解碼指令都新創(chuàng)建一個BytecodeReader實例,給它定義一個 Reset()方法,代碼如下:
func (self *BytecodeReader) Reset(code []byte, pc int) {
self.code = code
self.pc = pc
}
面實現(xiàn)一系列的Read()方法。首先是最簡單的ReadUint8()方法,代碼如下:
func (self *BytecodeReader) ReadUint8() uint8 {
i := self.code[self.pc]
self.pc++
return i
}
- 從
self.code字節(jié)切片中的self.pc位置讀取一個字節(jié)(8 位)的整數(shù)值。 - 然后將
self.pc的值增加1,以便下次讀取下一個字節(jié)。 - 最后,返回讀取的字節(jié)作為無符號 8 位整數(shù)
ReadInt8()方法調(diào)用ReadUint8(),然后把讀取到的值轉(zhuǎn)成int8 返回,代碼如下:
func (self *BytecodeReader) ReadInt8() int8 {
return int8(self.ReadUint8())
}
ReadUint16()連續(xù)讀取兩字節(jié)
func (self *BytecodeReader) ReadUint16() uint16 {
byte1 := uint16(self.ReadUint8())
byte2 := uint16(self.ReadUint8())
return (byte1 << 8) | byte2
}
ReadInt16()方法調(diào)用ReadUint16(),然后把讀取到的值轉(zhuǎn)成 int16返回,代碼如下:
func (self *BytecodeReader) ReadInt16() int16 {
return int16(self.ReadUint16())
}
ReadInt32()方法連續(xù)讀取4字節(jié),代碼如下:
func (self *BytecodeReader) ReadInt32() int32 {
byte1 := int32(self.ReadUint8())
byte2 := int32(self.ReadUint8())
byte3 := int32(self.ReadUint8())
byte4 := int32(self.ReadUint8())
return (byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4
}
在接下來的小節(jié)中,將按照分類依次實現(xiàn)約150條指令,占整個指令集的3/4
四、常量指令
常量指令把常量推入操作數(shù)棧頂。
常量可以來自三個地方:隱含在
操作碼里、操作數(shù)和運行時常量池。常量指令共有21條,本節(jié)實現(xiàn)其中的18條。另外3條是
ldc系列指令,用于從運行時常量池中加載常量,將在后續(xù)實現(xiàn)。
nop指令
nop指令是最簡單的一條指令,因為它什么也不做。
在\instructions\constants目錄下創(chuàng)建nop.go文件,在其中實現(xiàn)nop指令,代碼如下:
type NOP struct{ base.NoOperandsInstruction }
func (self *NOP) Execute(frame *rtda.Frame) {
// 什么也不用做
}
const系列指令
這一系列指令把隱含在操作碼中的常量值推入操作數(shù)棧頂。
constants目錄下創(chuàng)建const.go文件,在其中定義15條指令,代碼如下
type ACONST_NULL struct{ base.NoOperandsInstruction }
type DCONST_0 struct{ base.NoOperandsInstruction }
type DCONST_1 struct{ base.NoOperandsInstruction }
type FCONST_0 struct{ base.NoOperandsInstruction }
type FCONST_1 struct{ base.NoOperandsInstruction }
type FCONST_2 struct{ base.NoOperandsInstruction }
type ICONST_M1 struct{ base.NoOperandsInstruction }
type ICONST_0 struct{ base.NoOperandsInstruction }
type ICONST_1 struct{ base.NoOperandsInstruction }
type ICONST_2 struct{ base.NoOperandsInstruction }
type ICONST_3 struct{ base.NoOperandsInstruction }
type ICONST_4 struct{ base.NoOperandsInstruction }
type ICONST_5 struct{ base.NoOperandsInstruction }
type LCONST_0 struct{ base.NoOperandsInstruction }
type LCONST_1 struct{ base.NoOperandsInstruction }
以3條指令為例進行說明。aconst_null指令把null引用推入操作 數(shù)棧頂,代碼如下
func (self *ACONST_NULL) Execute(frame *rtda.Frame) {
frame.OperandStack().PushRef(nil)
}
dconst_0指令把double型0推入操作數(shù)棧頂,代碼如下
func (self *DCONST_0) Execute(frame *rtda.Frame) {
frame.OperandStack().PushDouble(0.0)
}
iconst_m1指令把int型-1推入操作數(shù)棧頂,代碼如下:
func (self *ICONST_M1) Execute(frame *rtda.Frame) {
frame.OperandStack().PushInt(-1)
}
bipush和sipush指令
-
bipush指令從操作數(shù)中獲取一個byte型整數(shù),擴展成int型,然后推入棧頂。 -
sipush指令從操作數(shù)中獲取一個short型整數(shù),擴展成int型,然后推入棧頂。
constants目錄下創(chuàng)建 ipush.go文件,在其中定義bipush和sipush指令,代碼如下:
type BIPUSH struct { val int8 } // Push byte
type SIPUSH struct { val int16 } // Push short
BIPUSH結(jié)構(gòu)體實現(xiàn)方法如下:
type BIPUSH struct {
val int8
}
func (self *BIPUSH) FetchOperands(reader *base.BytecodeReader) {
self.val = reader.ReadInt8()
}
func (self *BIPUSH) Execute(frame *rtda.Frame) {
i := int32(self.val)
frame.OperandStack().PushInt(i)
}
五、加載指令
加載指令用于從局部變量表獲取變量,并將其推入操作數(shù)棧頂。總共有 33 條加載指令,它們按照所操作的變量類型可以分為 6 類:
aload系列指令:用于操作引用類型變量。dload系列指令:用于操作double類型變量。fload系列指令:用于操作float變量。iload系列指令:用于操作int變量。lload系列指令:用于操作long變量。xaload指令:用于操作數(shù)組。本節(jié)將實現(xiàn)其中的 25 條加載指令。數(shù)組和xaload系列指令先不實現(xiàn)。
loads目錄下創(chuàng)建iload.go文件,在其中定義5 條指令,代碼如下:完整代碼移步:jvmgo
// 從局部變量表加載int類型
type ILOAD struct{ base.Index8Instruction }
type ILOAD_0 struct{ base.NoOperandsInstruction }
type ILOAD_1 struct{ base.NoOperandsInstruction }
type ILOAD_2 struct{ base.NoOperandsInstruction }
type ILOAD_3 struct{ base.NoOperandsInstruction }
為了避免重復(fù)代碼,定義一個函數(shù)供iload系列指令使用,代碼如下:
func _iload(frame *rtda.Frame, index uint) {
val := frame.LocalVars().GetInt(index)
frame.OperandStack().PushInt(val)
}
iload指令的索引來自操作數(shù),其Execute()方法如下:
func (self *ILOAD) Execute(frame *rtda.Frame) {
_iload(frame, uint(self.Index))
}
其余4條指令的索引隱含在操作碼中,以iload_1為例,其 Execute()方法如下:
func (self *ILOAD_1) Execute(frame *rtda.Frame) {
_iload(frame, 1)
}
六、存儲指令
和加載指令剛好相反,存儲指令把變量從操作數(shù)棧頂彈出,然后存入局部變量表。
和加載指令一樣,存儲指令也可以分為6類。以 lstore系列指令為例進行介紹。完整代碼移步:jvmgo
instructions\stores目錄下創(chuàng)建 lstore.go文件,在其中定義5條指令,代碼如下:
type LSTORE struct{ base.Index8Instruction }
type LSTORE_0 struct{ base.NoOperandsInstruction }
type LSTORE_1 struct{ base.NoOperandsInstruction }
type LSTORE_2 struct{ base.NoOperandsInstruction }
type LSTORE_3 struct{ base.NoOperandsInstruction }
同樣定義一個函數(shù)供5條指令使用,代碼如下:
func _lstore(frame *rtda.Frame, index uint) {
val := frame.OperandStack().PopLong()
frame.LocalVars().SetLong(index, val)
}
lstore指令的索引來自操作數(shù),其Execute()方法如下:
func (self *LSTORE) Execute(frame *rtda.Frame) {
_lstore(frame, uint(self.Index))
}
其余4條指令的索引隱含在操作碼中,以lstore_2為例,其 Execute()方法如下
func (self *LSTORE_2) Execute(frame *rtda.Frame) {
_lstore(frame, 2)
}
七、棧指令
棧指令直接對操作數(shù)棧進行操作,共9條:
pop和pop2指令將棧頂變量彈出
dup系列指令復(fù)制棧頂變量
swap指令交換棧頂?shù)膬蓚€變量
和其他類型的指令不同,棧指令并不關(guān)心變量類型。為了實現(xiàn)棧指令,需要給OperandStack結(jié)構(gòu)體添加兩個方法。操作數(shù)棧實現(xiàn)
rtda\operand_stack.go文件中,在其中定義PushSlot()和PopSlot() 方法,代碼如下:
func (self *OperandStack) PushSlot(slot Slot) {
self.slots[self.size] = slot
self.size++
}
func (self *OperandStack) PopSlot() Slot {
self.size--
return self.slots[self.size]
}
pop和pop2指令
stack目錄下創(chuàng)建pop.go文件,在其中定義 pop和pop2指令,代碼如下:
type POP struct{ base.NoOperandsInstruction }
type POP2 struct{ base.NoOperandsInstruction }
pop指令把棧頂變量彈出,代碼如下:
func (self *POP) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
stack.PopSlot()
}
pop指令只能用于彈出int、float等占用一個操作數(shù)棧位置的變量。
double和long變量在操作數(shù)棧中占據(jù)兩個位置,需要使用pop2指令彈出,代碼如下:
func (self *POP2) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
stack.PopSlot()
stack.PopSlot()
}
dup指令
創(chuàng)建dup.go文件,在其中定義6 條指令,代碼如下:完整代碼移步:jvmgo
type DUP struct{ base.NoOperandsInstruction }
type DUP_X1 struct{ base.NoOperandsInstruction }
type DUP_X2 struct{ base.NoOperandsInstruction }
type DUP2 struct{ base.NoOperandsInstruction }
type DUP2_X1 struct{ base.NoOperandsInstruction }
type DUP2_X2 struct{ base.NoOperandsInstruction }
dup指令復(fù)制棧頂?shù)膯蝹€變量,代碼如下:
func (self *DUP) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
slot := stack.PopSlot()
stack.PushSlot(slot)
stack.PushSlot(slot)
}
DUP_X1 :復(fù)制棧頂操作數(shù)一份放在第二個操作數(shù)的下方。Execute代碼如下:
/*
bottom -> top
[...][c][b][a]
__/
|
V
[...][c][a][b][a]
*/
func (self *DUP_X1) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
slot1 := stack.PopSlot()
slot2 := stack.PopSlot()
stack.PushSlot(slot1)
stack.PushSlot(slot2)
stack.PushSlot(slot1)
}
DUP_X2 :復(fù)制棧頂操作數(shù)棧的一個或兩個值,并將它們插入到操作數(shù)棧中的第三個值的下面。
/*
bottom -> top
[...][c][b][a]
_____/
|
V
[...][a][c][b][a]
*/
func (self *DUP_X2) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
slot1 := stack.PopSlot()
slot2 := stack.PopSlot()
slot3 := stack.PopSlot()
stack.PushSlot(slot1)
stack.PushSlot(slot3)
stack.PushSlot(slot2)
stack.PushSlot(slot1)
}
swap指令
swap指令作用是交換棧頂?shù)膬蓚€操作數(shù)
下創(chuàng)建swap.go文件,在其中定義swap指令,代碼如下:
type SWAP struct{ base.NoOperandsInstruction }
Execute()方法如下
func (self *SWAP) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
slot1 := stack.PopSlot()
slot2 := stack.PopSlot()
stack.PushSlot(slot1)
stack.PushSlot(slot2)
}
八、數(shù)學(xué)指令
數(shù)學(xué)指令大致對應(yīng)Java語言中的加、減、乘、除等數(shù)學(xué)運算符。
數(shù)學(xué)指令包括算術(shù)指令、位移指令和布爾運算指令等,共37條,將全部在本節(jié)實現(xiàn)。
算術(shù)指令
算術(shù)指令又可以進一步分為:
- 加法(add)指令
- 減法(sub)指令
- 乘法(mul)指令
- 除法(div)指令
- 求余(rem)指令
- 取反(neg)指令
加、減、乘、除和取反指令都比較簡單,本節(jié)以復(fù)雜的求余指令介紹。
math目錄下創(chuàng)建rem.go文件,在其中定義4條求余指令,代碼如下:
type DREM struct{ base.NoOperandsInstruction }
type FREM struct{ base.NoOperandsInstruction }
type IREM struct{ base.NoOperandsInstruction }
type LREM struct{ base.NoOperandsInstruction }
-
DREM結(jié)構(gòu)體:表示對雙精度浮點數(shù) (double) 執(zhí)行取余操作。 -
FREM結(jié)構(gòu)體:表示對單精度浮點數(shù) (float) 執(zhí)行取余操作 -
IREM結(jié)構(gòu)體:表示對整數(shù) (int) 執(zhí)行取余操作。 -
LREM結(jié)構(gòu)體:表示對長整數(shù) (long) 執(zhí)行取余操作。
irem和lrem代碼差不多,以irem為例,其Execute()方法如下:
func (self *IREM) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
v2 := stack.PopInt()
v1 := stack.PopInt()
if v2 == 0 {
panic("java.lang.ArithmeticException: / by zero")
}
result := v1 % v2
stack.PushInt(result)
}
先從操作數(shù)棧中彈出兩個int變量,求余,然后把結(jié)果推入操作 數(shù)棧。
注意!對int或long變量做除法和求余運算時,是有可能拋出ArithmeticException異常的。
frem和drem指令差不多,以 drem為例,其Execute()方法如下:
func (self *DREM) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
v2 := stack.PopDouble()
v1 := stack.PopDouble()
result := math.Mod(v1, v2)
stack.PushDouble(result)
}
Go語言沒有給浮點數(shù)類型定義求余操作符,所以需要使用 math包的Mod()函數(shù)。
浮點數(shù)類型因為有Infinity(無窮大)值,所以即使是除零,也不會導(dǎo)致ArithmeticException異常拋出
位移指令
分為左移和右移
- 左移
- 右移
- 算術(shù)右移(有符號右移)
- 邏輯右移(無符號右移)兩種。
算術(shù)右移和邏 輯位移的區(qū)別僅在于符號位的擴展,如下面的Java代碼所示。
int x = -1;
println(Integer.toBinaryString(x)); // 11111111111111111111111111111111
println(Integer.toBinaryString(x >> 8)); // 11111111111111111111111111111111
println(Integer.toBinaryString(x >>> 8)); // 00000000111111111111111111111111
math目錄下創(chuàng)建sh.go文件,在其中定義6條 位移指令,代碼如下
type ISHL struct{ base.NoOperandsInstruction } // int左位移
type ISHR struct{ base.NoOperandsInstruction } // int算術(shù)右位移
type IUSHR struct{ base.NoOperandsInstruction } // int邏輯右位移(無符號右移位)
type LSHL struct{ base.NoOperandsInstruction } // long左位移
type LSHR struct{ base.NoOperandsInstruction } // long算術(shù)右位移
type LUSHR struct{ base.NoOperandsInstruction } // long邏輯右移位(無符號右移位)
左移
左移指令比較簡單,以ishl指令為例,其Execute()方法如下:
func (self *ISHL) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
v2 := stack.PopInt()
v1 := stack.PopInt()
s := uint32(v2) & 0x1f
result := v1 << s
stack.PushInt(result)
}
先從操作數(shù)棧中彈出兩個int變量v2和v1。v1是要進行位移操作的變量,v2指出要移位多少比特。位移之后,把結(jié)果推入操作數(shù)棧。
s := uint32(v2) & 0x1f:這行代碼將被左移的位數(shù)v2強制轉(zhuǎn)換為uint32類型,然后執(zhí)行按位與操作(&)與常數(shù)0x1f。
這是為了確保左移的位數(shù)在范圍 0 到 31 內(nèi),因為在 Java 中,左移操作最多只能左移 31 位,超出這個范圍的位數(shù)將被忽略。
這里注意兩點:
int變量只有32位,所以只取v2的前5個比特就 足夠表示位移位數(shù)了
Go語言位移操作符右側(cè)必須是無符號 整數(shù),所以需要對v2進行類型轉(zhuǎn)換
右移
算數(shù)右移
算術(shù)右移指令需要擴展符號位,代碼和左移指令基本上差不多。以lshr指令為例,其Execute()方法如下:
func (self *LSHR) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
v2 := stack.PopInt()
//long變量有64位,所以取v2的前6個比特。
v1 := stack.PopLong()
s := uint32(v2) & 0x3f
result := v1 >> s
stack.PushLong(result)
}
s := uint32(v2) & 0x1f:
提取
v2變量的最低的 6 位,將其他位設(shè)置為 0,并將結(jié)果存儲在s變量中。這是為了限制右移的位數(shù)在 0 到 63 之間,因為在 Java 中,long類型右移操作最多只能右移 63 位
邏輯右移
無符號右移位,以iushr為例,在移位前,先將v2轉(zhuǎn)化為正數(shù),再進行移位,最后轉(zhuǎn)化為int32類型,如下代碼所示:
func (self *IUSHR) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
v2 := stack.PopInt()
v1 := stack.PopInt()
s := uint32(v2) & 0x1f
result := int32(uint32(v1) >> s)
stack.PushInt(result)
}
布爾運算指令
布爾運算指令只能操作int和long變量,分為:
- 按位與(and)
- 按位 或(or)
- 按位異或(xor)
math目錄下創(chuàng)建and.go文件,在其中定義iand和 land指令,代碼如下:
type IAND struct{ base.NoOperandsInstruction }
type LAND struct{ base.NoOperandsInstruction }
以iand指令為例,其Execute()方法如下:
func (self *IAND) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
v2 := stack.PopInt()
v1 := stack.PopInt()
result := v1 & v2
stack.PushInt(result)
}
iinc指令
iinc指令給局部變量表中的int變量增加常量值,局部變量表索引和常量值都由指令的操作數(shù)提供。
math目錄下創(chuàng)建iinc.go文件,在其中定義iinc指令,代碼如下:
type IINC struct {
//索引
Index uint
//常量值
Const int32
}
index:一個字節(jié),表示局部變量表中要增加值的變量的索引。這個索引指定了要修改的局部變量。const:一個有符號字節(jié),表示要增加的常數(shù)值。這個常數(shù)值將與局部變量的當前值相加,并將結(jié)果存儲回同一個局部變量。
FetchOperands()函數(shù)從字節(jié)碼里讀取操作數(shù),代碼如下:
func (self *IINC) FetchOperands(reader *base.BytecodeReader) {
self.Index = uint(reader.ReadUint8())
self.Const = int32(reader.ReadInt8())
}
Execute()方法從局部變量表中讀取變量,給它加上常量值,再把結(jié)果寫回局部變量表,代碼如下
func (self *IINC) Execute(frame *rtda.Frame) {
localVars := frame.LocalVars()
val := localVars.GetInt(self.Index)
val += self.Const
localVars.SetInt(self.Index, val)
}
九、類型轉(zhuǎn)換指令
類型轉(zhuǎn)換指令大致對應(yīng)Java語言中的基本類型強制轉(zhuǎn)換操作。 類型轉(zhuǎn)換指令有共15條,將全部在本節(jié)實現(xiàn)。
引用類型轉(zhuǎn)換對應(yīng)的是checkcast指令,將在后續(xù)完成。
類型轉(zhuǎn)換指令根據(jù)被
轉(zhuǎn)換變量的類型分為四種系列:
- i2x 系列指令:這些指令將整數(shù)(int)變量強制轉(zhuǎn)換為其他類型。
- l2x 系列指令:這些指令將長整數(shù)(long)變量強制轉(zhuǎn)換為其他類型。
- f2x 系列指令:這些指令將浮點數(shù)(float)變量強制轉(zhuǎn)換為其他類型。
- d2x 系列指令:這些指令將雙精度浮點數(shù)(double)變量強制轉(zhuǎn)換為其他類型。
這些類型轉(zhuǎn)換指令允許將不同類型的數(shù)據(jù)進行強制類型轉(zhuǎn)換,以滿足特定的計算或操作需求。
以d2x系列指令為例進行討論。
conversions目錄下創(chuàng)建d2x.go文件,在其中 定義d2f、d2i和d2l指令,代碼如下
type D2F struct{ base.NoOperandsInstruction }
type D2I struct{ base.NoOperandsInstruction }
type D2L struct{ base.NoOperandsInstruction }
以d2i指令為例,它的Execute()方法如下:
func (self *D2I) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
d := stack.PopDouble()
i := int32(d)
stack.PushInt(i)
}
因為Go語言可以很方便地轉(zhuǎn)換各種基本類型的變量,所以類型轉(zhuǎn)換指令實現(xiàn)起來還是比較容易的。
十、比較指令
比較指令可以分為兩類:
- 將比較結(jié)果推入操作數(shù)棧頂
- 根據(jù)比較結(jié)果跳轉(zhuǎn)
比較指令是編譯器實現(xiàn)if-else、for、while等語句的基石,共有19條
lcmp指令
lcmp指令用于比較long變量。
comparisons目錄下創(chuàng)建lcmp.go文件,在其中定義lcmp指令,代碼如下:
type LCMP struct{ base.NoOperandsInstruction }
Execute()方法把棧頂?shù)膬蓚€long變量彈出,進行比較,然后把比較結(jié)果(int型0、1或-1)推入棧頂,代碼如下:
func (self *LCMP) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
v2 := stack.PopLong()
v1 := stack.PopLong()
if v1 > v2 {
stack.PushInt(1)
} else if v1 == v2 {
stack.PushInt(0)
} else {
stack.PushInt(-1)
}
}
fcmp和dcmp指令
fcmpg和fcmpl指令用于比較float變量,它們的區(qū)別是對于非數(shù)字參與,fcmpg會默認為其大于任何非NaN值,fcmpl則相反。
comparisons目錄下創(chuàng)建fcmp.go文件,在其中定義 fcmpg和fcmpl指令,代碼如下:
type FCMPG struct{ base.NoOperandsInstruction }
type FCMPL struct{ base.NoOperandsInstruction }
由于浮點數(shù)計算有可能產(chǎn)生NaN(Not a Number)值,所以比較兩個浮點數(shù)時,除了大于、等于、小于之外,
還有第4種結(jié)果:無法比較。
編寫一個函數(shù)來統(tǒng)一比較float變量,如下:
func _fcmp(frame *rtda.Frame, gFlag bool) {
stack := frame.OperandStack()
v2 := stack.PopFloat()
v1 := stack.PopFloat()
if v1 > v2 {
stack.PushInt(1)
} else if v1 == v2 {
stack.PushInt(0)
} else if v1 < v2 {
stack.PushInt(-1)
} else if gFlag {
stack.PushInt(1)
} else {
stack.PushInt(-1)
}
}
Java虛擬機規(guī)范:浮點數(shù)比較指令
fcmpl和fcmpg的規(guī)范要求首先彈出v2,然后是v1,以便進行浮點數(shù)比較。
Execute()如下:
func (self *FCMPG) Execute(frame *rtda.Frame) {
_fcmp(frame, true)
}
func (self *FCMPL) Execute(frame *rtda.Frame) {
_fcmp(frame, false)
}
if<cond>指令
if<cond> 指令是 Java 字節(jié)碼中的條件分支指令,它根據(jù)條件 <cond> 來執(zhí)行不同的分支。
條件 <cond> 可以是各種比較操作,比如等于、不等于、大于、小于等等。
常見的
if<cond>指令包括:
ifeq: 如果棧頂?shù)闹档扔?,則跳轉(zhuǎn)。ifne: 如果棧頂?shù)闹挡坏扔?,則跳轉(zhuǎn)。iflt: 如果棧頂?shù)闹敌∮?,則跳轉(zhuǎn)。ifge: 如果棧頂?shù)闹荡笥诨虻扔?,則跳轉(zhuǎn)。ifgt: 如果棧頂?shù)闹荡笥?,則跳轉(zhuǎn)。ifle: 如果棧頂?shù)闹敌∮诨虻扔?,則跳轉(zhuǎn)。
創(chuàng)建ifcond.go文件,在其中定義6條if指令,代碼如下:
type IFEQ struct{ base.BranchInstruction }
type IFNE struct{ base.BranchInstruction }
type IFLT struct{ base.BranchInstruction }
type IFLE struct{ base.BranchInstruction }
type IFGT struct{ base.BranchInstruction }
type IFGE struct{ base.BranchInstruction }
以ifeq指令為例,其Execute()方法如下:
func (self *IFEQ) Execute(frame *rtda.Frame) {
val := frame.OperandStack().PopInt()
if val == 0 {
base.Branch(frame, self.Offset)
}
}
真正的跳轉(zhuǎn)邏輯在Branch()函數(shù)中。因為這個函數(shù)在很多指令中都會用到,所以定義在base\branch_logic.go 文件中,代碼如下:
func Branch(frame *rtda.Frame, offset int) {
pc := frame.Thread().PC()
nextPC := pc + offset
frame.SetNextPC(nextPC)
}
if_icmp<cond>指令
if_icmp<cond> 指令是 Java 字節(jié)碼中的一類條件分支指令,它用于對比兩個整數(shù)值,根據(jù)比較的結(jié)果來執(zhí)行條件分支。這些指令的操作數(shù)棧上通常有兩個整數(shù)值,它們分別用于比較。
這類指令包括:
if_icmpeq: 如果兩個整數(shù)相等,則跳轉(zhuǎn)。if_icmpne: 如果兩個整數(shù)不相等,則跳轉(zhuǎn)。if_icmplt: 如果第一個整數(shù)小于第二個整數(shù),則跳轉(zhuǎn)。if_icmpge: 如果第一個整數(shù)大于等于第二個整數(shù),則跳轉(zhuǎn)。if_icmpgt: 如果第一個整數(shù)大于第二個整數(shù),則跳轉(zhuǎn)。if_icmple: 如果第一個整數(shù)小于等于第二個整數(shù),則跳轉(zhuǎn)。
創(chuàng)建if_icmp.go文件,在 其中定義6條if_icmp指令,代碼如下:
type IF_ICMPEQ struct{ base.BranchInstruction }
type IF_ICMPNE struct{ base.BranchInstruction }
type IF_ICMPLT struct{ base.BranchInstruction }
type IF_ICMPLE struct{ base.BranchInstruction }
type IF_ICMPGT struct{ base.BranchInstruction }
type IF_ICMPGE struct{ base.BranchInstruction }
以if_icmpne指令 為例,其Execute()方法如下:
func (self *IF_ICMPNE) Execute(frame *rtda.Frame) {
if val1, val2 := _icmpPop(frame); val1 != val2 {
base.Branch(frame, self.Offset)
}
}
func _icmpPop(frame *rtda.Frame) (val1, val2 int32) {
stack := frame.OperandStack()
val2 = stack.PopInt()
val1 = stack.PopInt()
return
}
if_acmp<cond>指令
if_acmp<cond> 指令是 Java 字節(jié)碼中的一類條件分支指令,用于比較兩個引用類型的對象引用,根據(jù)比較的結(jié)果來執(zhí)行條件分支。這些指令的操作數(shù)棧上通常有兩個對象引用,它們分別用于比較。
這類指令包括:
if_acmpeq: 如果兩個引用相等,則跳轉(zhuǎn)。if_acmpne: 如果兩個引用不相等,則跳轉(zhuǎn)。
創(chuàng)建if_acmp.go文件,在 其中定義兩條if_acmp指令,代碼如下:
type IF_ACMPEQ struct{ base.BranchInstruction }
type IF_ACMPNE struct{ base.BranchInstruction }
以if_acmpeq指令為例,其Execute()方法如下:
func (self *IF_ACMPEQ) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
ref2 := stack.PopRef()
ref1 := stack.PopRef()
if ref1 == ref2 {
base.Branch(frame, self.Offset)
}
}
十一、控制指令
- 控制指令共有 11 條。
- 在 Java 6 之前,
。從 Java 6 開始,Oracle 的 Java 編譯器不再使用這兩條指令。jsr和ret指令用于實現(xiàn)finally子句 -
return系列指令有 6 條,用于從方法調(diào)用中返回,將在后續(xù)實現(xiàn)。 - 本節(jié)將實現(xiàn)剩下的 3 條指令:
goto、tableswitch和lookupswitch。
這些指令用于控制程序執(zhí)行流,包括條件分支和無條件跳轉(zhuǎn)等操作。其中,goto 用于無條件跳轉(zhuǎn)到指定的目標位置,而 tableswitch 和 lookupswitch 用于根據(jù)條件跳轉(zhuǎn)到不同的目標位置。
control目錄下創(chuàng)建goto.go文件,在其中定義 goto指令,代碼如下:
總結(jié)
以上是生活随笔為你收集整理的Golang实现JAVA虚拟机-指令集和解释器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一文了解Vprix容器流媒体平台和传统云
- 下一篇: Prometheus 监控告警系统搭建(