自制Java虚拟机(二)指令、帧/栈帧
自制Java虚拟机(二)指令、帧/栈帧
上篇文章中,我们已经成功地解析了class文件,包括其中的常量池(constant_pool)和代码(code),一个很直接的思路就是实现jvm的200多条指令,然后找到main方法,执行里面的指令。
一、初识jvm指令
一条java虚拟机由一个指明需要执行操作的opcode,以及后面跟着的0个 或多个被操作的值组成。
jvm指令是基于栈的,意味着这些指令不直接操作寄存器,实际上也没有操作寄存器的指令。
jvm的opcode都是占一个字节,所以jvm虚拟机最多可有256条opcode。目前jvm规范8中列出的opcode有0x00~0xc9(0~201),加上3个保留opcode 0xca,0xfe,0xff(202,254,255),共205条。其中前203条的编码都是连续的,很有规律。
表1 一些opcode的值与含义
opcode的十进制 | opcode的十六进制 | 助记符 | 含义 |
---|---|---|---|
0 | 0x00 | nop | 空操作 |
1 | 0x01 | aconst_null | 将一个null对象引用推到操作数栈上 |
2 | 0x02 | iconst_m1 | 将-1推到操作数栈上 |
3 | 0x03 | iconst_0 | 将0推到操作数栈上 |
… | |||
21 | 0x15 | iload | 从局部变量数组中加载一个int类型的数到操作数栈上 |
22 | 0x16 | lload | 从局部变量数组中加载一个long类型的数到操作数栈上 |
23 | 0x17 | fload | 从局部变量数组中加载一个float类型的数到操作数栈上 |
… | |||
54 | 0x36 | istore | 把当前操作数栈的栈顶上的int值保存到局部变量数组中 |
55 | 0x37 | lstore | 把当前操作数栈的栈顶上的long值保存到局部变量数组中 |
56 | 0x38 | fstore | 把当前操作数栈的栈顶上的float值保存到局部变量数组中 |
我们先看看iload
指令:
功能:从局部变量数组中加载一个int类型的值到操作数栈上
格式:iload index
操作数栈(操作前):…
操作数栈(操作后):…, value
其中index
是一个无符号的字节,解释为当前帧中局部变量数组的索引。
所以,如果遇到iload指令,就再往后读取一个字节,把这个字节转换成一个无符号的整数,然后根据这个整数从当前帧的局部变量数组中找到对应的整数value
,把它的值压栈(操作数栈,operand stack)。
istore
的操作与load
相反:
功能:把当前操作数栈顶的int值保存到局部变量数组中
格式:istore index
操作数栈(操作前):…, value
操作数栈(操作后):…
其中index
也是一个无符号的字节,解释为当前帧中局部变量数组的索引。
所以,如果遇到istore
指令,就再往后读取一个字节,把这个字节转换成一个无符号的整数,同时从操作数栈弹出一个int类型的值value
,然后根据这个整数从当前帧的局部变量数组中找到存储位置,把value存到这个位置。
jvm指令通常都涉及到局部变量数组(local variables array)、操作数栈(operand stack),这两个数据结构都存储在一个叫做帧(frame)的数据结构中。所以在实现指令之前,需要了解一下帧是什么。
二、帧(Frame)
帧用来保存数据和部分结果,也用来执行动态链接,给方法返回值,分发异常
每次方法调用都要创建一个新的帧,调用结束时销毁
每个帧有自己的局部变量数组、操作数栈和指向当前方法所在类的常量池引用
-
对于每个线程,任意时刻只有一个帧是活跃的(就是仅对当前帧操作)
方法调用和返回时稍有不同,因为需要通过栈来传递方法的参数和返回值,会跨帧操作(我自己的理解)
-
局部变量(local variables)
每个帧都包含一个局部变量数组,可以保存各种数据类型(复合数据类型除外)
局部变量数组的长度在编译时可以确定
typedef struct _Code_attribute { ushort attribute_type; ushort max_stack; ushort max_locals; // 这个就是局部变量数组的长度 uint code_length; uchar *code; ushort exception_table_length; exception_table *exceptions; ushort attributes_count; attribute_info **attributes; } Code_attribute;
变量是通过索引(下标)来寻址的,第一个局部变量的索引是0
long类型和double类型的变量需要占据两个单元
也就是说如果一个类型为long的变量的索引为0,那么下一个变量的的索引是2
- 局部变量数组也用来保存传给方法的参数,如果是实例方法(非静态),第一个局部变量(索引为0)通常是
this
指针,即所调用对象的引用。接下来的是函数参数
在Java的反射中,invoke方法的第一个参数通常都是对象(实例)。原来用
obj.method(args...)
,用反射,就变成method.invoke(obj, args...)
我们可有这样理解:局部变量数组以一个int类型的长度(4个字节)为基本单位,按索引来查找局部变量,long或double类型需要占据8个字节。
-
操作数栈(operand stack)
- 每个帧包含一个后进先出LIFO的栈,称之为操作数栈
- 操作数栈的最大深度是在编译期间确定的(Code_attribute的max_stack)
- 操作数栈可以保存jvm的任意数据类型,与局部变量数组类型,4个字节是基本单位,long和dobule占据来连续的两个单元
-
三、帧的表示、局部变量数组、操作数栈的操作实现
一个帧的基本结构如下:
typedef struct _StackFrame {
struct _StackFrame *prev;
int local_vars_count;
char* localvars;
char* sp;
char* sp_base;
} StackFrame;
其中,prev指向前一个帧,local_vars_count为局部变量数组中元素的格式,localvars指向局部变量数组,sp指向操作数栈栈顶,sp_base指向操作数栈栈底。
创建帧的代码:
StackFrame* newTestStackFrame(StackFrame* current_frame, int max_locals, int max_stack)
{
size_t total_size = sizeof(StackFrame) + ((max_locals + max_stack + 4) << 2);
StackFrame* stf = (StackFrame*)malloc(total_size);
memset(stf, 0, total_size);
stf->prev = current_frame;
stf->local_vars_count = max_locals;
stf->localvars = (char*)(stf + 1);
stf->sp = stf->localvars + ((max_locals+1) << 2);
stf->sp_base = stf->sp;
return stf;
}
首先根据StackFrame
结构体本身的大小、局部变量数和最大操作数栈深度计算需要分配的内存大小。
然后用malloc
函数申请内存,stf->localvars=(char*)(stf+1)
让localvars
指向局部变量数组的开始;
stf->sp = stf->localvars + ((max_local+1) << 2)
让sp
指向栈顶(刚开始时也是栈底)
局部变量的操作:
#define GET_LV_OFFSET(index) ((index) << 2)
#define PUT_LOCAL(stack,vindex,v,vtype) *((vtype*)(stack->localvars + GET_LV_OFFSET(vindex)))=v
#define GET_LOCAL(stack,vindex,vtype) *((vtype*)(stack->localvars + GET_LV_OFFSET(vindex)))
就是计算偏移,然后取值、赋值。顾名思义,vindex
是变量的索引,vtype
是变量的类型(int,float…)
操作数栈的操作:
#define PICK_STACKC(stack, vtype) (*(vtype*)(stack->sp))
#define PUSH_STACK(stack, v, vtype) *((vtype*)(stack->sp)) = v;\
SP_UP(stack)
#define PUSH_STACKL(stack, v, vtype) *((vtype*)(stack->sp)) = v;\
SP_UPL(stack)
#define GET_STACK(stack,result,vtype) SP_DOWN(stack);\
result=PICK_STACKC(stack,vtype)
#define GET_STACKL(stack,result,vtype) SP_DOWNL(stack);\
result=PICK_STACKC(stack,vtype)
相关的宏定义:
#define SZ_INT sizeof(int)
#define SZ_LONG (sizeof(int)<<1)
#define SP_STEP SZ_INT
#define SP_STEP_LONG SZ_LONG
#define SP_UP(stack) (stack->sp)+=SP_STEP
#define SP_DOWN(stack) (stack->sp)-=SP_STEP
#define SP_UPL(stack) (stack->sp)+=SP_STEP_LONG
#define SP_DOWNL(stack) (stack->sp)-=SP_STEP_LONG
很原始的栈操作。
局部变量的操作测试:
void testFrameLocal()
{
StackFrame* stf = newTestStackFrame(NULL, 7, 10);
int i;
short s;
long l;
float f;
double d;
PUT_LOCAL(stf, 0, 3656, int);
PUT_LOCAL(stf, 1, 16, short);
PUT_LOCAL(stf, 2, 1234567, long); // 由于long占两个单元,所以下个数的索引需要加2
PUT_LOCAL(stf, 4, 2.5f, float);
PUT_LOCAL(stf, 5, 12345.678, double);
i = GET_LOCAL(stf, 0, int);
s = GET_LOCAL(stf, 1, short);
l = GET_LOCAL(stf, 2, long);
f = GET_LOCAL(stf, 4, float);
d = GET_LOCAL(stf, 5, double);
printf("i=%d, s=%d, l=%ld, f=%f, d=%lf\n", i, s, l, f, d);
}
测试结果:
操作数栈的测试:
void tsetFrameStack()
{
StackFrame *stf = newTestStackFrame(NULL, 5, 10);
int i;
short s;
long l;
float f;
double d;
PUSH_STACK(stf, 2.67f, float);
PUSH_STACK(stf, -1234, int);
PUSH_STACK(stf, 1234567, long);
PUSH_STACK(stf, 128, short);
PUSH_STACK(stf, 4566.89, double);
GET_STACK(stf, d, double);
GET_STACK(stf, s, short);
GET_STACK(stf, l, long);
GET_STACK(stf, i, int);
GET_STACK(stf, f, float);
printf("d=%f, s=%d, l=%ld, i=%d, f=%f\n", d, s, l, i, f);
}
测试结果:
四、总结
本篇文章中,我们实现了帧的结构,以及局部变量、操作数栈的基本操作。这些是实现java 虚拟机指令的基础。
上一篇: 萌新的C++笔记
下一篇: xposed hook框架的使用(二)