自制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)

  • 帧用来保存数据和部分结果,也用来执行动态链接,给方法返回值,分发异常

  • 每次方法调用都要创建一个新的帧,调用结束时销毁

  • 每个帧有自己的局部变量数组、操作数栈和指向当前方法所在类的常量池引用

  • 对于每个线程,任意时刻只有一个帧是活跃的(就是仅对当前帧操作)

    方法调用和返回时稍有不同,因为需要通过栈来传递方法的参数和返回值,会跨帧操作(我自己的理解)

    1. 局部变量(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个字节。

    1. 操作数栈(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指向栈顶(刚开始时也是栈底)

自制Java虚拟机(二)指令、帧/栈帧_第1张图片

局部变量的操作:

#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 虚拟机指令的基础。

你可能感兴趣的:(java虚拟机)