这里要说的指令,并不是x86体系结构下的机器指令,而是虚拟机指令。Jerry虚拟机的设计并不是非常复杂——甚至看起来很不灵活——它是一个栈式虚拟机,不包括任何寄存器(除了指令计数器之外),所有的指令都是基于栈进行的。在文章最后将给出完整的指令集。现在先给出指令数据结构,它们包括
typedef enum {
// ... 指令集,文章最后会贴出完整的
} InstructionCode;
// 依旧 COOL 风格的成员
// segOffset 指令的偏移值
// code 指令码
#define memberAbstractInstruction \
int segOffset; \
InstructionCode code; /*******/
// 指令数据结构基类
struct AbstractInstruction {
memberAbstractInstruction
};
// 无参数的指令
struct NoParamInstruction {
memberAbstractInstruction
};
// 带整型参数的指令
struct IntParamInstruction {
memberAbstractInstruction
int param;
};
// 带实型参数的指令
struct RealParamInstruction {
memberAbstractInstruction
double param;
};
// 跳转指令
// targetIns 目标指令指针
struct JumpInstruction {
memberAbstractInstruction
struct AbstractInstruction* targetIns;
};
也许你会觉得奇怪,为什么跳转指令不计入整型参数的指令,要单独拿出来,还弄个指针参数?如果这是个解释器的话,问题还不大,但指针如果存放进字节码文件再取回内存,如果运气不是好到雇佣大猩猩能敲出Jerry编译程序的话,那么指针指向的地址将不是我们期望的。因此,这里的目标指令指针的含义将会在最后写出指令时变化为整数偏移量。
不在这一步就算好所有指令的地址,然后用整数来保存这个域,是考虑到后期优化。否则,某些删去无用指令的优化,将会导致跳转指令的偏移失效。为了避免每次这样小动作都重新计算跳转指令的偏移,方法就是,让跳转指令的偏移量在写出指令到文件之前的最后关口才确定下来。确定的方法就是用指令的 segOffset 域差值来计算——这样一来 segOffset 的值也必须到那个时候(之前一点点)计算才行。
刚才提到了,虚拟机是基于栈的,这就带来了很多偷懒的机会,比如加法指令就不用去操心拿那几个寄存器摆弄了。顺带提一下,Jerry的指令大多数是定长的,只有一部分带参数指令会将参数放在指令末尾,它们的长度较其他指令长。
运算指令
INT_PLUS 弹出栈顶 B 和次栈顶 A,计算 A + B 压入栈中
INT_MINUS -
INT_MULTIPLY *
INT_DIVIDE /
INT_LT <
INT_LE <=
INT_EQ ==
INT_NE !=
INT_GT >
INT_GE >=
注意上面 A 是次栈顶,而 B 是栈顶,下面也是。
INT_ASSIGN 弹出栈顶 B 和次栈顶 A,将 B 的值写入 A 指向的地址中去,然后在栈顶压入 B 的值。
对于实型数,有一整套对应的指令:
REAL_PLUS, REAL_MINUS, REAL_MULTIPLY, REAL_DIVIDE, REAL_ASSIGN,
REAL_LT, REAL_LE, REAL_EQ, REAL_NE, REAL_GT, REAL_GE
IO 指令
READ_INT 弹出栈顶 A,输入一个整数,将值放入 A 指向的地址中去。
READ_REAL 实数
WRITE_INT 弹出栈顶整数 A 输出。
WRITE_REAL 实数
运行时类型转换
INT_2_REAL 弹出栈顶整数,转换成实数压回栈中
REAL_2_INT 实数 整数
常数加载
LOAD_INT 弹出栈顶 A,输入一个整数,从 A 指向的地址加载一个整数到栈顶
LOAD_REAL 实数
弹栈
POP 其实就是单纯地让栈顶指针向基部靠拢,这是一个带整型参数的指令,参数说明了弹出的字节数。
常数加载
CONST_INT 加载一个常数整数到栈顶,这是一个带整型参数的指令。
CONST_REAL 实数 实型
控制指令
JMP 无条件跳转,参数为整数,表示偏移量
JMP_IF_TOP 弹出栈顶整数 A,如果 A 不为 0,则跳转,参数为整数,表示偏移量
JMP_NOT_TOP 为
END_PROGRAM 终止程序
其他
NOP 无操作
结束。
typedef enum { INT_PLUS, INT_MINUS, INT_MULTIPLY, INT_DIVIDE, INT_ASSIGN, INT_LT, INT_LE, INT_EQ, INT_NE, INT_GT, INT_GE, REAL_PLUS, REAL_MINUS, REAL_MULTIPLY, REAL_DIVIDE, REAL_ASSIGN, REAL_LT, REAL_LE, REAL_EQ, REAL_NE, REAL_GT, REAL_GE, INS_NOT, READ_INT, READ_REAL, WRITE_INT, WRITE_REAL, INT_2_REAL, REAL_2_INT, LOAD_INT, LOAD_REAL, POP, CONST_INT, CONST_REAL, JMP, JMP_IF_TOP, JMP_NOT_TOP, NOP, END_PROGRAM } InstructionCode; // 根据算术符号的枚举值获取对应操作码,注意两个枚举的顺序要一致 #define getCodeByIntOp(x) ((x) - PLUS + INT_PLUS) #define getCodeByRealOp(x) ((x) - PLUS + REAL_PLUS) // 是否为无参数操作 #define isNoParamIns(x) (((x) >= INT_PLUS && (x) <= LOAD_REAL) \ || NOP == (x) || END_PROGRAM == (x)) // 是否为整型参数操作 #define isIntParamIns(x) (POP == (x) || CONST_INT == (x)) // 是否为实型参数操作 #define isRealParamIns(x) (CONST_REAL == (x)) // 是否为跳转 #define isJump(x) (JMP <= (x) && (x) <= JMP_NOT_TOP)