本章节将在前面讲的基础上编写一个简单的解释器,并且实现一些具有代表性的指令,本章涉及到的go代码位于instructions包下。
Java虚拟机顾名思义,就是一台虚拟的机器,而字节码就是运行在这台虚拟机器上的机器码。字节码中存放编码后的Java虚拟机指令,每条指令都以一个单字节的操作码开头,这就是字节码名称的由来。看到这里才知道,之前的理解有些误区,只有Java方法(非抽象方法和本地方法)的代码才会编译成字节码,所以不能说整个class文件就是字节码,它只是包含字节码。由于只使用一个字节表示操作码,显而易见,Java虚拟机最多只能支持256条指令。到第八版为止,Java虚拟机规范已经定义了205条指令,操作码分别是0x00到0xCA、OxFE和0xFF。这205条指令构成了Java虚拟机的指令集。和汇编语言雷系,为了便于记忆,Java虚拟机规范给每个操作码都指定了一个助记符。比如操作码是0x00这条指令,因为它什么都不做,所以它的助记符是nop(no operation)。
Java虚拟机使用的是变长指令,操作码后面可以跟零字节或多字节的操作数。如果把指令想象成函数的话,操作数就是它的参数。为了让编码后的字节码更加紧凑,很多操作码本身就隐含了操作数,比如把常数0推入操作数栈的指令时iconst_0。下面通过具体的例子来观察Java虚拟机指令,还是以ClassFile.class为例,看下main()方法的第一个指令:
可以看到,该指令的操作码时0xB2,助记符是getstatic,它的操作数是0x0002,代表常量池的第二个常量。
在上衣章节中我们知道局部变量表和操作数栈只存放数据的值,并不记录数据类型。结果就是:指令必须知道自己在操作什么类型的数据,这一点也直接反映在了操作码的助记符上。例如,iadd指令就是对int值进行加法操作;dstore指令把操作数顶的double值弹出,存储到局部变量表中;areturn从方法中返回引用值。也就是说,如果某类指令可以操作不同类型的变量,则助记符的第一个字母表示变量类型。助记符首字母和变量类型的对应关系如下:
助记符首字母 | 数据类型 | 例子 |
---|---|---|
a | reference | aload、astore、areturn |
b | byte/boolean | bipush、baload |
c | char | caload、castore |
d | double | dload、dstore、dadd |
f | float | fload、fstore、fadd |
i | int | iload、istore、iadd |
l | long | lload、lstore、ladd |
s | short | sipush、sastore |
Java虚拟机规范把已经定义的的205条指令按用途分成11类,分别是:常量指令、加载指令、存储指令、操作数栈指令、数学指令、转换指令、比较指令、控制指令、引用指令、扩展指令和保留指令。其中保留指令一共有3条,这三条指令不允许出现在class文件中:
Java虚拟机解释器大致的逻辑就是在一个循环中不停地计算pc、指令解码、指令执行。可以把这个逻辑用Go语言写成一个for循环,把指令抽象成接口,解码和执行逻辑卸载具体的指令实现中。本节就先来定义指令接口和一些结构体来辅助指令解码。
在讲指令接口之前要先讲下BytecodeReader,因为指令接口用到了BytecodeReader的读取字节码的方法。BytecodeReader结构体位于/instructions/base包下的bytecode_reader.go文件中。顺便说一下,为了便于管理,本章会把每种指令的源文件放在各自的包下,所有指令的公用代码放在base包下。
// BytecodeReader结构体 type BytecodeReader struct { // 存放字节码 code []byte // 记录存取到了哪个字节码 pc int } // 为了避免每次解码指令都新创建一个BytecodeReader实例,所以定义一个Reset()方法 func(self *BytecodeReader)Reset(code []byte, pcint) { self.code = code self.pc = pc } // 获取pc func(self *BytecodeReader)PC()int { return self.pc } // 读取一个字节的uint8 func(self *BytecodeReader)ReadUint8()uint8 { i := self.code[self.pc] self.pc++ return i } // 将uint8转换成int8 func(self *BytecodeReader)ReadInt8()int8 { return int8(self.ReadUint8()) } //此处省略读取2个字节的方法 ...
该接口位于/instructions/base包下的instrucion.go文件中:
// 定义指令接口 type Instruction interface { // 从字节码中提取操作数 FetchOperands(reader *BytecodeReader) // 执行指令 Execute(frame *rtdata.Frame) }
下面再定义一些公用的结构体,继续修改instrucion.go文件:
/* 表示没有操作数的指令 没有任何字段,FetchOperands方法也为空 */ type NoOperandsInstruction struct { } func(self *NoOperandsInstruction)FetchOperands(reader *BytecodeReader) { } // 表示跳转指令,Offset字段存储跳转偏移量 type BranchInstruction struct { // 存储跳转偏移量 Offset int } func(self *BranchInstruction)FetchOperands(reader *BytecodeReader) { // 从字节码中读取uint16整数,转成int后赋给Offset self.Offset = int(reader.ReadInt16()) } //此处省略读取1个字节、2个字节的Instruction抽象结构体 ...
常量指令把常量推入操作数栈顶。常量可以来自三个地方:隐含在操作码里、操作数和运行时常量。常量池共有21条,本节将实现其中一部分。常量池相关代码都位于/instructions/constants包下。
nop指令是最简单的一条指令,因为它什么也不做。该指令实现代码位于nop.go文件中:
// 定义NOP结构体,继承NoOperandsInstruction type NOP struct{ base.NoOperandsInstruction } // 什么都不执行 func(self *NOP)Execute(frame *rtdata.Frame) { }
这一系列指令把隐含在操作码中的常量值推入操作数栈顶。由于实现很相似,下面贴出其中三种,该指令实现代码位于const.go文件中:
// aconst_null指令把null引用推入操作数栈顶 type ACONST_NULL struct{ base.NoOperandsInstruction } func(self *ACONST_NULL)Execute(frame *rtdata.Frame) { frame.OperandStack().PushRef(nil) } // dconst_0指令把double类型的0推入操作数栈顶 type DCONST_0 struct{ base.NoOperandsInstruction } func(self *DCONST_0)Execute(frame *rtdata.Frame) { frame.OperandStack().PushDouble(0.0) } // iconst_m1指令把int类型的-1推入操作数栈顶 type ICONST_M1 struct{ base.NoOperandsInstruction } func(self *ICONST_M1)Execute(frame *rtdata.Frame) { frame.OperandStack().PushInt(-1) } ...
bipush指令从操作数中获取一个byte型整数,扩展成int型,然后推入栈顶。sipush指令从操作数中获取一个short型整数,扩展成int型,然后推入栈顶。这两个指令实现代码位于ipush.go文件中:
// Push byte type BIPUSH struct { val int8 } // 读取单个字节操作数 func(self *BIPUSH)FetchOperands(reader *base.BytecodeReader) { self.val = reader.ReadInt8() } // 将操作数int值推入栈顶 func(self *BIPUSH)Execute(frame *rtdata.Frame) { i := int32(self.val) frame.OperandStack().PushInt(i) } // Push short代码类似,略 ...
加载指令从局部变量表获取变量,然后推入操作数栈顶。加载指令一共33条,按照操作数变量类型可以分为6类:aload系列指令操作引用类型变量、dload系列操作double类型变量、fload系列操作float类型变量、iload系列操作int类型变量、lload系列操作long类型变量、xaload操作数组。本节将实现其中一部分指令,加载指令相关代码位于/instructions/loads包下。
load系列指令的实现代码比较相似,这里挑iload指令详细讲下,实现代码位于iload.go文件中:
// iload指令结构体 type ILOAD struct{ base.Index8Instruction } // 通过索操作局部变量表 func(self *ILOAD)Execute(frame *rtdata.Frame) { _iload(frame, uint(self.Index)) } // 统一的iload函数 func_iload(frame *rtdata.Frame, indexuint) { // 通过索引读取局部变量表 val := frame.LocalVars().GetInt(index) // 将局部变量表中的值推入栈顶 frame.OperandStack().PushInt(val) } // 操作第0号局部变量,索引隐含在操作码中 type ILOAD_0 struct{ base.NoOperandsInstruction } func(self *ILOAD_0)Execute(frame *rtdata.Frame) { _iload(frame, 0) } // 省略ILOAD_1、ILOAD_2、ILOAD_3 ...