一、背景
笔者希望通过自己动手编写一个简单的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
源码地址 http://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 ;
}
}