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

做者:王智通(阿里雲安全工程師)java

1、背景

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

   它如今的功能以下:

一、java反彙編器, 山寨了javap的部分功能。
二、能解釋執行以下jvm指令:

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

 

 

   源碼地址: http://www.cloud-sec.org/jvm.tgz算法


舉2個測試例子:

test.java
=========數組

01 class aa {
02         int a = 6;
03
04         int debug(int a, int b)
05         {
06                 int sum;
07
08                 sum = a + b;
09
10                 return sum;
11         }
12 }
13
14
15 public class test {
16         public static void main(String args[]) {
17                 int a;
18
19                 aa bb = new aa();
20                 a = bb.debug(12);
21         }
22 }



test7.java
==========安全

01 public class test7 {
02         static int sub(int value)
03         {
04                 int a = 1;
05
06                 return value - 1;
07         }
08
09         static int add(int a, int b)
10         {
11                 int sum = 0;
12                 int c;
13
14                 sum = a + b;
15
16                 c = sub(sum);
17
18                 return c;
19         }
20
21         public static void main(String args[]) {
22                 int a = 1, b = 2;
23                 int ret;
24
25                 ret = add(a, b);
26                 return ;
27         }
28 }



2、JVM架構

   2個核心文件:

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

   運行時數據區:

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

   注意這裏缺乏了heap, native stack, 由於咱們如今還不支持這些功能。
   每一個method都有本身對應的棧幀stack frame, 在class文件解析的時候就已經建立好。數據結構

01 typedef struct jvm_stack_frame {
02         u1 *local_var_table;        // 本地變量表的指針
03         u1 *operand_stack;          // 操做數棧的指針
04         u4 *method;
05         u1 *return_addr;            // method調用函數的時候,保存的返回地址
06         u4 offset;                  // 操做數棧的偏移量
07         u2 max_stack;               // 本地變量表中的變量數量
08         u2 max_locals;              // 操做數棧的變量數量
09         struct jvm_stack_frame *prev_stack;    // 指向前一個棧幀結構
10 }JVM_STACK_FRAME;



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

01 int jvm_stack_init(void)
02 {
03         curr_jvm_stack = (JVM_STACK_FRAME *)malloc(sizeof(JVM_STACK_FRAME));
04         if (!curr_jvm_stack) {
05                 __error("malloc failed.");
06                 return -1;
07         }
08         memset(curr_jvm_stack, '\0'sizeof(JVM_STACK_FRAME));
09
10         jvm_stack_depth = 0;
11
12         return 0;
13 }



3、實現細節

   一、 虛擬機執行過程:
   
     初始化:jvm_init()
     從磁盤加載class文件並解析,在內存創建方法區數據結構, 初始化內存堆棧, 初始化jvm運行環境。

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

     退出: jvm_exit()
     釋放全部數據結構

   二、class文件加載與解析

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

01 typedef struct jvm_class {
02         u4 class_magic;               
03         u2 access_flag;               
04         u2 this_class;
05         u2 super_class;
06         u2 minor_version;
07         u2 major_version;
08         u2 constant_pool_count;
09         u2 interfaces_count;
10         u2 fileds_count;
11         u2 method_count;
12         char class_file[1024];
13         struct constant_info_st *constant_info;
14         struct list_head interface_list_head;
15         struct list_head filed_list_head;
16         struct list_head method_list_head;
17         struct list_head list;
18 }CLASS;


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

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

   三、解釋器設計

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

1 typedef int (*interp_func)(u2 opcode_len, char *symbol, void *base);
2
3 typedef struct bytecode_st {
4         u2 opcode;
5         u2 opcode_len;
6         char symbol[OPCODE_SYMBOL_LEN];
7         interp_func func;
8 }BYTECODE;



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

01 BYTECODE jvm_byte_code[OPCODE_LEN] = {
02                 {0x00,  1,      "nop",                  jvm_interp_nop},
03                 {0x01,  1,      "aconst_null",          jvm_interp_aconst_null},
04                 {0x02,  1,      "iconst_m1",            jvm_interp_iconst_m1},
05                 {0x03,  1,      "iconst_0",             jvm_interp_iconst_0},
06                 {0x04,  1,      "iconst_1",             jvm_interp_iconst_1},
07                 {0x05,  1,      "iconst_2",             jvm_interp_iconst_2},
08                 {0x06,  1,      "iconst_3",             jvm_interp_iconst_3},
09                 {0x07,  1,      "iconst_4",             jvm_interp_iconst_4},
10                 {0x08,  1,      "iconst_5",             jvm_interp_iconst_5},
11                 {0x09,  1,      "lconst_0",             jvm_interp_lconst_0},
12                 {0x0a,  1,      "lconst_1",             jvm_interp_lconst_1},
13                 {0x0b,  1,      "fconst_0",             jvm_interp_fconst_0},
14          ...
15                 {0xc5,  1,      "multianewarray",       jvm_interp_multianewarray},
16                 {0xc6,  1,      "ifnull",               jvm_interp_ifnull},
17                 {0xc7,  1,      "ifnonnull",            jvm_interp_ifnonnull},
18                 {0xc8,  1,      "goto_w",               jvm_interp_goto_w},
19                 {0xc9,  1,      "jsr_w",                jvm_interp_jsr_w},
20                 };
21
22 int jvm_interp_invokespecial(u2 len, char *symbol, void *base)
23 {
24         u2 index;
25
26         index = ((*(u1 *)(base + 1)) << 8) | (*(u1 *)(base + 2));
27         printf("%s #%x\n", symbol, index);
28 }
29
30 int jvm_interp_aload_0(u2 len, char *symbol, void *base)
31 {
32         printf("%s\n", symbol);
33 }
34
35 int jvm_interp_return(u2 len, char *symbol, void *base)
36 {
37         printf("%s\n", symbol);
38 }



對於一段bytecode:0x2a0xb70x00x10xb1, 手工解析以下:

0x2a表明aload_0指令, 它將本地局部變量中的第一個變量壓入到堆棧裏。這個指令自己長度就是一個字節,沒有參數, 所以0x2a的解析就很是簡單, 直接在屏幕打印出aload_0便可:

printf("%s\n", symbol);

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

1 u2 index;
2
3 index = ((*(u1 *)(base + 1)) << 8) | (*(u1 *)(base + 2));
4 printf("%s #%x\n", symbol, index);



注意0xb7解析完後,咱們要跳過3個字節的地址,那麼就是0xb1了, 它是return指令,沒有參數,所以它的解析方法跟aload_0同樣:
printf("%s\n", symbol);

用程序代碼實現是:

01 int interp_bytecode(CLASS_METHOD *method)
02 {
03         jvm_stack_depth++;                    // 函數掉用計數加1
04         curr_jvm_stack = &method->code_attr->stack_frame;    // 設置當前棧幀指針
05
06         curr_jvm_interp_env->constant_info = method->class->constant_info;    // 設置當前運行環境
07         curr_jvm_interp_env->prev_env = NULL;
08         for (;;) {
09                 if (jvm_stack_depth == 0) {            // 爲0表明全部函數執行完畢
10                         printf("interpret bytecode done.\n");
11                         break;
12                 }
13
14                 index = *(u1 *)jvm_pc.pc;            // 設置程序計數器
15                 jvm_byte_code[index].func(jvm_byte_code[index].opcode_len, // 解釋具體指令
16                         jvm_byte_code[index].symbol, jvm_pc.pc);
17                 sleep(1);
18         }
19 }


舉個例子:

01 int jvm_interp_iadd(u2 len, char *symbol, void *base)
02 {
03         u4 tmp1, tmp2;
04
05         printf("%s\n", symbol);
06
07         pop_operand_stack(int, tmp1)
08         pop_operand_stack(int, tmp2)
09
10         push_operand_stack(int, (tmp1 + tmp2))
11         jvm_pc.pc += len;
12 }



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

    下面是test7.java的執行演示:

01 public class test7 {
02         static int sub(int value)
03         {
04                 int a = 1;
05
06                 return value - 1;
07         }
08
09         static int add(int a, int b)
10         {
11                 int sum = 0;
12                 int c;
13
14                 sum = a + b;
15
16                 c = sub(sum);
17
18                 return c;
19         }
20
21         public static void main(String args[]) {
22                 int a = 1, b = 2;
23                 int ret;
24
25                 ret = add(a, b);
26                 return ;
27         }
28 }
相關文章
相關標籤/搜索