指令集是虛擬機中最底層也是最核心的部分,Java程序中的變量賦值、函數調用等所有操作最後都要被轉化為一條條的指令來執行。
指令集是在Java虛擬機規范中定義的,各種虛擬機實現要給予精確的實現,下面就來介紹一下指令集的分類以及在KVM中是如何實現的。
在頭文件kvm/vmcommon/h/interpret.h中有如下對指令集種類的定義:
typedef enum {
NOP = 0x00,
ACONST_NULL = 0x01,
ICONST_M1 = 0x02,
……
LASTBYTECODE = 0xDF
} ByteCode ;
以及每條指令的名字:
#define BYTE_CODE_NAMES {
"NOP", /* 0x00 */
"ACONST_NULL", /* 0x01 */
"ICONST_M1", /* 0x02 */
……
"CUSTOMCODE" /* 0xDF */ }
Java虛擬機的指令集非常多,大概有200種左右,本篇不詳細介紹每一條指令的功能和參數,只選取幾個典型的指令作為例子,介紹它們是如何實現的。
KVM中,所有指令的實現都放在kvm/vmcommon/src/bytecodes.c中,每一條指令都遵從如下的形式:
SELECT(指令號)
{Operations}
DONE(跳轉位置)
注:
#define SELECT(l1) case l1: {
#define SELECT2(l1, l2) case l1: case l2: {
#define SELECT3(l1, l2, l3) case l1: case l2: case l3: {
#define SELECT4(l1, l2, l3, l4) case l1: case l2: case l3: case l4: {
#define SELECT5(l1, l2, l3, l4, l5) case l1: case l2: case l3: case l4: case l5: {
#define SELECT6(l1, l2, l3, l4, l5, l6) case l1: case l2: case l3: case l4: case l5: case l6: {
#define DONE(n) } goto next##n;
#define DONEX }
#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) {
…
switch (token) {
…
#include "bytecodes.c"
…
next3: ip++;
next2: ip++;
next1: ip++;
next0:
reschedulePoint:
return;
}
函數結尾處的幾個標簽是指令完成後會跳轉到的地方。
依據Java虛擬機規范,虛擬機指令可以分為裝載和存儲指令、運算指令、類型轉換指令、對象創建與操縱指令、操作數棧管理指令、控制轉移指令、方法調用和返回指令、拋出和處理異常指令、實現finally指令和同步指令等10類,下面從中選取幾個簡單的指令來看一看它們是如何設計的:
1、ICONST_0
說明:
無參數,向操作數棧中壓入int型常量0。
實現代碼:
SELECT(ICONST_0) /* Push integer constant 0 onto the Operand stack */
pushStack(0);
DONE(1)
宏經適當展開後為:
case ICONST_0: {
*++GlobalState.gs_sp = 0;
} goto next1;
GlobalState.gs_sp是當前幀內操作數棧的指針,ICONST_0指令要做的只是把指針向後移動一個字(注意是“字”而不是“字節”),然後給新字賦值為0;最後程序計數器ip自加1,表明沒有跳轉,接著執行下一條指令。
2、DSTORE
說明:
本指令帶有一個字節的參數offset,作用是從操作數棧中讀取一個double型的值(雙字)並存放到局部變量區中的offset和offset+1位置。
實現代碼:
SELECT(DSTORE) /* Store double into local variable */
unsigned int index = ip[1];
lp[index+1] = popStack();
lp[index] = popStack();
DONE(2)
宏展開為:
case DSTORE: {
unsigned int index = GlobalState.gs_ip[1];
GlobalState.gs_lp[index+1] = *GlobalState.gs_sp --;
GlobalState.gs_lp[index] = *GlobalState.gs_sp --;
} goto next2;
首先從程序計數器的下一個字節中取出目標位置的偏移量index,然後從操作數棧中彈出兩個字分別作為double型數的底位和高位存入局部變量lp所指向的區域中的合適位置。
3、I2L
說明:
無參數,將操作數棧中的當前操作數由int型轉換為long型。
實現代碼:
SELECT(I2L) /* Convert integer to long */
long value = *(long *)sp;
#if BIG_ENDIAN
((long *)sp)[1] = value;
((long *)sp)[0] = value >> 31;
#elif LITTLE_ENDIAN || !COMPILER_SUPPORTS_LONG
((long *)sp)[1] = value >> 31;
#else
SET_LONG(sp, value);
#endif
getSP()++;
DONE(1)
由於long比int表示的范圍大,所以在擴展時多出來的高位只是用於符號擴展。先從操作數棧中取出int型整數並把它作為一個long型,如果定義了宏BIG_ENDIAN,說明操作數棧中的存儲規則是高字節在前,這時要把value的值向後移一個字作為低字來用,高字用於作符號擴展;如果操作數棧中是低位在前的話,原位置中的字不用動,只要把下一個字作符號擴展即可。最近,由於當前操作數由一個字變為兩個字,所以sp要自加1。
4、LMUL
說明:
無參數;從棧中彈出兩個long型數,相乘,然後將所得long型結果壓回棧。
實現代碼:
SELECT(LMUL) /* Mul long */
long64 rvalue = GET_LONG(sp - 1);
long64 lvalue = GET_LONG(sp - 3);
SET_LONG(sp - 3, ll_mul(lvalue, rvalue));
getSP() -= 2;
DONE(1)
先從操作數棧中分別取出兩個雙字長的長整型數,使用ll_mul()宏把它們相乘(這個宏的實現是依賴於操作系統的),然後再把相乘的結果寫入棧中。整個操作從棧中彈出了四個字而壓入兩個,在過程中指針sp都沒有變過,所以最後要把sp向前移兩個字。
以下作為例子的都是一些簡單的指令,但並不是所有指令都這樣簡單,像對象操作、異常處理和方法調用幾類指令都十分的復雜,本篇只是演示指令的原理,所以不介紹太復雜的指令。