汇编器源码剖析 - unixfy

时间:2024-03-08 07:10:34

汇编器源码剖析

汇编器源码剖析

         本文我们对一汇编器源代码进行剖析,了解汇编器实现原理,进而我们根据样例,自己实现一个汇编器。实现自己版本的汇编器放在另一篇中,本文主要是对别人的源码进行剖析。

         本文源代码是来自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

百度百科

CPLUSPLUS

ZHWEN

strcspn

size_t strcspn(const char* s1, const char* s2);

顺序在s1中搜寻与s2中字符的第一个相同字符,包括结束符0,如果存在,则返回该字符在s1中出现的位置;如果不存在,则返回s1的长度

百度百科

CPLUSPLUS

ZHWEN

         关于字符串的函数,还有: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,国庆假期 于家中。