做者:王智通(阿里雲安全工程師)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( 1 , 2 ); |
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 |
} |