基于栈的虚拟机源码剖析
之前我们曾剖析过一个栈虚拟机《栈虚拟机源码剖析》,并实现了一个栈虚拟机《实现一个栈虚拟机》。
本文我们对Kevin Lynx的《基于栈的虚拟机的实现》进行学习,学习其源码实现原理和技巧,其源码地址为:source code。
有关该基于栈的虚拟机说明,可以直接参考原文,我们不在此赘述。这里,我们主要是对源码进行分析学习。
该虚拟机对应两个文件:头文件sm.h和源文件sm.c。
其中,sm.h中定义了虚拟机的指令集(二进制指令集),该指令集为枚举类型:
enum op_type { opHalt, opIn, opOut, opAdd, opSub, opMul, opDiv, opDup, opLd, opSt, opLdc, opJlt, opJle, opJgt, opJge, opJeq, opJne, opJmp, opInvalid };
有关这些指令的说明,可以参考stack_machine.txt文件。
另外,还定义了指令结构体:
typedef struct Instruction { int op; int arg; } Instruction;
Instruction结构体,包含两部分:指令码和操作数。指令码最多有一个操作数。
此外,还包含了两个宏定义:
#define CODE_SIZE (1024) #define DATA_SIZE (1024)
sm.c文件中定义了堆栈:
/* the operation stack */ int op_stack[STACK_SIZE]; int op_pos = 0;
该堆栈op_stack是虚拟机处理数据的场所,op_pos是一个栈索引。堆栈元素类型为int型。
紧接着定义了指令内存:
Instruction i_mem[CODE_SIZE]; int pc;
pc相当于是指令指针寄存器。
int d_mem[DATA_SIZE];
d_men为数据内存。
enum err_code { Halt, Okay, errDivByZero, errDMem, errIMem, errStackOverflow, errStackEmpty, errUnknownOp };
err_code为枚举类型的错误码。这里不多作介绍。
void error( const char *err ) { fprintf( stderr, err ); }
error用于将错误信息输出。
/* get the op code arg(operand) count */ int get_operand_count( int op ) { int ret; switch( op ) { case opLdc: case opJlt: case opJle: case opJgt: case opJge: case opJeq: case opJne: case opJmp: ret = 1; break; default: ret = 0; } return ret; }
get_operand_count函数用于返回指令int型指令op所对应的操作数个数,op最多需要一个操作数。
int push_op_stack( int i ) { if( op_pos >= STACK_SIZE ) { error( "stack overflow" ); return -1; } op_stack[op_pos++] = i; return 0; }
push_op_stack用于压栈操作。
int pop_op_stack() { if( op_pos == 0 ) { error( "stack empty" ); return POP_ERR; } return op_stack[--op_pos]; }
pop_op_stack用于弹栈操作。
int top_op_stack() { if( op_pos == 0 ) { error( "stack empty" ); return POP_ERR; } return op_stack[op_pos-1]; }
top_op_stack用于返回栈顶元素值,不弹栈。
#define INC_P( t ) codes+=sizeof(t); size-=sizeof(t) /* read codes into the i_mem */ int read_instruction( const char *codes, int size ) { int op_count, loc = 0; Instruction inst; while( size > 0 && loc < CODE_SIZE ) { /* op is 1 byte in the code file */ inst.op = *codes; INC_P( char ); op_count = get_operand_count( inst.op ); if( op_count > 0 ) /* has arg */ { inst.arg = *(int*) codes; INC_P( int ); } else { inst.arg = 0; } i_mem[loc++] = inst; } return 1; }
read_instruction函数用于从codes读取指令,其中INC_P(t)的定义用于修改读取指令时codes和size的值。将读取到的指令码和对应的操作数存入到i_mem Instruction数组中。
int step_run() { Instruction *inst = &i_mem[pc++]; int ret = Okay;; switch( inst->op ) { case opHalt: { ret = Halt; } break; case opIn: { int i; printf( "input:" ); scanf( "%d", &i ); push_op_stack( i ); } break; case opOut: { int i = pop_op_stack(); printf( "output:%d\n", i ); } break; case opAdd: { int a = pop_op_stack(); int b = pop_op_stack(); push_op_stack( b + a ); } break; case opSub: { int a = pop_op_stack(); int b = pop_op_stack(); push_op_stack( b - a ); } break; case opMul: { int a = pop_op_stack(); int b = pop_op_stack(); push_op_stack( b * a ); } break; case opDiv: { int a = pop_op_stack(); int b = pop_op_stack(); if( a == 0 ) { return errDivByZero; } push_op_stack( b / a ); } break; case opDup: { push_op_stack( top_op_stack() ); } break; case opLd: { int addr = pop_op_stack(); if( addr < 0 || addr >= DATA_SIZE ) { error( "data memory access error" ); return errDMem; } else { push_op_stack( d_mem[addr] ); } } break; case opSt: { int val = pop_op_stack(); int addr = pop_op_stack(); if( addr < 0 || addr >= DATA_SIZE ) { error( "data memory access error" ); return errDMem; } else { d_mem[addr] = val; } } break; case opLdc: { push_op_stack( inst->arg ); } break; case opJlt: { int i = pop_op_stack(); if( i < 0 ) { pc = inst->arg; } } break; case opJle: { int i = pop_op_stack(); if( i <= 0 ) { pc = inst->arg; } } break; case opJgt: { int i = pop_op_stack(); if( i > 0 ) { pc = inst->arg; } } break; case opJge: { int i = pop_op_stack(); if( i >= 0 ) { pc = inst->arg; } } break; case opJeq: { int i = pop_op_stack(); if( i == 0 ) { pc = inst->arg; } } break; case opJne: { int i = pop_op_stack(); if( i != 0 ) { pc = inst->arg; } } break; case opJmp: { pc = inst->arg; } break; default: ret = errUnknownOp; } return ret; }
step_run函数用于执行当前指令。switch-case中虚拟机的二进制指令不作详细介绍,有关指令的定义,属于另一个专题讨论。
void run() { int ret = Okay; while( ret == Okay ) { ret = step_run(); } }
run函数用于根据step_run的执行结果,逐步执行i_mem中的指令。
int file_size( FILE *fp ) { int size; fseek( fp, 0, SEEK_SET ); fseek( fp, 0, SEEK_END ); size = ftell( fp ); fseek( fp, 0, SEEK_SET ); return size; }
两个文件函数:fseek和ftell。
函数 |
原型 |
功能 |
参考 |
fseek |
int fseek(FILE* stream, long offset, int fromwhere); |
设置文件指针stream的位置 |
|
ftell |
long ftell(FILE* stream); |
返回当前文件位置,返回FILE指针当前位置 |
file_size函数用于返回文件的大小。
extern void dasm_output( const char *file, const Instruction *insts, int size ); int main( int argc, char **argv ) { FILE *fp; char *buf; int size; if( argc < 2 ) { error( "Usage: SM <filename>" ); exit( -1 ); } fp = fopen( argv[1], "rb" ); if( fp == 0 ) { error( "Open file failed" ); exit( -1 ); } size = file_size( fp ); buf = (char*) malloc( size ); fread( buf, size, 1, fp ); read_instruction( buf, size ); if( argc > 2 ) { int dflag = atoi( argv[2] ); if( dflag ) { dasm_output( argv[1], i_mem, CODE_SIZE ); } } run(); free( buf ); fclose( fp ); return 0; }
main函数:如果参数个数argc小于2,则失败;否则读取argv[1]文件中的指令,如果argc大于2,则检测argv[2],如果为真,则将从argv[1]文件中读取出来的指令进行反汇编dasm_output,并将反汇编后的结果输出到argv[1]对应的反汇编文件中。
进而,执行run函数,执行从argv[1]中读取出来的虚拟机指令。最后将buf释放,并且将文件指针fp关闭。
总结
综上所述,实现一个基于堆栈的虚拟机主要包含以下几个重要模块:
模块 |
说明 |
虚拟机二进制指令集 |
枚举类型。有关指令集如何定义,属于另一个话题 |
指令结构体的定义 |
指令码+操作数 |
堆栈的实现 |
是虚拟机执行指令时,数据处理的场所;op_pos为堆栈的栈顶索引 |
指令内存 |
Instruction i_mem[CODE_SIZE]; 用于存放待执行的指令;pc为指令指针寄存器 |
数据内存 |
int d_mem[DATA_SIZE]; 用于存放待处理和已处理的数据 |
错误处理机制 |
虚拟机各个环节的错误处理,错误类型可以定义为枚举类型 |
读取指令 |
从文件或终端读取虚拟机的二进制指令 |
执行指令 |
根据不同指令,进行相应的操作,这些操作发生在堆栈、指令内存、数据内存之间 |
测试虚拟机 |
从读取指令,到执行执行,测试 |
以上是对实现一个基于堆栈虚拟机几个重要的模块说明,另外可以参考《实现一个堆栈虚拟机》中关于虚拟机原理的解析图。
以上是对基于栈的虚拟机的源码剖析,通过对虚拟机源码的学习,我们更进一步了解了虚拟机的实现原理,以及实现中几个重要的模块细节。下一步,我们将实现另一个版本的基于堆栈的虚拟机;另外,学习基于寄存器的虚拟机实现原理和实现一个基于寄存器的虚拟机。