自制Java虚拟机(三)运行第一个main函数

来源:互联网 发布:数据库宿舍管理报修表 编辑:程序博客网 时间:2024/04/28 16:58

自制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循环,还好,结果是对的。

原创粉丝点击