bboyjing's blog

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

本章节继续学习指令集和解释器。

控制指令

控制指令共有11条,本节实现其中的3条指令:goto、tableswitch和lookupswitch。该系列指令代码位于/instructions/control包下。

goto指令

goto指令实现代码位于goto.go文件中:

1
2
3
4
5
6
7
// goto指令结构体
type GOTO struct{ base.BranchInstruction }
func (self *GOTO) Execute(frame *rtdata.Frame) {
// 无条件跳转
base.Branch(frame, self.Offset)
}

tableswitch指令

Java语言中的switch-case语句有两种实现方式:如果case可以编码成一个索引表,则实现成tableswitch指令,否则实现成lookupswitch指令。借用下Java虚拟机规范中的例子来了解下这两种实现方式的区别。

  • 可以编译成tableswitch指令的Java方法:

    1
    2
    3
    4
    5
    6
    7
    8
    int chooseNear(int i){
    switch (i) {
    case 0: return 0;
    case 1: return 1;
    case 2: return 2;
    default: return -1;
    }
    }
  • 编译成lookupswitch指令的Java方法:

    1
    2
    3
    4
    5
    6
    7
    8
    int chooseFar(int i){
    switch (i) {
    case -100: return -1;
    case 0: return 0;
    case 100: return 1;
    default: return -1;
    }
    }

tableswitch指令实现代码位于tableswitch.go文件中,先看下该指令结构体定义:

1
2
3
4
5
6
7
8
9
type TABLE_SWITCH struct {
// 默认情况下执行跳转所需的字节码偏移量
defaultOffset int32
// low和high记录case的取值范围
low int32
high int32
// 索引表,存放high-low+1个int值
jumpOffsets []int32
}

这个指令比之前的指令要稍微复杂点,我们还是结合class文件来理解下,在java项目中新建TableSwitch类,并把chooseNear()方法添加至类中,然后查看class文件:
jvmgo_42
tableswitch的字节码比较长,我们慢慢看。

  • 第一个字节0xAA表示助记符tableswitch指令
  • 后面两个字节0x00、0x00没有实质意义,因为tableswitch指令操作码的后面会有0~3个字节的padding,以保证defaultOffset在字节码中的地址是4的倍数。需要修改bytecode_reader.go文件,添加SkipPadding方法

    1
    2
    3
    4
    5
    6
    // 跳过Padding字节
    func (self *BytecodeReader) SkipPadding() {
    for self.pc%4 != 0 {
    self.ReadUint8()
    }
    }
  • 再后面四个字节0x00000021表示default情况下执行跳转所需的字节码偏移量,就是switch语句中的default分支,对应TABLE_SWITCH结构体的defaultOffset

  • 再后面四个字节0x00000000表示case取值范围的最小值,也就是例子中的case分支的最小值0,对应TABLE_SWITCH结构体的low
  • 再后面四个字节0x00000002表示case取值范围的最大值,也就是例子中的case分支的最大值2,对应TABLE_SWITCH结构体的high
  • 最后是不定字节数((high - low + 1) × 4),每4个字节是一个int32值,存放到TABLE_SWITCH结构体的jumpOffSets索引表中,对应各个case情况下执行跳转所需的字节码偏移量。需要修改bytecode_reader.go文件,添加ReadInt32s方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 读取指定数量的int32,并返回数组
    func (self *BytecodeReader) ReadInt32s(n int32) []int32 {
    ints := make([]int32, n)
    for i := range ints {
    ints[i] = self.ReadInt32()
    }
    return ints
    }
    // 省略ReadInt32(),参照源码

理解了字节码,下面可以来读取和执行了,修改tableswitch.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
25
26
27
28
29
30
func (self *TABLE_SWITCH) FetchOperands(reader *base.BytecodeReader) {
// 跳过Padding字节
reader.SkipPadding()
// 读取4个字节default跳转字节码偏移量
self.defaultOffset = reader.ReadInt32()
// 读取4个字节case范围最小值
self.low = reader.ReadInt32()
// 读取4个字节case范围最大值
self.high = reader.ReadInt32()
// 计算索引表数量
jumpOffsetsCount := self.high - self.low + 1
// 填充索引表
self.jumpOffsets = reader.ReadInt32s(jumpOffsetsCount)
}
func (self *TABLE_SWITCH) Execute(frame *rtdata.Frame) {
// 弹出栈顶int型变量,作为需要匹配case分支的值
index := frame.OperandStack().PopInt()
var offset int
if index >= self.low && index <= self.high {
// 若在case范围内,则通过index获取索引表中的值
offset = int(self.jumpOffsets[index-self.low])
} else {
// 若不再case范围内,给出默认值
offset = int(self.defaultOffset)
}
// 跳转到指定offset
base.Branch(frame, offset)
}

lookupswitch指令

lookupswitch指令的字节码和tableswitch类似,这里就不详细说明了。该指令实现代码位于lookupswitch.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
25
26
27
28
29
30
31
32
33
34
35
36
37
type LOOKUP_SWITCH struct {
// 默认情况下执行跳转所需的字节码偏移量
defaultOffset int32
// case分支的数量
npairs int32
// 存放跳转指令偏移量
matchOffsets []int32
}
func (self *LOOKUP_SWITCH) FetchOperands(reader *base.BytecodeReader) {
// 跳过Padding字节
reader.SkipPadding()
// 读取4个字节default跳转字节码偏移量
self.defaultOffset = reader.ReadInt32()
// 读取4个字节case分值数量
self.npairs = reader.ReadInt32()
/*
读取各个case跳转指令偏移量
matchOffsets中每个case存储8个字节,前4个字节是case的值,后4个字节是跳转偏移量,有点像Map
*/
self.matchOffsets = reader.ReadInt32s(self.npairs * 2)
}
func (self *LOOKUP_SWITCH) Execute(frame *rtdata.Frame) {
// 弹出栈顶int型变量,作为需要匹配case分支的值
key := frame.OperandStack().PopInt()
for i := int32(0); i < self.npairs*2; i += 2 {
// 循环判断给定值是否命中case分值
if self.matchOffsets[i] == key {
// 若命中,则跳转到指定偏移量
offset := self.matchOffsets[i+1]
base.Branch(frame, int(offset))
return
}
}
base.Branch(frame, int(self.defaultOffset))
}

扩展指令

本节来实现一些扩展指令,相关代码位于/instructions/extended包下。

wide指令

加载类指令、存储类指令、ret指令和iinc指令需要按索引访问局部变量表,索引以uint8的形式存在字节码中。对于大部分方法来说,局部变量表大小都不会超过256,所以用一字节来表示索引就够了。但是如果有方法的局部变量表超过这限制,Java虚拟机规范定义了wide指令来扩展前述指令。wide指令实现代码位于wide.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
25
26
27
/*
wide指令结构体
wide指令只是增加了索引宽度,并不改变子指令操作
*/
type WIDE struct {
modifiedInstruction base.Instruction
}
func (self *WIDE) FetchOperands(reader *base.BytecodeReader) {
// 获取操作码
opcode := reader.ReadUint8()
switch opcode {
case 0x15:
// 初始化iload指令结构体(创建子指令实例)
inst := &loads.ILOAD{}
// 扩展iload1个字节操作数至两个字节
inst.Index = uint(reader.ReadUint16())
self.modifiedInstruction = inst
// 其他指令省略,参照项目源码
...
}
}
// 调用子指令的Execute()方法,不改变子指令行为
func (self *WIDE) Execute(frame *rtdata.Frame) {
self.modifiedInstruction.Execute(frame)
}

ifnull指令和ifnonnull指令

这两个指令的功能时根据引用是否时null进行跳转,实现代码位于ifnull.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ifnull指令结构体
type IFNULL struct{ base.BranchInstruction }
func (self *IFNULL) Execute(frame *rtdata.Frame) {
// 弹出栈顶引用变量
ref := frame.OperandStack().PopRef()
if ref == nil {
// 如果变量是null,则跳转
base.Branch(frame, self.Offset)
}
}
// 省略ifnonnull
...

goto_w指令

goto_w指令和goto指令唯一的区别就是索引从2个字节变成了4个字节,代码就不列出来了,参照goto_w.go文件。

指令集就学到这里,下一节将学习如何编写一个简单的解释器。