汇编器源码剖析
汇编器源码剖析
本文我们对一汇编器源代码进行剖析,了解汇编器实现原理,进而我们根据样例,自己实现一个汇编器。实现自己版本的汇编器放在另一篇中,本文主要是对别人的源码进行剖析。
本文源代码是来自Kevin Lynx的《基于栈的虚拟机的实现》中关于实现一个堆栈虚拟机中附带了汇编器的实现,源码下载地址如下:source code。由于本人对汇编器比较感兴趣,所以对其进行如下剖析。
汇编器主要是一个sasm.c源文件。
其中,一开始定义了一个const char* op_desc[],op_desc是一个数组,其元素类型是const char*,即op_desc是一个字符串数组,其用于存储汇编操作符。
/* map to op_type */ const char *op_desc[] = { "HALT", "IN", "OUT", "ADD", "SUB", "MUL", "DIV", "DUP", "LD", "ST", "LDC", "JLT", "JLE", "JGT", "JGE", "JEQ", "JNE", "JMP", 0 };
下面我们对各个操作符进行逐一介绍:
操作符 |
操作数个数 |
说明 |
HALT |
0,HALT |
终止 |
IN |
0,IN |
从标准输入中读入整型值并压栈 |
OUT |
0,OUT |
从栈中弹出,从标准输出 |
ADD |
0,ADD |
从栈中弹出a,弹出b,计算b+a,并将结果压入栈中 |
SUB |
0,SUB |
从栈中弹出a,弹出b,计算b-a,并将结果压入栈中 |
MUL |
0,MUL |
从栈中弹出a,弹出b,计算b*a,并将结果压入栈中 |
DIV |
0,DIV |
从栈中弹出a,弹出b,计算b/a,并将结果压入栈中 |
DUP |
0,DUP |
压入栈顶值的拷贝 |
LD |
0,LD |
从栈中弹出地址,并压入改地址里的整数值 |
ST |
0,ST |
从栈中弹出值,再弹出地址,并将该值存储到该地址中 |
LDC |
1,LDC value |
压入value |
JLT |
1,JLT loc |
弹出value,检测value是否小于0,如果小于,则pc=loc |
JLE |
1,JLE loc |
弹出value,检测value是否小于等于0,如果小于等于,则pc=loc |
JGT |
1,JGT loc |
弹出value,检测value是否大于0,如果大于,则pc=loc |
JGE |
1,JGE loc |
弹出value,检测value是否大于等于0,如果大于等于,则pc=loc |
JEQ |
1,JEQ loc |
弹出value,检测value是否等于0,如果等于,则pc=loc |
JNE |
1,JNE loc |
弹出value,检测value是否不等于0,如果不等于,则pc=loc |
JMP |
0,JMP |
不用弹出value,pc=loc |
sasm.c文件中包含了sm.h头文件,sm.h文件中定义了实际的指令op_type,该指令是枚举类型,op_type与op_desc的对应关系如下:
op_type |
op_desc |
opHalt |
HALT |
opIn |
IN |
opOut |
OUT |
opAdd |
ADD |
opSub |
SUB |
opMul |
MUL |
opDiv |
DIV |
opDup |
DUP |
opLd |
LD |
opSt |
ST |
opLdc |
LDC |
opJlt |
JLT |
opJle |
JLE |
opJgt |
JGT |
opJge |
JGE |
opJeq |
JEQ |
opJne |
JNE |
opJmp |
JMP |
opInvalid |
无效参数 |
指令为一个结构体,其定义如下:
typedef struct Instruction { int op; int arg; } Instruction;
op为操作符,arg为op对应的操作数,op可能是无参数操作码,所以arg可能无用。
sasm.c中get_op函数用于更具入参const char* s返回对应的实际的op_type,即是从op_desc到op_type的映射。
/* get op from its string desc */ int get_op( const char *s ) { int i = 0; for( ; op_desc[i] != 0; ++i ) { if( strcmp( op_desc[i], s ) == 0 ) { return i; } } return opInvalid; }
get_op函数根据const char* s的值,依次检测与op_desc中的字符串是否匹配,如果匹配则返回对应的索引值,如果不匹配则返回opInvalid,索引值i即是对应于op_type枚举值。
/* 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函数返回op需要的操作数个数。操作符的操作数个数可以参考上表列举的。这里,op对应的操作数个数只有两种情况:0和1。
void read_asm() { char line[256]; char op_str[32]; unsigned short op; int arg_c; int arg; unsigned short loc; while( !feof( fp_in ) ) { fgets( line, sizeof( line ) - 1, fp_in ); sscanf( line, "%d%s", (int*)&loc, op_str ); op = (unsigned short) get_op( op_str ); arg_c = get_operand_count( op ); if( arg_c > 0 ) { char *s = strstr( line, op_str ); s = &s[strcspn( s, " \t" )+1]; arg = atoi( s ); } else { arg = 0; } i_mem[loc].op = op; i_mem[loc].arg = arg; } }
read_asm函数用于读取汇编代码。在读取汇编代码的过程中,根据当前行的op_str值和get_op函数得到操作符op,并由get_operand_count函数来决定是否读取对应的操作数,将读取的指令复制给的指令结构体。
read_asm包含了两个字符串处理函数:strstr和strcspn:
函数 |
原型 |
功能 |
参考 |
strstr |
char* strstr(char* str1, char* str2); |
从str1中查找是否有字符串str2,如果有则返回str1中str2起始位置的指针;如果没有,则返回0 |
|
strcspn |
size_t strcspn(const char* s1, const char* s2); |
顺序在s1中搜寻与s2中字符的第一个相同字符,包括结束符0,如果存在,则返回该字符在s1中出现的位置;如果不存在,则返回s1的长度 |
关于字符串的函数,还有:strpbrk、strspn、strncmp等。
void output() { int loc = 0; int arg_c; for( ; i_mem[loc].op != opHalt; ++ loc ) { char op = (char) i_mem[loc].op; fwrite( &op, sizeof( char ), 1, fp_out ); /* op */ arg_c = get_operand_count( op ); if( arg_c > 0 ) { int arg = i_mem[loc].arg; fwrite( &arg, sizeof( arg ), 1, fp_out ); /* arg */ } } }
output函数将指令结构体数组中的指令逐个扫描,取操作码op将其输出,并根据op由get_operand_count函数得到op对应的操作数个数,如果有操作数,则将对应的操作数输出。
测试函数:
int main( int argc, char **argv ) { if( argc < 2 ) { fprintf( stderr, "Usage:%s <filename>\n", argv[0] ); exit( -1 ); } fp_in = fopen( argv[1], "r" ); if( fp_in == 0 ) { fprintf( stderr, "Open %s failed\n", argv[1] ); exit( -1 ); } { char output[256] = { 0 }; int l = strcspn( argv[1], "." ); strncpy( output, argv[1], l ); strcat( output, ".sm" ); fp_out = fopen( output, "wb" ); if( fp_out == 0 ) { fprintf( stderr, "Open %s failed\n", output ); exit( -1 ); } } read_asm(); output(); fclose( fp_in ); fclose( fp_out ); return 0; }
main函数先检测argc,截取argv[1]的文件名,并补上.sm后缀名,.sm文件是二进制文件,用于虚拟机的执行。输入输出文件都没问题后,调用read_asm和output函数将汇编代码输入,在输入的过程中利用get_op函数将汇编代码转换为二进制代码,然后利用output函数将二进制代码输出到文件中。最后,将输入输出文件都关闭。
总结
本文是对一汇编器源码进行了剖析,代码中的数据结构主要是:
op_desc:字符串数组,定义了汇编代码的指令字符串
op_type:枚举类型,标识实际的指令,由于是枚举类型,其值为0、1、2、……,指令本身的值即为0、1、2、……
Instruction:指令结构体,用来定义一个指令结构体数组。其包含两个元素:op和arg,op表示操作符,arg表示操作数,由于op最多只有一个操作数,所以arg满足要求。
代码中的一些函数:
get_op:根据字符串形式的汇编代码得到实际的指令值int型,实际也对应于枚举类型的op_type。
get_operand_count:根据实际的指令码op得到其对应的操作数个数。
read_asm:读取汇编代码,在读取汇编代码的过程中将其转换为对应的二进制代码,一个指令码占1个字节,如果有参数,其参数占4个字节大小。将指令码和操作数保存到Instruction结构体数组i_mem。
out_put:将i_mem中的指令,如果指令有操作数,则将操作数一并输出。
main:检测参数是否合法,如果合法且输入输出文件无误,则将汇编代码读入并转化,并将二进制代码输出到文件中。
以上是对该汇编器的源码剖析,接下来我们按照汇编器的实现原理,编写一个自己的汇编器,另外对反汇编器的源码进行学习,并实现一个自己版本的反汇编器。
——2013.10.4 20:18,国庆假期 于家中。