自制Java虛擬機(三)運行第一個main函數
一、執行指令的一般模型
Java虛擬機有200多條指令,用switch-case來一一匹配並執行每個指令,顯得過於臃腫又不靈活。我們可以把每個指令用一個函數實現,遇到指令就調用相應的函數處理之。這個函數應該知道它所處理指令的上下文,包括當前指令位置、當前類、當前幀等,這些我們都封裝在一個結構體內,通過指針傳給函數。函數太多,我們把它們組織到一個數組裏,以opcode的數值作爲索引,因爲除最後2條指令外,前203條指令都是連續的。目前爲了方便調試,把處理指令的函數又放到了結構體內。如下:
typedef void Opreturn;
typedef Opreturn (*InstructionFun)(OPENV *env); // 處理指令的函數原型(這裏定義一個函數指針)
typedef struct _Instruction {
const char *code_name; // 該條指令opcode的助記符
InstructionFun pre_action; // 預處理代碼(主要是大端轉小端)
InstructionFun action; // 實際的指令實現
} Instruction;
OPENV
是指令上下文,定義爲:
typedef uchar* PC;
typedef struct _OPENV {
PC pc; // 傳說中的程序計數器,這裏實際上是指向代碼的當前執行位置
PC pc_end;
PC pc_start;
StackFrame *current_stack;
Class *current_class;
Object *current_obj;
method_info* method;
} OPENV;
至少需要pc
(保存當前代碼指針位置)、current_stack
(當前幀/棧幀)和current_class
(當前類)等字段,其它字段方便調試用。
把指令處理相關的函數放在數組裏:
Instruction jvm_instructions[202] = { // 暫不考慮保留指令
{"nop", pre_nop, do_nop},
{"aconst_nul", pre_aconst_nul, do_aconst_nul},
{"iconst_m1", pre_iconst_m1, do_iconst_m1},
{"iconst_0", pre_iconst_0, do_iconst_0},
...
{"jsr_w", pre_jsr_w, do_jsr_w}
};
這些都很有規律,可以寫個腳本來生成。
然後就是執行一個方法裏面的代碼了,大致如下:
void runMethod(OPENV *env)
{
uchar op;
Instruction instruction;
do {
op = *(env->pc); // 取指令的opcode
instruction = jvm_instructions[op]; // 取對應的實現函數
printf("#%d: %s ", env->pc-env->pc_start, instruction.code_name);
env->pc=env->pc+1; // 移到下個位置(可能是該條指令的操作碼,也可能是下一條指令)
instruction.action(env); // 執行指令
printf("\n");
} while(1);
}
跟現實世界CPU的執行指令的流程有點像。這是個死循環,不過不用擔心,在return
系列指令的實現裏自有辦法處理。
二、執行main方法
一個Java程序的入口是main
方法。我們先從執行簡單的main方法開始,找找成就感。在這個main方法裏我們不創建對象,也不涉及到方法調用,類變量、實例變量等,因而只需要實現簡單的指令即可。
1. 尋找main方法
首先我們要找到main
方法,可以從我們解析出來的Class結果的methods
數組中查找。
Class的結構(有省略):
typedef struct _ClassFile{
uint magic;
...
ushort constant_pool_count;
cp_info constant_pool;
...
ushort methods_count;
method_info **methods;
...
} ClassFile;
typedef ClassFile Class;
查找main
方法:
method_info* findMainMethod(Class *pclass)
{
ushort index=0;
while(index < pclass->methods_count) {
if(IS_MAIN_METHOD(pclass, pclass->methods[index])){
break;
}
index++;
}
if (index == pclass->methods_count) {
return NULL;
}
return pclass->methods[index];
}
相關的宏定義如下:
#define get_utf8(pool) ((CONSTANT_Utf8_info*)(pool))->bytes
#define IS_MAIN_METHOD(pclass, method) (strcmp(get_utf8(pclass->constant_pool[method->name_index]), "main") == 0)
2. 執行main方法
void runMainMethod(Class *pclass)
{
StackFrame* mainStack;
OPENV mainEnv;
method_info *mainMethod;
Code_attribute* mainCode_attr;
// 1. find main method
mainMethod = findMainMethod(pclass);
if (NULL == mainMethod) {
printf("Error: cannot find main method!\n");
exit(1);
}
// 2. find the code and create a frame
mainCode_attr = (Code_attribute*)(mainMethod->code_attribute_addr);
mainStack = newStackFrame(NULL, mainCode_attr);
// 3. set the opcode executation environment
mainEnv.current_class = pclass;
mainEnv.current_stack = mainStack;
mainEnv.pc = mainCode_attr->code;
mainEnv.pc_end = mainCode_attr->code + mainCode_attr->code_length;
mainEnv.pc_start = mainCode_attr->code;
mainEnv.method = mainMethod;
// 4. run main method
runMethod(&mainEnv);
}
主要做了以下幾步:
- 找到main方法,如果沒有則退出
- 初始化一個幀/棧幀
- 設置opcode的執行環境
- 執行main方法
三、小試牛刀,實現指令
執行指令、main方法的流程是定下來了,可是指令的實際操作並沒有實現。這個艱鉅的任務就在本節來完成。java虛擬機定義的指令那麼多,逐個實現顯得過於刻板,耗時費力又讓人看不到希望。我們先寫個小程序:求1~100的整數的平均值,實現這個程序裏面的指令即可。
Average.java:
package test;
public class Average{
public static void main(String[] args) {
double avg;
int n = 100;
int sum = 0;
for (int i=1; i<=n; i++) {
sum += i;
}
avg = (double)(sum)/n;
}
}
編譯成class文件,用javap
反編譯,同時對比我們自己程序的結果,看解析的class文件對不對,還好是對的。
Average.class的常量池長這樣:
Constant pool:
#1 = Methodref #3.#14 // java/lang/Object."<init>":()V
#2 = Class #15 // test/Average
#3 = Class #16 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 main
#9 = Utf8 ([Ljava/lang/String;)V
#10 = Utf8 StackMapTable
#11 = Class #17 // "[Ljava/lang/String;"
#12 = Utf8 SourceFile
#13 = Utf8 Average.java
#14 = NameAndType #4:#5 // "<init>":()V
#15 = Utf8 test/Average
#16 = Utf8 java/lang/Object
#17 = Utf8 [Ljava/lang/String;
生成的main方法的指令如下(除去前兩行,/ / 後面是註釋):
Code:
stack=4, locals=6, args_size=1
0: bipush 100 // 把一個字節的數push到操作數棧,這個100是直接跟着bipush這個opcode的
2: istore_3 // 把當前操作數棧頂的整數保存到索引爲3的int型局部變量中,對應 int n=100
3: iconst_0 // 把整數常量0 push到操作數棧
4: istore 4 // 把當前操作數棧頂的整數保存到索引爲4的int型局部變量中,對應 int sum=0
6: iconst_1 // 把整數常量1 push到操作數棧
7: istore 5 // 把當前操作數棧頂的整數保存到索引爲5的int型局部變量中,對應 int i=1
9: iload 5 // 把索引爲5的int類型局部變量push到操作數棧,即剛纔的 i
11: iload_3 // 把索引爲3的int類型局部變量push到操作數棧,即 n
12: if_icmpgt 28 // 對兩個int類型執行比較,如果 i>n 則跳轉到偏移爲28的指令處執行
15: iload 4 // 把索引爲4的int型局部變量push到操作數棧,即 sum
17: iload 5 // 把索引爲5的int型局部變量push到操作數棧,即 i
19: iadd // 兩個int類型的數相加, sum + i,兩個操作數出棧,結果入棧
20: istore 4 // 把當前操作數棧頂的整數保存到 sum 中
22: iinc 5, 1 // 把索引爲5的局部變量加1並保存
25: goto 9 // 跳轉到偏移爲9的指令
28: iload 4 // 把索引爲4的int型變量push到操作數棧,即 sum
30: i2d // int -> double,對應 (double)(sum)
31: iload_3 // 把索引爲3的int型變量push到操作數棧,即 n
32: i2d // int -> double,這是編譯器自動添加的類型轉換
33: ddiv // 兩個double類型的數相除, sum/n,兩個操作數出棧,結果入棧
34: dstore_1 // 把棧頂的double型的數保存到索引爲1的局部變量,即 賦值給 avg
35: return // 指令執行完畢,返回(沒有返回值)
每一行的指令採用如下格式表示:
index: opcode [operand1[,operand2]]
其中,index表示該行指令的opcode在當前method代碼中的偏移量(單位是字節),opcode是操作碼的助記符,operand1, operand2表示該opcode的操作數。如 2: istore 3
,表示該行指令的opcode偏移量爲2各字節,opcode爲istore,這條指令的功能是把當前操作數棧頂的一個整數彈出,並保存到索引爲3的局部變量中)。
掐指一算,去除重複的指令,還是有十幾條。這是個艱難的過程,一步一步往前走。
bipush
指令:
Opreturn do_bipush(OPENV *env)
{
PUSH_STACK(env->current_stack,TO_BYTE(env->pc), int);
INC_PC(env->pc);
}
取值,轉換,入棧,pc
加1。由於這個字節是有符號的,所以安全起見,轉換一下。
iconst_0
和iconst_1
指令:
Opreturn do_iconst_0(OPENV *env)
{
PUSH_STACK(env->current_stack, 0, int);
}
Opreturn do_iconst_1(OPENV *env)
{
PUSH_STACK(env->current_stack, 1, int);
RETURNV;
}
除nop
指令外最簡單的指令。
istore
系列指令和dstore
指令:
Opreturn do_istore(OPENV *env)
{
int i = (int)(TO_CHAR(env->pc));
ISTORE(env, i);
INC_PC(env->pc);
}
Opreturn do_istore_3(OPENV *env)
{
ISTORE(env, 3);
}
Opreturn do_dstore(OPENV *env)
{
int i = (int)(TO_CHAR(env->pc));
DSTORE(env, i);
INC_PC(env->pc);
}
其中ISTORE
是一個宏,定義如下:
#define XSTORE(env, index, xtype) PUT_LOCAL(env->current_stack, index, PICK_STACK(env->current_stack, xtype), xtype);\
POP_STACK(env->current_stack)
#define ISTORE(env, index) XSTORE(env, index, int)
DSTORE
宏的定義:
#define XSTOREL(env, index, xtype) PUT_LOCAL(env->current_stack, index, PICK_STACKL(env->current_stack, xtype), xtype);\
POP_STACKL(env->current_stack)
#define DSTORE(env, index) XSTOREL(env, index, double)
都是先根據需要操作的字節數定義一般的宏,然後定義具體的宏。
iload
系列指令:
Opreturn do_iload(OPENV *env)
{
ushort index = (ushort)(TO_CHAR(env->pc));
ILOAD(env, index);
INC_PC(env->pc);
}
Opreturn do_iload_3(OPENV *env)
{
ILOAD(env, 3);
}
ILOAD
也被定義成一個宏:
#define XLOAD(env, index, xtype) PUSH_STACK(env->current_stack, GET_LOCAL(env->current_stack, index, xtype), xtype)
#define ILOAD(env, index) XLOAD(env, index, int)
與STORE
系列的類似
icmpgt
指令:
Opreturn do_if_icmpgt(OPENV *env)
{
ICMPGT(env);
}
ICMPGT
是個宏,定義如下:
#define ICMPXEQ(env, OP) short offset;\
int v1,v2;\
GET_STACK(env->current_stack, v2, int);\
GET_STACK(env->current_stack, v1, int);\
DEBUG_SP_DOWNL(env->dbg);\
if (v1 OP v2) {\
offset = TO_SHORT(env->pc);\
env->pc+=(offset-1);\
} else {\
INC2_PC(env->pc);\
}
#define ICMPGT(env) ICMPXEQ(env, >)
因爲jvm裏面還有一系列類似的指令,只是算符不同而已,所以定義了個一般的宏。另一個相似的指令icmpeq
,可定義如下:#define ICMPEQ(env) ICMPXEQ(env, ==)
。
iadd
和ddiv
指令:
Opreturn do_iadd(OPENV *env)
{
IADD(env);
}
Opreturn do_ddiv(OPENV *env)
{
DDIV(env);
}
它們都有幾個相關的宏:
#define XOP(env, xtype, OP) SP_DOWNL(env->current_stack);\
PUSH_STACK(env->current_stack,(PICK_STACKC(env->current_stack, xtype) OP PICK_STACKU(env->current_stack, xtype)), xtype)
#define XOPL(env, xtype, OP) SP_DOWNDL(env->current_stack);\
PUSH_STACKL(env->current_stack,(PICK_STACKC(env->current_stack, xtype) OP PICK_STACKUL(env->current_stack, xtype)), xtype)
#define IADD(env) XOP(env, int, +)
#define DDIV(env) XOPL(env, double, /)
先是定義了一般的宏,XOP
表示針對4字節的算術操作,如int
、float
類型,XOPL
是針對8字節數的算術操作,如double
、long
類型。然後根據操作數類型(int
、double
)和算符(+
、-
)定義了具體的宏。這樣其它指令也可以很方便實現,如:兩個float
類型數相加的fadd
指令可這樣定義:#define FADD(env) XOP(env,float, +)
iinc
指令:
Opreturn do_iinc(OPENV *env)
{
IINC(env);
}
也是一個宏:
#define IINC(env) GET_LOCAL(env->current_stack, TO_CHAR(env->pc), int)+=(TO_CHAR(env->pc+1));\
env->pc+=2
取值,相加,pc
往後移2個字節。
goto
:
Opreturn do_goto(OPENV *env)
{
short offset = TO_SHORT(env->pc);
env->pc+=(offset-1);
}
就是更改pc
的值,跟彙編類似。
i2d
:
Opreturn do_i2d(OPENV *env)
{
I2D(env);
}
類型轉換而已。
return
Opreturn do_return(OPENV *env)
{
exit(0);
}
簡單起見,這個指令啥也不幹,退出。
其它沒有實現的指令,留個空函數佔位,反正執行不到。
四、測試
測試代碼:
int main()
{
Class *pclass = loadClass("Average.class");
runMainMethod(pclass);
return 0;
}
由於我們的虛擬機沒有實現native方法調用(需要加載動態鏈接庫,然後調用裏面的函數。jre8/bin目錄下有很多動態鏈接庫),我們不能用System.out.print
之類的方法來打印程序執行的結果(System.out.print最終會執行一個native方法,這是由C實現的方法,由虛擬機調用)。爲方便調試,只得自己在相關代碼後面插入一些調試代碼,打印相關內容。
執行結果如下(100個循環,輸出的調試內容太多了,只截取最後幾條指令的):
OK。
五、總結
本節中,探索了虛擬機執行指令執行的一般模型,以及執行main方法的流程,還實現了十幾條指令(iload
、iconst_0
、istore
、iadd
、icmpgt
、goto
等),執行了一個求平均值的方法,裏面有個for循環,還好,結果是對的。