自制Java虛擬機(三)運行第一個main函數

自制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);
}

主要做了以下幾步:

  1. 找到main方法,如果沒有則退出
  2. 初始化一個幀/棧幀
  3. 設置opcode的執行環境
  4. 執行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           // 把當前操作數棧頂的整數保存到索引爲3int型局部變量中,對應 int n=100
         3: iconst_0           // 把整數常量0 push到操作數棧
         4: istore        4    // 把當前操作數棧頂的整數保存到索引爲4int型局部變量中,對應 int sum=0
         6: iconst_1           // 把整數常量1 push到操作數棧
         7: istore        5    // 把當前操作數棧頂的整數保存到索引爲5int型局部變量中,對應 int i=1
         9: iload         5    // 把索引爲5int類型局部變量push到操作數棧,即剛纔的 i
        11: iload_3            // 把索引爲3int類型局部變量push到操作數棧,即 n
        12: if_icmpgt     28   // 對兩個int類型執行比較,如果 i>n 則跳轉到偏移爲28的指令處執行
        15: iload         4    // 把索引爲4int型局部變量push到操作數棧,即 sum
        17: iload         5    // 把索引爲5int型局部變量push到操作數棧,即 i
        19: iadd               // 兩個int類型的數相加, sum + i,兩個操作數出棧,結果入棧
        20: istore        4    // 把當前操作數棧頂的整數保存到 sum 中
        22: iinc          5, 1 // 把索引爲5的局部變量加1並保存
        25: goto          9    // 跳轉到偏移爲9的指令
        28: iload         4    // 把索引爲4int型變量push到操作數棧,即 sum
        30: i2d                // int -> double,對應 (double)(sum)
        31: iload_3            // 把索引爲3int型變量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_0iconst_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, ==)

iaddddiv指令:

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字節的算術操作,如intfloat類型,XOPL是針對8字節數的算術操作,如doublelong類型。然後根據操作數類型(intdouble)和算符(+-)定義了具體的宏。這樣其它指令也可以很方便實現,如:兩個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方法的流程,還實現了十幾條指令(iloadiconst_0istoreiaddicmpgtgoto等),執行了一個求平均值的方法,裏面有個for循環,還好,結果是對的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章