→Tags:指令 引擎 执行 操作 实现 一个 long sp l1 虚拟 两个 ←
指令集是虚拟机中最底层也是最核心的部分,Java程序中的变量赋值、函数调用等所有操作最后都要被转化为一条条的指令来执行。
指令集是在Java虚拟机规范中定义的,各种虚拟机实现要给予精确的实现,下面就来介绍一下指令集的分类以及在KVM中是如何实现的。
在头文件kvm/vmcommon/h/interpret.h中有如下对指令集种类的定义:
Word-BREAK: break-all; PADDING-TOP: 4px; BORDER-BOTTOM: windowtext 0.5pt solid">
typedef enum {
Ac8北方站长站 NOP = 0x00,
Ac8北方站长站 ACONST_NULL = 0x01,
Ac8北方站长站 ICONST_M1 = 0x02,
Ac8北方站长站……
Ac8北方站长站 LASTBYTECODE = 0xDF
Ac8北方站长站} ByteCode ;
以及每条指令的名字:
#define BYTE_CODE_NAMES {
Ac8北方站长站 "NOP", /* 0x00 */
Ac8北方站长站 "ACONST_NULL", /* 0x01 */
Ac8北方站长站"ICONST_M1", /* 0x02 */
Ac8北方站长站……
Ac8北方站长站"CUSTOMCODE" /* 0xDF */ }
Java虚拟机的指令集非常多,大概有200种左右,本篇不详细介绍每一条指令的功能和参数,只选取几个典型的指令作为例子,介绍它们是如何实现的。
KVM中,所有指令的实现都放在kvm/vmcommon/src/bytecodes.c中,每一条指令都遵从如下的形式:
SELECT(指令号)
Ac8北方站长站 {operations}
Ac8北方站长站DONE(跳转位置)
Ac8北方站长站Ac8北方站长站注:
Ac8北方站长站#define SELECT(l1) case l1: {
Ac8北方站长站#define SELECT2(l1, l2) case l1: case l2: {
Ac8北方站长站#define SELECT3(l1, l2, l3) case l1: case l2: case l3: {
Ac8北方站长站#define SELECT4(l1, l2, l3, l4) case l1: case l2: case l3: case l4: {
Ac8北方站长站#define SELECT5(l1, l2, l3, l4, l5) case l1: case l2: case l3: case l4: case l5: {
Ac8北方站长站#define SELECT6(l1, l2, l3, l4, l5, l6) case l1: case l2: case l3: case l4: case l5: case l6: {
Ac8北方站长站#define DONE(n) } goto next##n;
Ac8北方站长站#define DONEX }
Ac8北方站长站#define DONE_R } goto reschedulePoint;
{operations}部分是该指令的具体实现。
整个bytecodes.c文件其实是一个switch分支结构中的cases部分,这个文件中定义了所有的case。这个文件会被源文件kvm/vmcommon/src/execute.c所包含,execute.c中定义有一个方法
void SlowInterpret(ByteCode token);
它是解释执行Java指令的主要函数,参数token就是一条指令,在本函数中会有一个switch()结构来选择token的执行路径:
void SlowInterpret(ByteCode token) {
Ac8北方站长站…
Ac8北方站长站switch (token) {
Ac8北方站长站…
Ac8北方站长站#include "bytecodes.c"
Ac8北方站长站…
Ac8北方站长站next3: ip++;
Ac8北方站长站next2: ip++;
Ac8北方站长站next1: ip++;
Ac8北方站长站next0:
Ac8北方站长站reschedulePoint:
Ac8北方站长站 return;
Ac8北方站长站}
函数结尾处的几个标签是指令完成后会跳转到的地方。
依据Java虚拟机规范,虚拟机指令可以分为装载和存储指令、运算指令、类型转换指令、对象创建与操纵指令、操作数栈管理指令、控制转移指令、方法调用和返回指令、抛出和处理异常指令、实现finally指令和同步指令等10类,下面从中选取几个简单的指令来看一看它们是如何设计的:
1、ICONST_0
说明:
无参数,向操作数栈中压入int型常量0。
实现代码:
SELECT(ICONST_0) /* Push integer constant 0 onto the operand stack */
Ac8北方站长站 pushStack(0);
Ac8北方站长站DONE(1)
Ac8北方站长站宏经适当展开后为:
Ac8北方站长站case ICONST_0: {
Ac8北方站长站 *++GlobalState.gs_sp = 0;
Ac8北方站长站} goto next1;
GlobalState.gs_sp是当前帧内操作数栈的指针,ICONST_0指令要做的只是把指针向后移动一个字(注意是“字”而不是“字节”),然后给新字赋值为0;最后程序计数器ip自加1,表明没有跳转,接着执行下一条指令。
2、DSTORE
说明:
本指令带有一个字节的参数offset,作用是从操作数栈中读取一个double型的值(双字)并存放到局部变量区中的offset和offset+1位置。
实现代码:
SELECT(DSTORE) /* Store double into local variable */
Ac8北方站长站 unsigned int index = ip[1];
Ac8北方站长站 lp[index+1] = popStack();
Ac8北方站长站 lp[index] = popStack();
Ac8北方站长站DONE(2)
Ac8北方站长站宏展开为:
Ac8北方站长站case DSTORE: {
Ac8北方站长站 unsigned int index = GlobalState.gs_ip[1];
Ac8北方站长站 GlobalState.gs_lp[index+1] = *GlobalState.gs_sp --;
Ac8北方站长站 GlobalState.gs_lp[index] = *GlobalState.gs_sp --;
Ac8北方站长站} goto next2;
首先从程序计数器的下一个字节中取出目标位置的偏移量index,然后从操作数栈中弹出两个字分别作为double型数的底位和高位存入局部变量lp所指向的区域中的合适位置。
3、I2L
说明:
无参数,将操作数栈中的当前操作数由int型转换为long型。
实现代码:
SELECT(I2L) /* Convert integer to long */
Ac8北方站长站 long value = *(long *)sp;
Ac8北方站长站#if BIG_ENDIAN
Ac8北方站长站 ((long *)sp)[1] = value;
Ac8北方站长站 ((long *)sp)[0] = value >> 31;
Ac8北方站长站#elif LITTLE_ENDIAN !COMPILER_SUPPORTS_LONG
Ac8北方站长站 ((long *)sp)[1] = value >> 31;
Ac8北方站长站#else
Ac8北方站长站 SET_LONG(sp, value);
Ac8北方站长站#endif
Ac8北方站长站 getSP()++;
Ac8北方站长站DONE(1)
由于long比int表示的范围大,所以在扩展时多出来的高位只是用于符号扩展。先从操作数栈中取出int型整数并把它作为一个long型,如果定义了宏BIG_ENDIAN,说明操作数栈中的存储规则是高字节在前,这时要把value的值向后移一个字作为低字来用,高字用于作符号扩展;如果操作数栈中是低位在前的话,原位置中的字不用动,只要把下一个字作符号扩展即可。最近,由于当前操作数由一个字变为两个字,所以sp要自加1。
4、LMUL
说明:
无参数;从栈中弹出两个long型数,相乘,然后将所得long型结果压回栈。
实现代码:
共有 0 位网友发表了评论 此处只显示部分留言 点击查看完整评论页面