《自己動手寫java虛擬機》學習筆記(九)-----指令集和解釋器

項目地址:https://github.com/gongxianshengjiadexiaohuihui

     後面的筆記將不會區分go很java,也會改變以前大篇幅的寫代碼,更加註重對思想的講解,但是兩種語言的代碼會同步更新

     我們只到cpu是通過執行一條條設定好的指令,來指揮我們的電腦,同樣虛擬機的工作也是執行一條條指令,那麼我們的指令放在那裏呢?如果有印象的話,實在MemberInfo的Code屬性中,我們需要讀取裏面的字節碼然後,去執行指令。字節碼中存放編碼後的java虛擬機指令。每一條指令都以一個單字節的操作碼開頭,這就是字節碼名稱的由來。

     由於只使用一個字節表示操作碼,java虛擬機最多隻能支持256條指令。到第八版爲止。java虛擬機規範已經定義了205條指令構成了java虛擬機的指令集。和彙編語言類似,java虛擬機規範給每個操作碼都指定了一個助記符。比如0x00這條指令,助記符是nop。

     java虛擬機使用的是變長指令,操作碼後面可以跟零字節或多字節。如果把指令想象成函數的話。操作數就是它的參數。爲了讓編碼後的字節碼更加緊湊,很多操作碼本身隱含了操作數,比如把常數0推入操作數棧的助記就是iconst_0

    java虛擬機規範把已經定義的205條指令按用途分爲了11類,

    指令涉及的操作有 取指令  解析指令  執行指令,解析指令我們可以寫一個工廠類來完成,取指令和執行指令我們可以抽象出一個接口來

package instructions.base;

import rtda.Frame;

/**
 * @Author:ggp
 * @Date:2019/2/28 14 02
 * @Description:指令的接口,每條指令都要實現
 */
public interface Instruction {
    /**
     * 取指令操作
     * @param reader
     */
    void fetchOperands(ByteCodeReader reader);

    /**
     * 執行指令操作
     * @param frame
     */
    void execute(Frame frame);
}

同時指令按照類型還能分爲沒有操作數的指令 操作數是單字節的需要訪問索引,操作數是雙字節的,需要訪問運行時常量池。還有跳轉指令

拿使用頻率最高的無操作數指令的抽象父類來舉例

package instructions.base;

/**
 * @Author:ggp
 * @Date:2019/3/9 14 18
 * @Description:沒有操作數的指令,操作數往往隱藏在指令中
 */
public abstract class NoOperandsInstruction implements Instruction{
    @Override
    public void fetchOperands(ByteCodeReader reader){

    }
}

 然後下面的幾種指令集只需要繼承上面的幾個抽象父類,實現自己的執行指令方法即可

 常量指令 

  常量指令把常量推入操作數棧頂。常量可以來自三個地方:隱含在操作碼裏、操作數和運行時常量池

 加載指令 loads

 加載指令是根據索引去局部變量表中拿取變量放入操作數棧中,索引來自操作數,或隱藏在操作碼中

 存儲指令 stores

 存儲指令和加載指令相反,從操作數棧彈出棧頂元素,然後根據索引存儲在局部變量表中,索引來自操作數或隱藏在操作碼中

 操作數棧指令 stack

 pop系列,彈出棧頂slot

 dup系列,複製棧頂變量

 swap系列,交換棧頂變量

 數學指令  math

 彈出操作數棧的兩個變量,進行數學運算,包括加減乘除取餘取反位移還有邏輯運算

 轉換指令 conversions

 彈出操作數棧的棧頂元素,進行四種基本類型的轉換 int long double float

 比較指令 comparisons

 分兩種,一種是把比較結果推入操作數棧頂,另一種是根據比較結果跳轉

 控制指令 control

 跳轉指令和switch指令

 擴展指令 extended

  需要訪問局部變量表的指令,索引以uint8的形式存在字節碼中,對於大部分方法來說,局部變量表大小一般不會超過256,所以用一個自己就可以了,但是如果有方法的局部變量表超過這限制時,java虛擬機規範定義了wide指令來擴展上面的指令

 

 引用指令 references

 本節不涉及

 保留指令 reserved

  保留指令一共有三條。其中一條是留給調試器的,用於實現斷點,操作碼是202,助記符是breakpoint。另外兩條是留給java虛擬機內部使用。分別是254和266,助記符是impdep1和impdep2,這三條指令不允許出現在class文件中。

 

   解釋器

  其實我們上面已經說了解釋器的工作 ,取指令 解析指令 執行指令

  我們看一下實現

 

package main

import (
	"fmt"
	"jvmgo/classfile"
	"jvmgo/instructions"
	"jvmgo/instructions/base"
	"jvmgo/rtda"
)

//解釋器
func interpret(methodInfo *classfile.MemberInfo ){
	codeAtrr := methodInfo.CodeAttribute()
	maxLocals := codeAtrr.MaxLocals()
	maxStack := codeAtrr.MaxStack()
	bytecode := codeAtrr.Code()
    //回憶一下,new線程的時候,會創建一個1024幀大小的stack
	thread := rtda.NewThread()
	//暫時只推進一幀,也就是隻執行一個方法
	frame := thread.NewFrame(maxLocals, maxStack)
	thread.PushFrame(frame)
    //defer是Golang中的關鍵字,常用來釋放資源,會在函數返回之前進行調用。如果有多個defer表達式,調用順序類似於棧,後進先出
    //defer函數調用的執行時機是外層函數設置返回值之後,並且在即將返回之前
	defer catchErr(frame)
    loop(thread, bytecode)
}
func catchErr(frame *rtda.Frame){
	//go中可以拋出一個panic異常,然後通過在defer中通過recover捕獲這個異常,然後正常處理
	//在一個主進程中,多個go線程處理邏輯的結構中,這個很重要,如果不用recover捕獲panic異常,會導致整個進程出錯中斷
	if r := recover(); r != nil{
		fmt.Printf("LocalVars:%v\n", frame.LocalVars())
		fmt.Printf("OperandStack:%v\n", frame.OperandStack())
		panic(r)
	}
}
//循環執行 計算pc,解碼指令,執行指令
func loop(thread *rtda.Thread, bytecode []byte){
	fmt.Printf("**\n")
	frame := thread.PopFrame()
	reader := &base.ByteCodeReader{}
	for{
		//取指令
		pc := frame.NextPC();
		thread.SetPC(pc);
		//主要針對跳轉指令
		reader.Reset(bytecode,pc)
		opcode := reader.ReadUint8()

		//解碼
		inst := instructions.NewInstruction(opcode);
        inst.FetchOperands(reader)
        frame.SetNextPC(reader.PC())

        //執行
        fmt.Printf("pc:%2d inst:%T %v\n",pc, inst, inst)
        inst.Execute(frame)
	}
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章