bboyjing's blog

自己动手写JVM十三【指令集和解释器(一)】

本章节将在前面讲的基础上编写一个简单的解释器,并且实现一些具有代表性的指令,本章涉及到的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()方法的第一个指令:
jvmgo_41
可以看到,该指令的操作码时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文件中:

  • 0xCA,助记符是breakpoint,留给调试器使用
  • 0xFE、0xFF,助记符分别是impdep1和impdep2,留给Java虚拟机实现内部使用

指令和指令解码

Java虚拟机解释器大致的逻辑就是在一个循环中不停地计算pc、指令解码、指令执行。可以把这个逻辑用Go语言写成一个for循环,把指令抽象成接口,解码和执行逻辑卸载具体的指令实现中。本节就先来定义指令接口和一些结构体来辅助指令解码。

BytecodeReader

在讲指令接口之前要先讲下BytecodeReader,因为指令接口用到了BytecodeReader的读取字节码的方法。BytecodeReader结构体位于/instructions/base包下的bytecode_reader.go文件中。顺便说一下,为了便于管理,本章会把每种指令的源文件放在各自的包下,所有指令的公用代码放在base包下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// BytecodeReader结构体
type BytecodeReader struct {
// 存放字节码
code []byte
// 记录存取到了哪个字节码
pc int
}
// 为了避免每次解码指令都新创建一个BytecodeReader实例,所以定义一个Reset()方法
func (self *BytecodeReader) Reset(code []byte, pc int) {
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个字节的方法
...

Instruction接口

该接口位于/instructions/base包下的instrucion.go文件中:

1
2
3
4
5
6
7
// 定义指令接口
type Instruction interface {
// 从字节码中提取操作数
FetchOperands(reader *BytecodeReader)
// 执行指令
Execute(frame *rtdata.Frame)
}

下面再定义一些公用的结构体,继续修改instrucion.go文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
表示没有操作数的指令
没有任何字段,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指令是最简单的一条指令,因为它什么也不做。该指令实现代码位于nop.go文件中:

1
2
3
4
5
6
// 定义NOP结构体,继承NoOperandsInstruction
type NOP struct{ base.NoOperandsInstruction }
// 什么都不执行
func (self *NOP) Execute(frame *rtdata.Frame) {
}

const系列指令

这一系列指令把隐含在操作码中的常量值推入操作数栈顶。由于实现很相似,下面贴出其中三种,该指令实现代码位于const.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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和sipush指令

bipush指令从操作数中获取一个byte型整数,扩展成int型,然后推入栈顶。sipush指令从操作数中获取一个short型整数,扩展成int型,然后推入栈顶。这两个指令实现代码位于ipush.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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包下。

iload指令

load系列指令的实现代码比较相似,这里挑iload指令详细讲下,实现代码位于iload.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// iload指令结构体
type ILOAD struct{ base.Index8Instruction }
// 通过索操作局部变量表
func (self *ILOAD) Execute(frame *rtdata.Frame) {
_iload(frame, uint(self.Index))
}
// 统一的iload函数
func _iload(frame *rtdata.Frame, index uint) {
// 通过索引读取局部变量表
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
...