閱讀184 返回首頁    go 阿裏雲 go 技術社區[雲棲]


jvm開發筆記3—java虛擬機雛形

作者:王智通

 

一、背景

筆者希望通過自己動手編寫一個簡單的jvm來了解java虛擬機內部的工作細節畢竟hotsopt以及android的dalvik都有幾十萬行的c代碼級別。 在前麵的2篇開發筆記中已經實現了一個class文件解析器和一個java反匯編器 在這基礎上 java虛擬機的雛形也已經寫好。還沒有內存管理功能 沒有線程支持。它能解釋執行的指令取決於我的java語法範圍 在這之前我對java一無所知 通過寫這個jvm順便也把java學會了

它現在的功能如下

1、java反匯編器 山寨了javap的部分功能。
2、能解釋執行如下jvm指令

iload_n, istore_n, aload_n, astore_n, iadd, isub, bipush,
invokespecail, invokestatic, invokevirtual, goto, return,
ireturn, if_icmpge, putfiled, new, dup

 

源碼地址 https://www.cloud-sec.org/jvm.tgz
舉2個測試例子

test.java
=========

class aa {
        int a = 6;

        int debug(int a, int b)
        {
                int sum;

                sum = a + b;

                return sum;
        }
}

public class test {
        public static void main(String args[]) {
                int a;

                aa bb = new aa();
                a = bb.debug(1, 2);
        }
}

test7.java
==========

public class test7 {
        static int sub(int value)
        {
                int a = 1;

                return value - 1;
        }

        static int add(int a, int b)
        {
                int sum = 0;
                int c;

                sum = a + b;

                c = sub(sum);

                return c;
        }

        public static void main(String args[]) {
                int a = 1, b = 2;
                int ret;

                ret = add(a, b);
                return ;
        }
}

二、JVM架構

2個核心文件:

classloader.c   – 從硬盤加載class文件並解析。
interp_engine.c – bytecode解釋器。

運行時數據區

————————————————————–
| 方法區(method) | 堆棧(stack) | 程序計數器(pc) |
————————————————————–

注意這裏缺少了heap, native stack 因為我們現在還不支持這些功能。
每個method都有自己對應的棧幀stack frame 在class文件解析的時候就已經創建好。

typedef struct jvm_stack_frame {
        u1 *local_var_table;        // 本地變量表的指針
        u1 *operand_stack;          // 操作數棧的指針
        u4 *method;
        u1 *return_addr;            // method調用函數的時候保存的返回地址
        u4 offset;                  // 操作數棧的偏移量
        u2 max_stack;               // 本地變量表中的變量數量
        u2 max_locals;              // 操作數棧的變量數量
        struct jvm_stack_frame *prev_stack;    // 指向前一個棧幀結構
}JVM_STACK_FRAME;

定義了一個叫curr_jvm_stack的全局變量 它用來保存當前解釋器使用的棧幀結構 在jvm初始化的時候進行設置

int jvm_stack_init(void)
{
        curr_jvm_stack = (JVM_STACK_FRAME *)malloc(sizeof(JVM_STACK_FRAME));
        if (!curr_jvm_stack) {
                __error("malloc failed.");
                return -1;
        }
        memset(curr_jvm_stack, '', sizeof(JVM_STACK_FRAME));

        jvm_stack_depth = 0;

        return 0;
}

三、實現細節

1、 虛擬機執行過程

初始化jvm_init()
從磁盤加載class文件並解析在內存建立方法區數據結構 初始化內存堆棧 初始化jvm運行環境。

解釋器運行 jvm_run()
初始化程序計數器pc, 從方法區中查找main函數開始解釋執行。

退出 jvm_exit()
釋放所有數據結構

2、class文件加載與解析

對於每一個class文件使用CLASS數據結構表示

typedef struct jvm_class {
        u4 class_magic;                
        u2 access_flag;                
        u2 this_class;
        u2 super_class;
        u2 minor_version;
        u2 major_version;
        u2 constant_pool_count;
        u2 interfaces_count;
        u2 fileds_count;
        u2 method_count;
        char class_file[1024];
        struct constant_info_st *constant_info;
        struct list_head interface_list_head;
        struct list_head filed_list_head;
        struct list_head method_list_head;
        struct list_head list;
}CLASS;

CLASS結構的前部分是按java虛擬機規範中對class文件結構的描述設置的。 class_file保存的是這個CLASS結構對應的磁盤class文件名。constant_info保存的是class文件常量池的字符串。utf8interface_list_headfiled_list_headmethod_list_head分別是接口字段 方法的鏈表頭。

在解析class文件的時候 隻解析了java虛擬機規範中規定的一個jvm最起碼能解析的屬性。 這個部分沒什麼好說的大家直接看源碼 在對照java虛擬機規範就能看懂了。

3、解釋器設計

java虛擬機規範中一共涉及了201條指令。沒有使用switch case這種常用的算法。而是為每個jvm指令設計了一個數據結構

typedef int (*interp_func)(u2 opcode_len, char *symbol, void *base);

typedef struct bytecode_st {
        u2 opcode;
        u2 opcode_len;
        char symbol[OPCODE_SYMBOL_LEN];
        interp_func func;
}BYTECODE;

opcode是jvm指令的機器碼 opcode_len是這條jvm指令的長度symbol指令的助記符func是具體的這條指令解釋函數。事先建立了一個BYTECODE數組

BYTECODE jvm_byte_code[OPCODE_LEN] = {
                {0x00,  1,      "nop",                  jvm_interp_nop},
                {0x01,  1,      "aconst_null",          jvm_interp_aconst_null},
                {0x02,  1,      "iconst_m1",            jvm_interp_iconst_m1},
                {0x03,  1,      "iconst_0",             jvm_interp_iconst_0},
                {0x04,  1,      "iconst_1",             jvm_interp_iconst_1},
                {0x05,  1,      "iconst_2",             jvm_interp_iconst_2},
                {0x06,  1,      "iconst_3",             jvm_interp_iconst_3},
                {0x07,  1,      "iconst_4",             jvm_interp_iconst_4},
                {0x08,  1,      "iconst_5",             jvm_interp_iconst_5},
                {0x09,  1,      "lconst_0",             jvm_interp_lconst_0},
                {0x0a,  1,      "lconst_1",             jvm_interp_lconst_1},
                {0x0b,  1,      "fconst_0",             jvm_interp_fconst_0},
         ...
                {0xc5,  1,      "multianewarray",       jvm_interp_multianewarray},
                {0xc6,  1,      "ifnull",               jvm_interp_ifnull},
                {0xc7,  1,      "ifnonnull",            jvm_interp_ifnonnull},
                {0xc8,  1,      "goto_w",               jvm_interp_goto_w},
                {0xc9,  1,      "jsr_w",                jvm_interp_jsr_w},
                };

int jvm_interp_invokespecial(u2 len, char *symbol, void *base)
{
        u2 index;

        index = ((*(u1 *)(base + 1)) << 8) | (*(u1 *)(base + 2));
        printf("%s #%xn", symbol, index);
}

int jvm_interp_aload_0(u2 len, char *symbol, void *base)
{
        printf("%sn", symbol);
}

int jvm_interp_return(u2 len, char *symbol, void *base)
{
        printf("%sn", symbol);
}

對於一段bytecode0x2a0xb70x00x10xb1 手工解析如下

0x2a代表aload_0指令 它將本地局部變量中的第一個變量壓入到堆棧裏。這個指令本身長度就是一個字節沒有參數 因此0x2a的解析就非常簡單 直接在屏幕打印出aload_0即可

printf(“%sn”, symbol);

0xb7代表invokespecial 它用來調用超類構造方法實例初始化方法 私有方法。它的用法如下
invokespecial indexbyte1 indexbyte2indexbyte1和indexbyte2各占一個字節用(indexbyte1 << 8) | indexbyte2來構建一個常量池中的索引。每個jvm指令本身都占用一個字節加上它的兩個參數 invokespecial語句它將占用3個字節空間。 所以它的解析算法如下

        u2 index;

        index = ((*(u1 *)(base + 1)) << 8) | (*(u1 *)(base + 2));
        printf("%s #%xn", symbol, index);

注意0xb7解析完後我們要跳過3個字節的地址那麼就是0xb1了 它是return指令沒有參數因此它的解析方法跟aload_0一樣
printf(“%sn”, symbol);

用程序代碼實現是

int interp_bytecode(CLASS_METHOD *method)
{
        jvm_stack_depth++;                    // 函數掉用計數加1
        curr_jvm_stack = &method->code_attr->stack_frame;    // 設置當前棧幀指針

        curr_jvm_interp_env->constant_info = method->class->constant_info;    // 設置當前運行環境
        curr_jvm_interp_env->prev_env = NULL;
        for (;;) {
                if (jvm_stack_depth == 0) {            // 為0代表所有函數執行完畢
                        printf("interpret bytecode done.n");
                        break;
                }

                index = *(u1 *)jvm_pc.pc;            // 設置程序計數器
                jvm_byte_code[index].func(jvm_byte_code[index].opcode_len, // 解釋具體指令
                        jvm_byte_code[index].symbol, jvm_pc.pc);
                sleep(1);
        }
}

舉個例子

int jvm_interp_iadd(u2 len, char *symbol, void *base)
{
        u4 tmp1, tmp2;

        printf("%sn", symbol);

        pop_operand_stack(int, tmp1)
        pop_operand_stack(int, tmp2)

        push_operand_stack(int, (tmp1 + tmp2))
        jvm_pc.pc += len;
}

jvm_interp_iadd用於解釋執行iadd指令 首先從操作數棧中彈出2個int型變量tmp1, tmp2。
把tmp1 + tmp2相加後在壓入到操作數棧裏。

下麵是test7.java的執行演示

public class test7 {
        static int sub(int value)
        {
                int a = 1;

                return value - 1;
        }

        static int add(int a, int b)
        {
                int sum = 0;
                int c;

                sum = a + b;

                c = sub(sum);

                return c;
        }

        public static void main(String args[]) {
                int a = 1, b = 2;
                int ret;

                ret = add(a, b);
                return ;
        }
}

1

2

3

4

 

5

 

最後更新:2017-04-03 07:57:05

  上一篇:go [數據庫]MySQL Hash索引和B-Tree索引的區別
  下一篇:go 項目管理中的導向性 .