Golang實現JAVA虛擬機器-指令集和直譯器

橡皮筋兒發表於2024-01-11

原文連結:https://gaoyubo.cn/blogs/f57f32cf.html

前置

Golang實現JAVA虛擬機器-解析class檔案

Golang實現JAVA虛擬機器-執行時資料區

一、位元組碼、class檔案、指令集的關係

class檔案(二進位制)和位元組碼(十六進位制)的關係

class檔案

  • 經過編譯器編譯後的檔案(如javac),一個class檔案代表一個類或者介面;

  • 是由位元組碼組成的,主要儲存的是位元組碼,位元組碼是訪問jvm的重要指令

  • 檔案本身是2進位制,對應的是16進位制的數。

位元組碼

  • 包括操作碼(Opcode)運算元:操作碼是一個位元組

  • 如果方法不是抽象的,也不是本地方法,方法的Java程式碼就會被編譯器編譯成位元組碼,存放在method_info結構的Code屬性中

如圖:操作碼為B2,助記符為助記符是getstatic。它的運算元是0x0002,代表常量池裡的第二個常量。

運算元棧和區域性變數表只存放資料的值, 並不記錄資料型別。結果就是:指令必須知道自己在操作什麼型別的資料。

這一點也直接反映在了操作碼的助記符上。

例如,iadd指令:對int值進行加法操作;
dstore指令:把運算元棧頂的double值彈出,儲存到區域性變數表中;
areturn:從方法中返回引用值。

助記符

如果某類指令可以操作不同型別的變數,則助記符的第一個字母表示變數型別。助記符首字母和變數型別的對應關係如下:

指令分類

Java虛擬機器規範把已經定義的205條指令按用途分成了11類, 分別是:

  • 常量(constants)指令
  • 載入(loads)指令
  • 儲存(stores)指令
  • 運算元棧(stack)指令
  • 數學(math)指令
  • 轉換(conversions)指令
  • 比較(comparisons)指令
  • 控制(control)指令
  • 引用(references)指令
  • 擴充套件(extended)指令
  • 保留(reserved)指令:
    • 操作碼:202(0xCA),助記符:breakpoint,用於偵錯程式的斷點除錯
    • 254(0xFE),助記符:impdep1
    • 266(0xFF),助記符:impdep2
    • 這三條指令不允許出現在class檔案中

本章將要實現的指令涉及11類中的9類

二、JVM執行引擎

執行引擎是Java虛擬機器四大組成部分中一個核心組成(另外三個分別是類載入器子系統執行時資料區垃圾回收器),

Java虛擬機器的執行引擎主要是用來執行Java位元組碼。

它有兩種主要執行方式:透過位元組碼直譯器執行,透過即時編譯器執行

解釋和編譯

在瞭解位元組碼直譯器和即使編譯器之前,需要先了解解釋編譯

  • 解釋是將程式碼逐行或逐條指令地轉換為機器程式碼並立即執行的方式,適合實現跨平臺性。
  • 編譯是將整個程式或程式碼塊翻譯成機器程式碼的方式,生成的機器程式碼可反覆執行,通常更快,但不具備跨平臺性。

位元組碼直譯器

位元組碼直譯器將逐條解釋執行Java位元組碼指令。這意味著它會逐個讀取位元組碼檔案中的指令,並根據每個指令執行相應的操作。雖然解釋執行相對較慢。

逐行解釋和執行程式碼。它會逐行讀取原始碼或位元組碼,將每一行翻譯成計算機指令,然後立即執行該指令。

因此具有平臺無關性,因為位元組碼可以在不同的平臺上執行。

即時編譯器(Just-In-Time Compiler,JIT)

即時編譯器將位元組碼編譯成本地機器程式碼,然後執行原生程式碼。

這種方式更快,因為它避免了位元組碼解釋的過程,但編譯需要一些時間。

即時編譯器通常會選擇性地編譯某些熱點程式碼路徑,以提高效能。

直譯器規範

Java虛擬機器規範的2.11節介紹了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);
  1. 從當前程式計數器(Program Counter,通常簡稱為 PC)中獲取當前要執行的位元組碼指令的地址。
  2. 從該地址獲取位元組碼指令的操作碼(opcode),並執行該操作碼對應的操作。
  3. 如果指令需要運算元(operands),則獲取運算元。
  4. 執行指令對應的操作。
  5. 更新 PC,以便繼續執行下一條位元組碼指令。
  6. 迴圈執行上述步驟,直到沒有更多的指令需要執行。

每次迴圈都包含三個部分:計算pc、指令解碼、指令執行

可以把這個邏輯用Go語言寫成一個for迴圈,裡面是個大大的switch-case語句。但這樣的話,程式碼的可讀性將非常差。

所以採用另外一種方式:把指令抽象成介面,解碼和執行邏輯寫在具體的指令實現中。

這樣編寫出的直譯器就和Java虛擬機器規範裡的虛擬碼一樣簡單,虛擬碼如下:

for {
    pc := calculatePC()
    opcode := bytecode[pc]
    inst := createInst(opcode)
    inst.fetchOperands(bytecode)
    inst.execute()
}

三、指令和指令解碼

本節先定義指令介面,然後定義一個結構體用來輔助指令解碼

Instruction介面

為了便於管理,把每種指令的原始檔都放在各自的包裡,所有指令都共用的程式碼則放在base包裡。

因此instructions目錄下會有如下10個子目錄:

base目錄下建立instruction.go檔案,在其中定義Instruction介面,程式碼如下:

type Instruction interface {
    FetchOperands(reader *BytecodeReader)
    Execute(frame *rtda.Frame)
}

FetchOperands()方法從位元組碼中提取運算元,Execute()方法執行指令邏輯。

有很多指令的運算元都是類似的。為了避免重複程式碼,按照運算元型別定義一些結構體,並實現FetchOperands()方 法。

無運算元指令

instruction.go檔案中定義NoOperandsInstruction結構體,程式碼如下:

type NoOperandsInstruction struct {}

NoOperandsInstruction表示沒有運算元的指令,所以沒有定義 任何欄位。FetchOperands()方法自然也是空空如也,什麼也不用 讀,程式碼如下:

func (self *NoOperandsInstruction) FetchOperands(reader *BytecodeReader) {
	// nothing to do
}

跳轉指令

定義BranchInstruction結構體,程式碼如下:

type BranchInstruction struct {
    //偏移量
	Offset int
}

BranchInstruction表示跳轉指令,Offset欄位存放跳轉偏移量。

FetchOperands()方法從位元組碼中讀取一個uint16整數,轉成int後賦給Offset欄位。程式碼如下:

func (self *BranchInstruction) FetchOperands(reader *BytecodeReader) {
	self.Offset = int(reader.ReadInt16())
}

儲存和載入指令

儲存和載入類指令需要根據索引存取區域性變數表,索引由單位元組運算元給出。把這類指令抽象成Index8Instruction結構體,定義Index8Instruction結構體,程式碼如下:

type Index8Instruction struct {
    //索引
    Index uint
}

FetchOperands()方法從位元組碼中讀取一個int8整數,轉成uint後賦給Index欄位。程式碼如下:

func (self *Index8Instruction) FetchOperands(reader *BytecodeReader) {
	self.Index = uint(reader.ReadUint8())
}

訪問常量池的指令

有一些指令需要訪問執行時常量池,常量池索引由兩位元組運算元給出,用Index欄位表示常量池索引。定義Index16Instruction結構體,程式碼如下:

type Index16Instruction struct {
	Index uint
}

FetchOperands()方法從位元組碼中讀取一個 uint16整數,轉成uint後賦給Index欄位。程式碼如下

func (self *Index16Instruction) FetchOperands(reader *BytecodeReader) {
    self.Index = uint(reader.ReadUint16())
}

指令介面和“抽象”指令定義好了,下面來看BytecodeReader結構體

BytecodeReader結構體

base目錄下建立bytecode_reader.go檔案,在 其中定義BytecodeReader結構體

type BytecodeReader struct {
    code []byte // bytecodes
    pc   int
}

code欄位存放位元組碼,pc欄位記錄讀取到了哪個位元組。

為了避免每次解碼指令都新建立一個BytecodeReader例項,給它定義一個 Reset()方法,程式碼如下:

func (self *BytecodeReader) Reset(code []byte, pc int) {
    self.code = code
    self.pc = pc
}

面實現一系列的Read()方法。首先是最簡單的ReadUint8()方法,程式碼如下:

func (self *BytecodeReader) ReadUint8() uint8 {
    i := self.code[self.pc]
    self.pc++
    return i
}
  • self.code 位元組切片中的 self.pc 位置讀取一個位元組(8 位)的整數值。
  • 然後將 self.pc 的值增加1,以便下次讀取下一個位元組。
  • 最後,返回讀取的位元組作為無符號 8 位整數

ReadInt8()方法呼叫ReadUint8(),然後把讀取到的值轉成int8 返回,程式碼如下:

func (self *BytecodeReader) ReadInt8() int8 {
	return int8(self.ReadUint8())
}

ReadUint16()連續讀取兩位元組

func (self *BytecodeReader) ReadUint16() uint16 {
    byte1 := uint16(self.ReadUint8())
    byte2 := uint16(self.ReadUint8())
    return (byte1 << 8) | byte2
}

ReadInt16()方法呼叫ReadUint16(),然後把讀取到的值轉成 int16返回,程式碼如下:

func (self *BytecodeReader) ReadInt16() int16 {
	return int16(self.ReadUint16())
}

ReadInt32()方法連續讀取4位元組,程式碼如下:

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
}

在接下來的小節中,將按照分類依次實現約150條指令,佔整個指令集的3/4

四、常量指令

常量指令把常量推入運算元棧頂。

常量可以來自三個地方:隱含在操作碼裡運算元執行時常量池

常量指令共有21條,本節實現其中的18條。另外3條是ldc系列指令,用於從執行時常量池中載入常量,將在後續實現。

nop指令

nop指令是最簡單的一條指令,因為它什麼也不做。
\instructions\constants目錄下建立nop.go檔案,在其中實現nop指令,程式碼如下:

type NOP struct{ base.NoOperandsInstruction }

func (self *NOP) Execute(frame *rtda.Frame) {
// 什麼也不用做
}

const系列指令

這一系列指令把隱含在操作碼中的常量值推入運算元棧頂。

constants目錄下建立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引用推入操作 數棧頂,程式碼如下

func (self *ACONST_NULL) Execute(frame *rtda.Frame) {
	frame.OperandStack().PushRef(nil)
}

dconst_0指令把double型0推入運算元棧頂,程式碼如下

func (self *DCONST_0) Execute(frame *rtda.Frame) {
	frame.OperandStack().PushDouble(0.0)
}

iconst_m1指令把int型-1推入運算元棧頂,程式碼如下:

func (self *ICONST_M1) Execute(frame *rtda.Frame) {
	frame.OperandStack().PushInt(-1)
}

bipush和sipush指令

  • bipush指令從運算元中獲取一個byte型整數,擴充套件成int型,然後推入棧頂。
  • sipush指令從運算元中獲取一個short型整數,擴充套件成int型,然後推入棧頂。

constants目錄下建立 ipush.go檔案,在其中定義bipush和sipush指令,程式碼如下:

type BIPUSH struct { val int8 } // Push byte
type SIPUSH struct { val int16 } // Push short

BIPUSH結構體實現方法如下:

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)
}

五、載入指令

載入指令用於從區域性變數表獲取變數,並將其推入運算元棧頂。總共有 33 條載入指令,它們按照所操作的變數型別可以分為 6 類:

  1. aload 系列指令:用於操作引用型別變數。
  2. dload 系列指令:用於操作 double 型別變數。
  3. fload 系列指令:用於操作 float 變數。
  4. iload 系列指令:用於操作 int 變數。
  5. lload 系列指令:用於操作 long 變數。
  6. xaload 指令:用於運算元組。

本節將實現其中的 25 條載入指令。陣列和xaload系列指令先不實現。

loads目錄下建立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 }

為了避免重複程式碼,定義一個函式供iload系列指令使用,程式碼如下:

func _iload(frame *rtda.Frame, index uint) {
    val := frame.LocalVars().GetInt(index)
    frame.OperandStack().PushInt(val)
}

iload指令的索引來自運算元,其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)
}

六、儲存指令

和載入指令剛好相反,儲存指令把變數從運算元棧頂彈出,然後存入區域性變數表。

和載入指令一樣,儲存指令也可以分為6類。以 lstore系列指令為例進行介紹。完整程式碼移步:jvmgo

instructions\stores目錄下建立 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 }

同樣定義一個函式供5條指令使用,程式碼如下:

func _lstore(frame *rtda.Frame, index uint) {
    val := frame.OperandStack().PopLong()
    frame.LocalVars().SetLong(index, val)
}

lstore指令的索引來自運算元,其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)
}

七、棧指令

棧指令直接對運算元棧進行操作,共9條:

pop和pop2指令將棧頂變數彈出

dup系列指令複製棧頂變數

swap指令交換棧頂的兩個變數

和其他型別的指令不同,棧指令並不關心變數型別。為了實現棧指令,需要給OperandStack結構體新增兩個方法。運算元棧實現
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目錄下建立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等佔用一個運算元棧位置的變數。

double和long變數在運算元棧中佔據兩個位置,需要使用pop2指令彈出,程式碼如下:

func (self *POP2) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    stack.PopSlot()
    stack.PopSlot()
}

dup指令

建立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指令複製棧頂的單個變數,程式碼如下:

func (self *DUP) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    slot := stack.PopSlot()
    stack.PushSlot(slot)
    stack.PushSlot(slot)
}

DUP_X1 :複製棧頂運算元一份放在第二個運算元的下方。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 :複製棧頂運算元棧的一個或兩個值,並將它們插入到運算元棧中的第三個值的下面。

/*
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指令作用是交換棧頂的兩個運算元

下建立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)
}

八、數學指令

數學指令大致對應Java語言中的加、減、乘、除等數學運算子。

數學指令包括算術指令、位移指令和布林運算指令等,共37條,將全部在本節實現。

算術指令

算術指令又可以進一步分為:

  • 加法(add)指令
  • 減法(sub)指令
  • 乘法(mul)指令
  • 除法(div)指令
  • 求餘(rem)指令
  • 取反(neg)指令

加、減、乘、除和取反指令都比較簡單,本節以複雜的求餘指令介紹。

math目錄下建立rem.go檔案,在其中定義4條求餘指令,程式碼如下:

type DREM struct{ base.NoOperandsInstruction }
type FREM struct{ base.NoOperandsInstruction }
type IREM struct{ base.NoOperandsInstruction }
type LREM struct{ base.NoOperandsInstruction }
  • DREM 結構體:表示對雙精度浮點數 (double) 執行取餘操作。
  • FREM 結構體:表示對單精度浮點數 (float) 執行取餘操作
  • IREM 結構體:表示對整數 (int) 執行取餘操作。
  • LREM 結構體:表示對長整數 (long) 執行取餘操作。

iremlrem程式碼差不多,以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)
}

先從運算元棧中彈出兩個int變數,求餘,然後把結果推入操作 數棧。

注意!對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語言沒有給浮點數型別定義求餘運算子,所以需要使用 math包Mod()函式。

浮點數型別因為有Infinity(無窮大)值,所以即使是除零,也不會導致ArithmeticException異常丟擲

位移指令

分為左移和右移

  • 左移
  • 右移
    • 算術右移(有符號右移)
    • 邏輯右移(無符號右移)兩種。

算術右移和邏 輯位移的區別僅在於符號位的擴充套件,如下面的Java程式碼所示。

int x = -1;
println(Integer.toBinaryString(x)); // 11111111111111111111111111111111
println(Integer.toBinaryString(x >> 8)); // 11111111111111111111111111111111
println(Integer.toBinaryString(x >>> 8)); // 00000000111111111111111111111111

math目錄下建立sh.go檔案,在其中定義6條 位移指令,程式碼如下

type ISHL struct{ base.NoOperandsInstruction } // int左位移
type ISHR struct{ base.NoOperandsInstruction } // int算術右位移
type IUSHR struct{ base.NoOperandsInstruction } // int邏輯右位移(無符號右移位)
type LSHL struct{ base.NoOperandsInstruction } // long左位移
type LSHR struct{ base.NoOperandsInstruction } // long算術右位移
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)
}

先從運算元棧中彈出兩個int變數v2和v1。v1是要進行位移操作的變數,v2指出要移位多少位元。位移之後,把結果推入運算元棧。

s := uint32(v2) & 0x1f:這行程式碼將被左移的位數 v2 強制轉換為 uint32 型別,然後執行按位與操作(&)與常數 0x1f
這是為了確保左移的位數在範圍 0 到 31 內,因為在 Java 中,左移操作最多隻能左移 31 位,超出這個範圍的位數將被忽略。

這裡注意兩點:

int變數只有32位,所以只取v2的前5個位元就 足夠表示位移位數了

Go語言位移運算子右側必須是無符號 整數,所以需要對v2進行型別轉換

右移

算數右移

算術右移指令需要擴充套件符號位,程式碼和左移指令基本上差不多。以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 位,將其他位設定為 0,並將結果儲存在 s 變數中。這是為了限制右移的位數在 0 到 63 之間,因為在 Java 中,long型別右移操作最多隻能右移 63 位

邏輯右移

無符號右移位,以iushr為例,在移位前,先將v2轉化為正數,再進行移位,最後轉化為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目錄下建立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變數增加常量值,區域性變數表索引和常量值都由指令的運算元提供。

math目錄下建立iinc.go檔案,在其中定義iinc指令,程式碼如下:

type IINC struct {
    //索引
	Index uint
    //常量值
	Const int32
}
  • index:一個位元組,表示區域性變數表中要增加值的變數的索引。這個索引指定了要修改的區域性變數。
  • const:一個有符號位元組,表示要增加的常數值。這個常數值將與區域性變數的當前值相加,並將結果儲存回同一個區域性變數。

FetchOperands()函式從位元組碼裡讀取運算元,程式碼如下:

func (self *IINC) FetchOperands(reader *base.BytecodeReader) {
    self.Index = uint(reader.ReadUint8())
    self.Const = int32(reader.ReadInt8())
}

Execute()方法從區域性變數表中讀取變數,給它加上常量值,再把結果寫回區域性變數表,程式碼如下

func (self *IINC) Execute(frame *rtda.Frame) {
    localVars := frame.LocalVars()
    val := localVars.GetInt(self.Index)
    val += self.Const
    localVars.SetInt(self.Index, val)
}

九、型別轉換指令

型別轉換指令大致對應Java語言中的基本型別強制轉換操作。 型別轉換指令有共15條,將全部在本節實現。

引用型別轉換對應的是checkcast指令,將在後續完成。

型別轉換指令根據被轉換變數的型別分為四種系列:

  • i2x 系列指令:這些指令將整數(int)變數強制轉換為其他型別。
  • l2x 系列指令:這些指令將長整數(long)變數強制轉換為其他型別。
  • f2x 系列指令:這些指令將浮點數(float)變數強制轉換為其他型別。
  • d2x 系列指令:這些指令將雙精度浮點數(double)變數強制轉換為其他型別。

這些型別轉換指令允許將不同型別的資料進行強制型別轉換,以滿足特定的計算或操作需求。

d2x系列指令為例進行討論。

conversions目錄下建立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語言可以很方便地轉換各種基本型別的變數,所以型別轉換指令實現起來還是比較容易的。

十、比較指令

比較指令可以分為兩類:

  • 將比較結果推入運算元棧頂
  • 根據比較結果跳轉

比較指令是編譯器實現if-else、for、while等語句的基石,共有19條

lcmp指令

lcmp指令用於比較long變數。

comparisons目錄下建立lcmp.go檔案,在其中定義lcmp指令,程式碼如下:

type LCMP struct{ base.NoOperandsInstruction }

Execute()方法把棧頂的兩個long變數彈出,進行比較,然後把比較結果(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指令

fcmpgfcmpl指令用於比較float變數,它們的區別是對於非數字參與,fcmpg會預設為其大於任何非NaN值,fcmpl則相反。

comparisons目錄下建立fcmp.go檔案,在其中定義 fcmpgfcmpl指令,程式碼如下:

type FCMPG struct{ base.NoOperandsInstruction }
type FCMPL struct{ base.NoOperandsInstruction }

由於浮點數計算有可能產生NaN(Not a Number)值,所以比較兩個浮點數時,除了大於、等於、小於之外,
還有第4種結果:無法比較。

編寫一個函式來統一比較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虛擬機器規範:浮點數比較指令 fcmplfcmpg 的規範要求首先彈出 v2,然後是 v1,以便進行浮點數比較。

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 位元組碼中的條件分支指令,它根據條件 <cond> 來執行不同的分支。
條件 <cond> 可以是各種比較操作,比如等於、不等於、大於、小於等等。

常見的 if<cond> 指令包括:

  • ifeq: 如果棧頂的值等於0,則跳轉。
  • ifne: 如果棧頂的值不等於0,則跳轉。
  • iflt: 如果棧頂的值小於0,則跳轉。
  • ifge: 如果棧頂的值大於或等於0,則跳轉。
  • ifgt: 如果棧頂的值大於0,則跳轉。
  • ifle: 如果棧頂的值小於或等於0,則跳轉。

建立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)
	}
}

真正的跳轉邏輯在Branch()函式中。因為這個函式在很多指令中都會用到,所以定義在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 位元組碼中的一類條件分支指令,它用於對比兩個整數值,根據比較的結果來執行條件分支。這些指令的運算元棧上通常有兩個整數值,它們分別用於比較。

這類指令包括:

  • if_icmpeq: 如果兩個整數相等,則跳轉。
  • if_icmpne: 如果兩個整數不相等,則跳轉。
  • if_icmplt: 如果第一個整數小於第二個整數,則跳轉。
  • if_icmpge: 如果第一個整數大於等於第二個整數,則跳轉。
  • if_icmpgt: 如果第一個整數大於第二個整數,則跳轉。
  • if_icmple: 如果第一個整數小於等於第二個整數,則跳轉。

建立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 位元組碼中的一類條件分支指令,用於比較兩個引用型別的物件引用,根據比較的結果來執行條件分支。這些指令的運算元棧上通常有兩個物件引用,它們分別用於比較。

這類指令包括:

  • if_acmpeq: 如果兩個引用相等,則跳轉。
  • if_acmpne: 如果兩個引用不相等,則跳轉。

建立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 之前,jsrret 指令用於實現 finally 子句。從 Java 6 開始,Oracle 的 Java 編譯器不再使用這兩條指令。
  • return 系列指令有 6 條,用於從方法呼叫中返回,將在後續實現。
  • 本節將實現剩下的 3 條指令:gototableswitchlookupswitch

這些指令用於控制程式執行流,包括條件分支和無條件跳轉等操作。其中,goto 用於無條件跳轉到指定的目標位置,而 tableswitchlookupswitch 用於根據條件跳轉到不同的目標位置。

control目錄下建立goto.go檔案,在其中定義 goto指令,程式碼如下: