自制Java虚拟机(三)运行第一个main函数
一、执行指令的一般模型
Java虚拟机有200多条指令,用switch-case来一一匹配并执行每个指令,显得过于臃肿又不灵活。我们可以把每个指令用一个函数实现,遇到指令就调用相应的函数处理之。这个函数应该知道它所处理指令的上下文,包括当前指令位置、当前类、当前帧等,这些我们都封装在一个结构体内,通过指针传给函数。函数太多,我们把它们组织到一个数组里,以opcode的数值作为索引,因为除最后2条指令外,前203条指令都是连续的。目前为了方便调试,把处理指令的函数又放到了结构体内。如下:
typedef void Opreturn;
typedef Opreturn (*InstructionFun)(OPENV *env); // 处理指令的函数原型(这里定义一个函数指针)
typedef struct _Instruction {
const char *code_name; // 该条指令opcode的助记符
InstructionFun pre_action; // 预处理代码(主要是大端转小端)
InstructionFun action; // 实际的指令实现
} Instruction;
OPENV
是指令上下文,定义为:
typedef uchar* PC;
typedef struct _OPENV {
PC pc; // 传说中的程序计数器,这里实际上是指向代码的当前执行位置
PC pc_end;
PC pc_start;
StackFrame *current_stack;
Class *current_class;
Object *current_obj;
method_info* method;
} OPENV;
至少需要pc
(保存当前代码指针位置)、current_stack
(当前帧/栈帧)和current_class
(当前类)等字段,其它字段方便调试用。
把指令处理相关的函数放在数组里:
Instruction jvm_instructions[202] = { // 暂不考虑保留指令
{
"nop", pre_nop, do_nop},
{
"aconst_nul", pre_aconst_nul, do_aconst_nul},
{
"iconst_m1", pre_iconst_m1, do_iconst_m1},
{
"iconst_0", pre_iconst_0, do_iconst_0},
...
{
"jsr_w", pre_jsr_w, do_jsr_w}
};
这些都很有规律,可以写个脚本来生成。
然后就是执行一个方法里面的代码了,大致如下:
void runMethod(OPENV *env)
{
uchar op;
Instruction instruction;
do {
op = *(env->pc); // 取指令的opcode
instruction = jvm_instructions[op]; // 取对应的实现函数
printf("#%d: %s ", env->pc-env->pc_start, instruction.code_name);
env->pc=env->pc+1; // 移到下个位置(可能是该条指令的操作码,也可能是下一条指令)
instruction.action(env); // 执行指令
printf("\n");
} while(1);
}
跟现实世界CPU的执行指令的流程有点像。这是个死循环,不过不用担心,在return
系列指令的实现里自有办法处理。
二、执行main方法
一个Java程序的入口是main
方法。我们先从执行简单的main方法开始,找找成就感。在这个main方法里我们不创建对象,也不涉及到方法调用,类变量、实例变量等,因而只需要实现简单的指令即可。
1. 寻找main方法
首先我们要找到main
方法,可以从我们解析出来的Class结果的methods
数组中查找。
Class的结构(有省略):
typedef struct _ClassFile{
uint magic;
...
ushort constant_pool_count;
cp_info constant_pool;
...
ushort methods_count;
method_info **methods;
...
} ClassFile;
typedef ClassFile Class;
查找main
方法:
method_info* findMainMethod(Class *pclass)
{
ushort index=0;
while(index < pclass->methods_count) {
if(IS_MAI