本章节将在前面讲的基础上编写一个简单的解释器,并且实现一些具有代表性的指令,本章涉及到的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文件中:
- 0xCA,助记符是breakpoint,留给调试器使用
- 0xFE、0xFF,助记符分别是impdep1和impdep2,留给Java虚拟机实现内部使用
指令和指令解码
Java虚拟机解释器大致的逻辑就是在一个循环中不停地计算pc、指令解码、指令执行。可以把这个逻辑用Go语言写成一个for循环,把指令抽象成接口,解码和执行逻辑卸载具体的指令实现中。本节就先来定义指令接口和一些结构体来辅助指令解码。
BytecodeReader
在讲指令接口之前要先讲下BytecodeReader,因为指令接口用到了BytecodeReader的读取字节码的方法。BytecodeReader结构体位于/instructions/base包下的bytecode_reader.go文件中。顺便说一下,为了便于管理,本章会把每种指令的源文件放在各自的包下,所有指令的公用代码放在base包下。
Instruction接口
该接口位于/instructions/base包下的instrucion.go文件中:
下面再定义一些公用的结构体,继续修改instrucion.go文件:
常量池指令
常量指令把常量推入操作数栈顶。常量可以来自三个地方:隐含在操作码里、操作数和运行时常量。常量池共有21条,本节将实现其中一部分。常量池相关代码都位于/instructions/constants包下。
nop指令
nop指令是最简单的一条指令,因为它什么也不做。该指令实现代码位于nop.go文件中:
const系列指令
这一系列指令把隐含在操作码中的常量值推入操作数栈顶。由于实现很相似,下面贴出其中三种,该指令实现代码位于const.go文件中:
bipush和sipush指令
bipush指令从操作数中获取一个byte型整数,扩展成int型,然后推入栈顶。sipush指令从操作数中获取一个short型整数,扩展成int型,然后推入栈顶。这两个指令实现代码位于ipush.go文件中:
加载指令
加载指令从局部变量表获取变量,然后推入操作数栈顶。加载指令一共33条,按照操作数变量类型可以分为6类:aload系列指令操作引用类型变量、dload系列操作double类型变量、fload系列操作float类型变量、iload系列操作int类型变量、lload系列操作long类型变量、xaload操作数组。本节将实现其中一部分指令,加载指令相关代码位于/instructions/loads包下。
iload指令
load系列指令的实现代码比较相似,这里挑iload指令详细讲下,实现代码位于iload.go文件中: